@resq-systems/security 2.1.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/sanitize.d.mts.map +1 -1
- package/lib/threats/rules/markup.d.mts.map +1 -1
- package/lib/threats/rules/markup.mjs +1 -1
- package/lib/threats/rules/markup.mjs.map +1 -1
- package/lib/threats/rules/system.d.mts.map +1 -1
- package/lib/threats/rules/system.mjs +4 -4
- package/lib/threats/rules/system.mjs.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -490,7 +490,7 @@ Exported types: `ThreatDetectionResult`, `ThreatFinding`, `ThreatType`, `ThreatD
|
|
|
490
490
|
## Prerequisites
|
|
491
491
|
|
|
492
492
|
- **Runtime**: Bun 1.1+ or Node.js 20+
|
|
493
|
-
- **Peer Dependencies**: `effect` (
|
|
493
|
+
- **Peer Dependencies**: `effect` (^4.0.0), optional `jsdom` (>=22.0.0)
|
|
494
494
|
|
|
495
495
|
## Configuration
|
|
496
496
|
|
package/lib/sanitize.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sanitize.d.mts","names":[],"sources":["../src/sanitize.ts"],"mappings":";;;;;;;;KAqCK,WAAW,KAAK,OAAE,MAAM;;;;;qBAShB,mBAAiB,OAAA;;YAElB,qBAAqB,kBAAkB;;;;;qBAMtC,2BAAyB,OAAA;;;;;;;;;YAS1B,6BAA6B,0BAA0B;;;;;qBAMtD,wBAAsB,OAAA;;;;;;;YAOvB,0BAA0B,uBAAuB;qBAuBhD,eAAa,OAAA;;;;;;;;;;;;YA4Bd,UAAU;;;;;;qBAOT,uBAAqB,OAAA;;;;;;;;YAQtB,yBAAyB,sBAAsB;;;;;;;;;qBAU9C,aAAW,OAAA;;;;;;;YASZ,QAAQ;;;;;qBAMP,mBAAiB,OAAA;;;;;;;YASlB,cAAc;;;;;qBAMb,WAAS,OAAA;;;;;;;YAOV,MAAM;;;;;qBAML,kBAAgB,OAAA;;;;;;;;YAUjB,aAAa;;;;qBAKZ,YAAU,OAAA;;;;;;;YAOX,OAAO;;;;;;;;;;;;;;;qBAmBN,
|
|
1
|
+
{"version":3,"file":"sanitize.d.mts","names":[],"sources":["../src/sanitize.ts"],"mappings":";;;;;;;;KAqCK,WAAW,KAAK,OAAE,MAAM;;;;;qBAShB,mBAAiB,OAAA;;YAElB,qBAAqB,kBAAkB;;;;;qBAMtC,2BAAyB,OAAA;;;;;;;;;YAS1B,6BAA6B,0BAA0B;;;;;qBAMtD,wBAAsB,OAAA;;;;;;;YAOvB,0BAA0B,uBAAuB;qBAuBhD,eAAa,OAAA;;;;;;;;;;;;YA4Bd,UAAU;;;;;;qBAOT,uBAAqB,OAAA;;;;;;;;YAQtB,yBAAyB,sBAAsB;;;;;;;;;qBAU9C,aAAW,OAAA;;;;;;;YASZ,QAAQ;;;;;qBAMP,mBAAiB,OAAA;;;;;;;YASlB,cAAc;;;;;qBAMb,WAAS,OAAA;;;;;;;YAOV,MAAM;;;;;qBAML,kBAAgB,OAAA;;;;;;;;YAUjB,aAAa;;;;qBAKZ,YAAU,OAAA;;;;;;;YAOX,OAAO;;;;;;;;;;;;;;;qBAmBN,aAAU;;;;;;;;;;;;;;;;;;;;;;;;;;qBAsCV,oBAAiB,aAClB,4BACgB,kBACzB,KAAK,aAAa,OAAE;;;;;;;;;;;;;;;;qBA+CV,cAAW,aACZ,4BACgB;;;;;;;;;;;;;;;;;;;;qBAyGf,eAAY,cAAgB,UAAY;;;;;;;;;;;;;;;;;qBA6BxC,0BAAuB,eACtB,UACJ,qBACP,KAAK,aAAa,OAAE;;;;;;;;;;;;;;;;;qBA+DV,oBAAiB,eAAiB,oBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;qBAmFlC,sBAAuB,GAAC,oBAClB,QACV,WAAW,OACjB,OAAO,OAAO;;;;;;;;;;;;;;;;;;;;;qBA0CJ,eAAY;;;;;;;;;;;;;;;;;;;;;;qBA0CZ,YAAS;;;;;;;;;;;;;;;qBAoET,kBAAe,cACf,UACH,wBACP,KAAK,aAAa,OAAE;;;;;;;;;;;;;;;;;;;;;;;;;qBAuEV,YAAS,cACT,UACH;EACR,iBAAiB;IAAQ,SAAS;IAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;qBA0D/B,gBAAa,cACb,0BACW;;;;;;;;;;qBAyCX,eAAY,kBAAoB,SAAS;;;;;;;;;qBAYzC,eAAY,kBAAoB,SAAS;;;;;;;;;qBAYzC,aAAU,gBAAkB,OAAO;;;;;;;;;qBAYnC,aAAU,gBAAkB,OAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markup.d.mts","names":[],"sources":["../../../src/threats/rules/markup.ts"],"mappings":";;;qBAwCa,8BAA8B;;qBAmE9B,mCAAmC;;;;;;;;;qBA0KnC,iCAAiC;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"markup.d.mts","names":[],"sources":["../../../src/threats/rules/markup.ts"],"mappings":";;;qBAwCa,8BAA8B;;qBAmE9B,mCAAmC;;;;;;;;;qBA0KnC,iCAAiC;;;;;;;;;;qBAqHjC,0BAA0B"}
|
|
@@ -268,7 +268,7 @@ const PROMPT_INJECTION_RULES = [
|
|
|
268
268
|
description: "Line beginning with a conversational role label",
|
|
269
269
|
cwe: 1427,
|
|
270
270
|
primaryControl: PROMPT_CONTROL,
|
|
271
|
-
pattern:
|
|
271
|
+
pattern: /^[^\S\r\n\u2028\u2029]*(?:system|assistant|developer|tool)\s{0,8}:/im
|
|
272
272
|
},
|
|
273
273
|
{
|
|
274
274
|
id: "PROMPT-EXFIL-001",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markup.mjs","names":[],"sources":["../../../src/threats/rules/markup.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n * SPDX-License-Identifier: Apache-2.0\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @fileoverview Document- and prompt-level signatures: XML/XXE, server-side template\n * and SSI injection, LLM prompt injection, plus the small set of universal rules that\n * apply in every context because no legitimate input needs them.\n *\n * XXE and SSTI are configuration weaknesses, not filtering ones. XXE is prevented by\n * disabling DTD processing and external entity resolution in the parser; SSTI is\n * prevented by passing untrusted values as template *data* rather than splicing them\n * into template *source*. Both rule groups exist to catch a payload that a\n * misconfigured parser or a `renderString(userInput)` call would otherwise execute.\n *\n * @module @resq-systems/security/threats/rules/markup\n */\n\nimport { ALL_THREAT_CONTEXTS, type ThreatRule } from \"../types.js\";\n\n//#region XML / XXE\n\n/** Repeated across every XML rule. */\nconst XML_CONTROL =\n\t\"Disable DTD processing and external entity resolution in the XML parser (noent:false, resolveExternals:false)\";\n\n/** XML external entity and entity-expansion signatures. */\nexport const XML_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"XML-ENTITY-DECL-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Inline ENTITY declaration\",\n\t\tcwe: 611,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /<!ENTITY\\b/i,\n\t},\n\t{\n\t\tid: \"XML-EXTERNAL-ID-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"SYSTEM or PUBLIC external identifier\",\n\t\tcwe: 611,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /\\b(?:SYSTEM|PUBLIC)\\s+[\"']/,\n\t},\n\t{\n\t\tid: \"XML-DOCTYPE-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"DOCTYPE declaration in untrusted XML\",\n\t\tcwe: 611,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /<!DOCTYPE\\b/i,\n\t},\n\t{\n\t\tid: \"XML-XINCLUDE-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"XInclude directive — an entity-free route to the same file read\",\n\t\tcwe: 611,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /<\\s{0,8}xi:include\\b|www\\.w3\\.org\\/2001\\/XInclude/i,\n\t},\n\t{\n\t\tid: \"XML-CDATA-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"low\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"CDATA section, used to smuggle markup past naive filters\",\n\t\tcwe: 91,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /<!\\[CDATA\\[/,\n\t},\n];\n\n//#endregion\n\n//#region Template injection\n\n/** Repeated across most template-injection rules. */\nconst SSTI_CONTROL =\n\t\"Pass untrusted values as template data (context variables), never as template source; render with autoescaping on\";\n\n/** Server-side template injection and SSI directives. */\nexport const TEMPLATE_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"SSTI-SANDBOX-ESCAPE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Python object-graph traversal used to escape a template sandbox\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /\\b__(?:class|globals|subclasses|mro|builtins|import|base|init|reduce)__\\b/,\n\t},\n\t{\n\t\tid: \"SSTI-OGNL-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"OGNL/EL expression reaching a runtime execution class\",\n\t\tcwe: 917,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /_memberAccess|\\bRuntime\\s{0,8}\\.\\s{0,8}getRuntime|\\bProcessBuilder\\b|@java\\.lang\\b/i,\n\t},\n\t{\n\t\tid: \"SSTI-MUSTACHE-DELIMITER-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Handlebars/Jinja/Twig expression delimiters\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\t// The inner class excludes `{` as well as `}`. With only `}` excluded, an\n\t\t// input of 100 000 `{` characters makes the engine consume 200 of them at\n\t\t// every start position and then backtrack — ~40ms per scan, measured by\n\t\t// tests/regex-safety.test.ts. Excluding `{` makes the failure immediate, and\n\t\t// costs nothing: a template expression does not contain a bare `{`.\n\t\tpattern: /\\{\\{[^{}]{0,200}\\}\\}/,\n\t},\n\t{\n\t\tid: \"SSTI-STATEMENT-DELIMITER-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Jinja/Twig statement delimiters\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /\\{%[^%]{0,200}%\\}/,\n\t},\n\t{\n\t\tid: \"SSTI-EL-DELIMITER-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"JSP EL, Freemarker, or JS template-literal interpolation\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\t// `{` excluded from the inner class for the same reason as the rule above.\n\t\tpattern: /[$#]\\{[^{}]{0,200}\\}/,\n\t},\n\t{\n\t\tid: \"SSI-DIRECTIVE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\", \"html\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Server-side include directive\",\n\t\tcwe: 97,\n\t\tprimaryControl:\n\t\t\t\"Disable server-side includes, or serve untrusted content from a path with SSI turned off\",\n\t\tpattern: /<!--#\\s{0,8}(?:exec|include|echo|config|fsize|flastmod|printenv|set)\\b/i,\n\t},\n\t{\n\t\tid: \"SSTI-SCRIPTLET-DELIMITER-001\",\n\t\ttype: \"template_injection\",\n\t\t// `template` only. A scriptlet spliced into an already-rendered response does\n\t\t// not execute — only template *source* is an execution path. SSI-DIRECTIVE-001\n\t\t// is in both because Apache and nginx post-process served HTML; no equivalent\n\t\t// stage exists for ERB, JSP, ASP, or EJS.\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"ERB / JSP / ASP / EJS scriptlet delimiters\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /<%[=\\-@#!]|<%[^%]{0,200}%>/,\n\t},\n\t{\n\t\tid: \"SSTI-FREEMARKER-DIRECTIVE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"FreeMarker directive or user-defined directive call\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern:\n\t\t\t/<#\\s{0,8}(?:assign|list|if|else|import|include|macro|function|setting|attempt|global|local|nested|recurse|switch|visit|compress|noparse|outputformat)\\b|<@[\\w.$]{1,64}\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"SSTI-VELOCITY-DIRECTIVE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Velocity directive\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /#(?:set|parse|evaluate|macro|foreach|include)\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"SSTI-DOTNET-REFLECTION-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Razor/.NET expression reaching a process or assembly-loading class\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\t// A bare Razor `@(` / `@{` delimiter rule was rejected: two characters of common\n\t\t// punctuation that matched Objective-C literals and `@media` queries. This\n\t\t// targets capability reach instead, mirroring how SSTI-OGNL-001 handles Java.\n\t\t// `Process\\.Start` is required literally — the spaced form matched the sentence\n\t\t// \"Our deployment process. Start(ing) Monday\".\n\t\tpattern:\n\t\t\t/\\bSystem\\.Diagnostics\\.Process|\\bProcess\\.Start\\s{0,8}\\(|\\bActivator\\.CreateInstance\\s{0,8}\\(|\\bAssembly\\.Load\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"SSTI-NODE-REQUIRE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Node module-graph traversal reaching a process or filesystem module\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern:\n\t\t\t/\\bprocess\\s{0,8}\\.\\s{0,8}(?:mainModule|binding|constructor)\\b|\\brequire\\s{0,8}\\(\\s{0,8}[\"'](?:node:)?(?:child_process|fs|vm|os)[\"']/i,\n\t},\n\t{\n\t\tid: \"SSTI-SMARTY-PHP-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Smarty {php} tag\",\n\t\tcwe: 94,\n\t\tprimaryControl: \"Disable the {php} tag; it was removed entirely in Smarty 4\",\n\t\tpattern: /\\{\\s{0,8}\\/?\\s{0,8}php\\s{0,8}\\}/i,\n\t},\n];\n\n//#endregion\n\n//#region Prompt injection\n\n/** Repeated across every prompt-injection rule. */\nconst PROMPT_CONTROL =\n\t\"Keep untrusted content in a clearly delimited data channel, constrain tool scopes, and require confirmation for consequential tool calls\";\n\n/**\n * LLM prompt injection.\n *\n * These are the weakest signatures in the catalog by construction — prompt injection\n * is expressed in natural language, so no finite pattern set bounds it. They are\n * graded accordingly (mostly `medium` confidence) and are useful as telemetry and as\n * a tripwire for human review, never as an authorization decision.\n */\nexport const PROMPT_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"PROMPT-OVERRIDE-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Instruction to disregard prior directives\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:ignore|disregard|forget|override|bypass)\\s+(?:all\\s+|any\\s+|the\\s+|your\\s+|previous\\s+|prior\\s+|earlier\\s+|above\\s+){0,3}(?:instructions?|prompts?|rules?|directions?|guidelines?|constraints?)\\b/i,\n\t},\n\t{\n\t\tid: \"PROMPT-DELIMITER-SPOOF-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Chat-template control token forged in user content\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\tpattern: /<\\|(?:im_start|im_end|endoftext|system|user|assistant|channel)\\|>/i,\n\t},\n\t{\n\t\tid: \"PROMPT-ROLE-SPOOF-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Line beginning with a conversational role label\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\tpattern: /^\\s{0,8}(?:system|assistant|developer|tool)\\s{0,8}:/im,\n\t},\n\t{\n\t\tid: \"PROMPT-EXFIL-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Request to disclose the system prompt or hidden instructions\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:reveal|repeat|print|output|show|display|summarize)\\s+(?:me\\s+)?(?:your|the)\\s+(?:system\\s+|initial\\s+|original\\s+|hidden\\s+){0,2}(?:prompt|instructions?|rules?|directives?)\\b/i,\n\t},\n\t{\n\t\tid: \"PROMPT-TOOL-COERCION-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Directive coercing a specific tool or function invocation\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:you\\s+must|always|immediately|be\\s+sure\\s+to)\\s+(?:call|invoke|run|use|execute)\\s+(?:the\\s+)?[\\w.-]{1,64}\\s+(?:tool|function|command|api)\\b/i,\n\t},\n];\n\n//#endregion\n\n//#region Universal\n\n/**\n * Every sink. An invisible formatting character has no legitimate place in a value\n * bound to any of them.\n *\n * This list previously excluded `sql`, `shell`, `ldap` and the other machine-syntax\n * sinks, on the stated grounds that those \"get the stricter {@link UNIVERSAL_RULES}\n * bidi and control-character rules instead\". Mutation testing showed that reasoning was\n * wrong: the bidi rule matches only U+202A-202E and U+2066-2069, and the control-character\n * rule only C0/C1 — neither matches U+200B. A zero-width space inside a SQL keyword\n * therefore raised **no finding at all**, and `1 U<ZWSP>NION SELECT` scored zero.\n *\n * The severity stays `medium`/`medium` rather than rising, because at the human-text\n * sinks the same code points are orthographic in Persian and Hindi and structural in\n * every ZWJ emoji sequence. The finding is raised everywhere; the verdict is left to\n * scoring.\n */\nconst INVISIBLE_CONTEXTS = [\n\t\"general_text\",\n\t\"html\",\n\t\"identifier\",\n\t\"object_merge\",\n\t\"url\",\n\t\"url_parameter\",\n\t\"sql\",\n\t\"nosql\",\n\t\"shell\",\n\t\"filesystem\",\n\t\"ldap\",\n\t\"xpath\",\n\t\"xml\",\n\t\"template\",\n\t\"spreadsheet\",\n\t\"jwt\",\n\t\"http_header\",\n\t\"log\",\n\t\"llm_prompt\",\n] as const;\n\n/**\n * Rules that apply in every context, including `general_text`.\n *\n * The bar for membership is high: a rule belongs here only if there is no legitimate\n * reason for the character to appear in *any* user-supplied value. Bidirectional\n * overrides (the \"Trojan Source\" class, CVE-2021-42574), zero-width characters used\n * to spoof identity, and C0 control characters all clear it. SQL keywords and `../`\n * emphatically do not, which is why they are context-scoped instead.\n */\nexport const UNIVERSAL_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"UNICODE-BIDI-OVERRIDE-001\",\n\t\ttype: \"homoglyph\",\n\t\tcontexts: ALL_THREAT_CONTEXTS,\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription:\n\t\t\t\"Bidirectional override character — reorders rendered text away from its logical order\",\n\t\tcwe: 451,\n\t\tprimaryControl: \"Reject bidi controls outright, or render with them stripped\",\n\t\tpattern: /[\\u202a-\\u202e\\u2066-\\u2069]/,\n\t},\n\t{\n\t\tid: \"UNICODE-INVISIBLE-001\",\n\t\ttype: \"homoglyph\",\n\t\tcontexts: INVISIBLE_CONTEXTS,\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Zero-width or invisible formatting character\",\n\t\tcwe: 1007,\n\t\tprimaryControl: \"Strip default-ignorable code points before comparison or storage\",\n\t\tpattern: /[\\u00ad\\u200b-\\u200f\\u2060-\\u2064\\ufeff]/,\n\t},\n\t{\n\t\tid: \"UNICODE-INVISIBLE-IDENTIFIER-001\",\n\t\ttype: \"homoglyph\",\n\t\t// Scoped to identifiers alone, and graded harder than UNICODE-INVISIBLE-001.\n\t\t// ZWNJ and ZWJ are required for correct Persian and Hindi rendering and appear\n\t\t// in ordinary emoji sequences, so the general rule stays at medium/medium. In a\n\t\t// username, domain, or package name there is no such defence: an invisible\n\t\t// character exists only to make two different identifiers render alike.\n\t\tcontexts: [\"identifier\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Invisible formatting character in a protected identifier\",\n\t\tcwe: 1007,\n\t\tprimaryControl:\n\t\t\t\"Strip default-ignorable code points, then compare UTS #39 skeletons at registration time\",\n\t\tpattern: /[\\u00ad\\u200b-\\u200f\\u2060-\\u2064\\ufeff]/,\n\t},\n\t{\n\t\tid: \"CONTROL-CHAR-001\",\n\t\ttype: \"resource_abuse\",\n\t\tcontexts: ALL_THREAT_CONTEXTS,\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"C0/C1 control character other than tab, CR, or LF\",\n\t\tcwe: 74,\n\t\tprimaryControl: \"Reject or strip control characters at the trust boundary\",\n\t\t// biome-ignore lint/suspicious/noControlCharactersInRegex: detecting control characters is this rule's entire purpose\n\t\tpattern: /[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f]/,\n\t},\n];\n\n//#endregion\n"],"mappings":"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EACb,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,YAAY;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SACC;CACF;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,YAAY;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SACC;CACF;AACD;;;;;;;;;;AAqDA,MAAa,kBAAyC;CACrD;EACC,IAAI;EACJ,MAAM;EACN,UAAU;EACV,UAAU;EACV,YAAY;EACZ,aACC;EACD,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU;GA9CX;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EA4BW;EACV,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EAMN,UAAU,CAAC,YAAY;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBACC;EACD,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU;EACV,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAEhB,SAAS;CACV;AACD"}
|
|
1
|
+
{"version":3,"file":"markup.mjs","names":[],"sources":["../../../src/threats/rules/markup.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n * SPDX-License-Identifier: Apache-2.0\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @fileoverview Document- and prompt-level signatures: XML/XXE, server-side template\n * and SSI injection, LLM prompt injection, plus the small set of universal rules that\n * apply in every context because no legitimate input needs them.\n *\n * XXE and SSTI are configuration weaknesses, not filtering ones. XXE is prevented by\n * disabling DTD processing and external entity resolution in the parser; SSTI is\n * prevented by passing untrusted values as template *data* rather than splicing them\n * into template *source*. Both rule groups exist to catch a payload that a\n * misconfigured parser or a `renderString(userInput)` call would otherwise execute.\n *\n * @module @resq-systems/security/threats/rules/markup\n */\n\nimport { ALL_THREAT_CONTEXTS, type ThreatRule } from \"../types.js\";\n\n//#region XML / XXE\n\n/** Repeated across every XML rule. */\nconst XML_CONTROL =\n\t\"Disable DTD processing and external entity resolution in the XML parser (noent:false, resolveExternals:false)\";\n\n/** XML external entity and entity-expansion signatures. */\nexport const XML_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"XML-ENTITY-DECL-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Inline ENTITY declaration\",\n\t\tcwe: 611,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /<!ENTITY\\b/i,\n\t},\n\t{\n\t\tid: \"XML-EXTERNAL-ID-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"SYSTEM or PUBLIC external identifier\",\n\t\tcwe: 611,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /\\b(?:SYSTEM|PUBLIC)\\s+[\"']/,\n\t},\n\t{\n\t\tid: \"XML-DOCTYPE-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"DOCTYPE declaration in untrusted XML\",\n\t\tcwe: 611,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /<!DOCTYPE\\b/i,\n\t},\n\t{\n\t\tid: \"XML-XINCLUDE-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"XInclude directive — an entity-free route to the same file read\",\n\t\tcwe: 611,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /<\\s{0,8}xi:include\\b|www\\.w3\\.org\\/2001\\/XInclude/i,\n\t},\n\t{\n\t\tid: \"XML-CDATA-001\",\n\t\ttype: \"xml_injection\",\n\t\tcontexts: [\"xml\"],\n\t\tseverity: \"low\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"CDATA section, used to smuggle markup past naive filters\",\n\t\tcwe: 91,\n\t\tprimaryControl: XML_CONTROL,\n\t\tpattern: /<!\\[CDATA\\[/,\n\t},\n];\n\n//#endregion\n\n//#region Template injection\n\n/** Repeated across most template-injection rules. */\nconst SSTI_CONTROL =\n\t\"Pass untrusted values as template data (context variables), never as template source; render with autoescaping on\";\n\n/** Server-side template injection and SSI directives. */\nexport const TEMPLATE_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"SSTI-SANDBOX-ESCAPE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Python object-graph traversal used to escape a template sandbox\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /\\b__(?:class|globals|subclasses|mro|builtins|import|base|init|reduce)__\\b/,\n\t},\n\t{\n\t\tid: \"SSTI-OGNL-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"OGNL/EL expression reaching a runtime execution class\",\n\t\tcwe: 917,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /_memberAccess|\\bRuntime\\s{0,8}\\.\\s{0,8}getRuntime|\\bProcessBuilder\\b|@java\\.lang\\b/i,\n\t},\n\t{\n\t\tid: \"SSTI-MUSTACHE-DELIMITER-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Handlebars/Jinja/Twig expression delimiters\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\t// The inner class excludes `{` as well as `}`. With only `}` excluded, an\n\t\t// input of 100 000 `{` characters makes the engine consume 200 of them at\n\t\t// every start position and then backtrack — ~40ms per scan, measured by\n\t\t// tests/regex-safety.test.ts. Excluding `{` makes the failure immediate, and\n\t\t// costs nothing: a template expression does not contain a bare `{`.\n\t\tpattern: /\\{\\{[^{}]{0,200}\\}\\}/,\n\t},\n\t{\n\t\tid: \"SSTI-STATEMENT-DELIMITER-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Jinja/Twig statement delimiters\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /\\{%[^%]{0,200}%\\}/,\n\t},\n\t{\n\t\tid: \"SSTI-EL-DELIMITER-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"JSP EL, Freemarker, or JS template-literal interpolation\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\t// `{` excluded from the inner class for the same reason as the rule above.\n\t\tpattern: /[$#]\\{[^{}]{0,200}\\}/,\n\t},\n\t{\n\t\tid: \"SSI-DIRECTIVE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\", \"html\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Server-side include directive\",\n\t\tcwe: 97,\n\t\tprimaryControl:\n\t\t\t\"Disable server-side includes, or serve untrusted content from a path with SSI turned off\",\n\t\tpattern: /<!--#\\s{0,8}(?:exec|include|echo|config|fsize|flastmod|printenv|set)\\b/i,\n\t},\n\t{\n\t\tid: \"SSTI-SCRIPTLET-DELIMITER-001\",\n\t\ttype: \"template_injection\",\n\t\t// `template` only. A scriptlet spliced into an already-rendered response does\n\t\t// not execute — only template *source* is an execution path. SSI-DIRECTIVE-001\n\t\t// is in both because Apache and nginx post-process served HTML; no equivalent\n\t\t// stage exists for ERB, JSP, ASP, or EJS.\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"ERB / JSP / ASP / EJS scriptlet delimiters\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /<%[=\\-@#!]|<%[^%]{0,200}%>/,\n\t},\n\t{\n\t\tid: \"SSTI-FREEMARKER-DIRECTIVE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"FreeMarker directive or user-defined directive call\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern:\n\t\t\t/<#\\s{0,8}(?:assign|list|if|else|import|include|macro|function|setting|attempt|global|local|nested|recurse|switch|visit|compress|noparse|outputformat)\\b|<@[\\w.$]{1,64}\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"SSTI-VELOCITY-DIRECTIVE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Velocity directive\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern: /#(?:set|parse|evaluate|macro|foreach|include)\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"SSTI-DOTNET-REFLECTION-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Razor/.NET expression reaching a process or assembly-loading class\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\t// A bare Razor `@(` / `@{` delimiter rule was rejected: two characters of common\n\t\t// punctuation that matched Objective-C literals and `@media` queries. This\n\t\t// targets capability reach instead, mirroring how SSTI-OGNL-001 handles Java.\n\t\t// `Process\\.Start` is required literally — the spaced form matched the sentence\n\t\t// \"Our deployment process. Start(ing) Monday\".\n\t\tpattern:\n\t\t\t/\\bSystem\\.Diagnostics\\.Process|\\bProcess\\.Start\\s{0,8}\\(|\\bActivator\\.CreateInstance\\s{0,8}\\(|\\bAssembly\\.Load\\s{0,8}\\(/i,\n\t},\n\t{\n\t\tid: \"SSTI-NODE-REQUIRE-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Node module-graph traversal reaching a process or filesystem module\",\n\t\tcwe: 1336,\n\t\tprimaryControl: SSTI_CONTROL,\n\t\tpattern:\n\t\t\t/\\bprocess\\s{0,8}\\.\\s{0,8}(?:mainModule|binding|constructor)\\b|\\brequire\\s{0,8}\\(\\s{0,8}[\"'](?:node:)?(?:child_process|fs|vm|os)[\"']/i,\n\t},\n\t{\n\t\tid: \"SSTI-SMARTY-PHP-001\",\n\t\ttype: \"template_injection\",\n\t\tcontexts: [\"template\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Smarty {php} tag\",\n\t\tcwe: 94,\n\t\tprimaryControl: \"Disable the {php} tag; it was removed entirely in Smarty 4\",\n\t\tpattern: /\\{\\s{0,8}\\/?\\s{0,8}php\\s{0,8}\\}/i,\n\t},\n];\n\n//#endregion\n\n//#region Prompt injection\n\n/** Repeated across every prompt-injection rule. */\nconst PROMPT_CONTROL =\n\t\"Keep untrusted content in a clearly delimited data channel, constrain tool scopes, and require confirmation for consequential tool calls\";\n\n/**\n * LLM prompt injection.\n *\n * These are the weakest signatures in the catalog by construction — prompt injection\n * is expressed in natural language, so no finite pattern set bounds it. They are\n * graded accordingly (mostly `medium` confidence) and are useful as telemetry and as\n * a tripwire for human review, never as an authorization decision.\n */\nexport const PROMPT_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"PROMPT-OVERRIDE-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Instruction to disregard prior directives\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:ignore|disregard|forget|override|bypass)\\s+(?:all\\s+|any\\s+|the\\s+|your\\s+|previous\\s+|prior\\s+|earlier\\s+|above\\s+){0,3}(?:instructions?|prompts?|rules?|directions?|guidelines?|constraints?)\\b/i,\n\t},\n\t{\n\t\tid: \"PROMPT-DELIMITER-SPOOF-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Chat-template control token forged in user content\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\tpattern: /<\\|(?:im_start|im_end|endoftext|system|user|assistant|channel)\\|>/i,\n\t},\n\t{\n\t\tid: \"PROMPT-ROLE-SPOOF-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Line beginning with a conversational role label\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\t// The leading run is unbounded, because a reader that strips indentation strips all\n\t\t// of it, and horizontal. Under `/m`, `^` matches after every line terminator, so a\n\t\t// run that could cross one would rescan the rest of the input from each line start\n\t\t// and go quadratic. `\\s` holds all four terminators (LF, CR, U+2028, U+2029), so the\n\t\t// class removes each; the run then never leaves its line and the scan stays linear.\n\t\tpattern: /^[^\\S\\r\\n\\u2028\\u2029]*(?:system|assistant|developer|tool)\\s{0,8}:/im,\n\t},\n\t{\n\t\tid: \"PROMPT-EXFIL-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Request to disclose the system prompt or hidden instructions\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:reveal|repeat|print|output|show|display|summarize)\\s+(?:me\\s+)?(?:your|the)\\s+(?:system\\s+|initial\\s+|original\\s+|hidden\\s+){0,2}(?:prompt|instructions?|rules?|directives?)\\b/i,\n\t},\n\t{\n\t\tid: \"PROMPT-TOOL-COERCION-001\",\n\t\ttype: \"prompt_injection\",\n\t\tcontexts: [\"llm_prompt\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Directive coercing a specific tool or function invocation\",\n\t\tcwe: 1427,\n\t\tprimaryControl: PROMPT_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:you\\s+must|always|immediately|be\\s+sure\\s+to)\\s+(?:call|invoke|run|use|execute)\\s+(?:the\\s+)?[\\w.-]{1,64}\\s+(?:tool|function|command|api)\\b/i,\n\t},\n];\n\n//#endregion\n\n//#region Universal\n\n/**\n * Every sink. An invisible formatting character has no legitimate place in a value\n * bound to any of them.\n *\n * This list previously excluded `sql`, `shell`, `ldap` and the other machine-syntax\n * sinks, on the stated grounds that those \"get the stricter {@link UNIVERSAL_RULES}\n * bidi and control-character rules instead\". Mutation testing showed that reasoning was\n * wrong: the bidi rule matches only U+202A-202E and U+2066-2069, and the control-character\n * rule only C0/C1 — neither matches U+200B. A zero-width space inside a SQL keyword\n * therefore raised **no finding at all**, and `1 U<ZWSP>NION SELECT` scored zero.\n *\n * The severity stays `medium`/`medium` rather than rising, because at the human-text\n * sinks the same code points are orthographic in Persian and Hindi and structural in\n * every ZWJ emoji sequence. The finding is raised everywhere; the verdict is left to\n * scoring.\n */\nconst INVISIBLE_CONTEXTS = [\n\t\"general_text\",\n\t\"html\",\n\t\"identifier\",\n\t\"object_merge\",\n\t\"url\",\n\t\"url_parameter\",\n\t\"sql\",\n\t\"nosql\",\n\t\"shell\",\n\t\"filesystem\",\n\t\"ldap\",\n\t\"xpath\",\n\t\"xml\",\n\t\"template\",\n\t\"spreadsheet\",\n\t\"jwt\",\n\t\"http_header\",\n\t\"log\",\n\t\"llm_prompt\",\n] as const;\n\n/**\n * Rules that apply in every context, including `general_text`.\n *\n * The bar for membership is high: a rule belongs here only if there is no legitimate\n * reason for the character to appear in *any* user-supplied value. Bidirectional\n * overrides (the \"Trojan Source\" class, CVE-2021-42574), zero-width characters used\n * to spoof identity, and C0 control characters all clear it. SQL keywords and `../`\n * emphatically do not, which is why they are context-scoped instead.\n */\nexport const UNIVERSAL_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"UNICODE-BIDI-OVERRIDE-001\",\n\t\ttype: \"homoglyph\",\n\t\tcontexts: ALL_THREAT_CONTEXTS,\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription:\n\t\t\t\"Bidirectional override character — reorders rendered text away from its logical order\",\n\t\tcwe: 451,\n\t\tprimaryControl: \"Reject bidi controls outright, or render with them stripped\",\n\t\tpattern: /[\\u202a-\\u202e\\u2066-\\u2069]/,\n\t},\n\t{\n\t\tid: \"UNICODE-INVISIBLE-001\",\n\t\ttype: \"homoglyph\",\n\t\tcontexts: INVISIBLE_CONTEXTS,\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Zero-width or invisible formatting character\",\n\t\tcwe: 1007,\n\t\tprimaryControl: \"Strip default-ignorable code points before comparison or storage\",\n\t\tpattern: /[\\u00ad\\u200b-\\u200f\\u2060-\\u2064\\ufeff]/,\n\t},\n\t{\n\t\tid: \"UNICODE-INVISIBLE-IDENTIFIER-001\",\n\t\ttype: \"homoglyph\",\n\t\t// Scoped to identifiers alone, and graded harder than UNICODE-INVISIBLE-001.\n\t\t// ZWNJ and ZWJ are required for correct Persian and Hindi rendering and appear\n\t\t// in ordinary emoji sequences, so the general rule stays at medium/medium. In a\n\t\t// username, domain, or package name there is no such defence: an invisible\n\t\t// character exists only to make two different identifiers render alike.\n\t\tcontexts: [\"identifier\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Invisible formatting character in a protected identifier\",\n\t\tcwe: 1007,\n\t\tprimaryControl:\n\t\t\t\"Strip default-ignorable code points, then compare UTS #39 skeletons at registration time\",\n\t\tpattern: /[\\u00ad\\u200b-\\u200f\\u2060-\\u2064\\ufeff]/,\n\t},\n\t{\n\t\tid: \"CONTROL-CHAR-001\",\n\t\ttype: \"resource_abuse\",\n\t\tcontexts: ALL_THREAT_CONTEXTS,\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"C0/C1 control character other than tab, CR, or LF\",\n\t\tcwe: 74,\n\t\tprimaryControl: \"Reject or strip control characters at the trust boundary\",\n\t\t// biome-ignore lint/suspicious/noControlCharactersInRegex: detecting control characters is this rule's entire purpose\n\t\tpattern: /[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\u007f]/,\n\t},\n];\n\n//#endregion\n"],"mappings":"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EACb,KAAK;EACL,gBAAgB;EAMhB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,YAAY;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SACC;CACF;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,YAAY;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SACC;CACF;AACD;;;;;;;;;;AAqDA,MAAa,kBAAyC;CACrD;EACC,IAAI;EACJ,MAAM;EACN,UAAU;EACV,UAAU;EACV,YAAY;EACZ,aACC;EACD,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU;GA9CX;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EA4BW;EACV,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EAMN,UAAU,CAAC,YAAY;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBACC;EACD,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU;EACV,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAEhB,SAAS;CACV;AACD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system.d.mts","names":[],"sources":["../../../src/threats/rules/system.ts"],"mappings":";;;qBA6Ca,kCAAkC;;qBA8HlC,+BAA+B;;qBA2J/B,+BAA+B;;;;;;;;
|
|
1
|
+
{"version":3,"file":"system.d.mts","names":[],"sources":["../../../src/threats/rules/system.ts"],"mappings":";;;qBA6Ca,kCAAkC;;qBA8HlC,+BAA+B;;qBA2J/B,+BAA+B;;;;;;;;qBAsF/B,qBAAqB"}
|
|
@@ -270,7 +270,7 @@ const FILE_INCLUSION_RULES = [
|
|
|
270
270
|
description: "Remote scheme where a local path was expected",
|
|
271
271
|
cwe: 98,
|
|
272
272
|
primaryControl: RFI_CONTROL,
|
|
273
|
-
pattern: /(?:^\s
|
|
273
|
+
pattern: /(?:^\s*|[/\\=])(?:https?|ftps?|smb|cifs|nfs|webdav|ssh2|rar|zlib|compress\.(?:zlib|bzip2)):\/\//i
|
|
274
274
|
},
|
|
275
275
|
{
|
|
276
276
|
id: "RFI-DATA-URI-001",
|
|
@@ -281,7 +281,7 @@ const FILE_INCLUSION_RULES = [
|
|
|
281
281
|
description: "data: URI where a local path was expected",
|
|
282
282
|
cwe: 98,
|
|
283
283
|
primaryControl: RFI_CONTROL,
|
|
284
|
-
pattern: /(?:^\s
|
|
284
|
+
pattern: /(?:^\s*|[/\\=])data:(?:\/\/)?(?:[a-z][a-z0-9+.-]{0,32}\/[a-z0-9+.-]{1,32}[a-z0-9;=+.-]{0,32}|;base64|),/i
|
|
285
285
|
},
|
|
286
286
|
{
|
|
287
287
|
id: "RFI-REMOTE-HOST-PATH-001",
|
|
@@ -292,7 +292,7 @@ const FILE_INCLUSION_RULES = [
|
|
|
292
292
|
description: "Protocol-relative or UNC host path where a local path was expected",
|
|
293
293
|
cwe: 98,
|
|
294
294
|
primaryControl: RFI_CONTROL,
|
|
295
|
-
pattern: /^\s
|
|
295
|
+
pattern: /^\s*(?:\/\/|\\\\)[a-z0-9][a-z0-9-]{0,62}(?:\.[a-z0-9-]{1,63}){1,4}[/\\]/i
|
|
296
296
|
}
|
|
297
297
|
];
|
|
298
298
|
/** Repeated across every SSRF rule. */
|
|
@@ -369,7 +369,7 @@ const SSRF_RULES = [
|
|
|
369
369
|
description: "Non-HTTP scheme in a fetchable URL",
|
|
370
370
|
cwe: 918,
|
|
371
371
|
primaryControl: SSRF_CONTROL,
|
|
372
|
-
pattern: /^\s
|
|
372
|
+
pattern: /^\s*(?:file|gopher|dict|tftp|ldaps?|jar|netdoc|sftp):/i
|
|
373
373
|
},
|
|
374
374
|
{
|
|
375
375
|
id: "SSRF-USERINFO-INTERNAL-HOST-001",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system.mjs","names":[],"sources":["../../../src/threats/rules/system.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n * SPDX-License-Identifier: Apache-2.0\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @fileoverview Host-facing signatures: OS command injection, path traversal, file\n * inclusion, and server-side request forgery.\n *\n * Two of these categories are only weakly served by signatures, and the rules say so\n * in their `primaryControl`:\n *\n * - **Path traversal** is prevented by canonicalizing the candidate path and\n * verifying it stays beneath the allowed base directory — see `resolveContainedPath`\n * in `@resq-systems/security/paths`. Detection catches `../`; it does not catch a\n * symlink, nor an absolute path that happens to resolve inside the base.\n * - **SSRF** is prevented by parsing the URL, resolving the host, rejecting private\n * and link-local ranges, re-validating after every redirect, and constraining\n * network egress. A regex over the URL string catches a literal `127.0.0.1` but\n * never a hostname whose DNS record points there.\n *\n * @module @resq-systems/security/threats/rules/system\n */\n\nimport type { ThreatRule } from \"../types.js\";\n\n//#region Command injection\n\n/** Repeated across every command-injection rule. */\nconst CMD_CONTROL =\n\t\"Spawn with an argv array and shell:false; never build a shell string from input\";\n\n/** OS command injection. Scoped to the `shell` context — never runs elsewhere. */\nexport const COMMAND_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"CMD-SUBSTITUTION-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Shell command substitution $(...)\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\t// `(` excluded alongside `)` so a run of `$(` cannot make the engine consume\n\t\t// 200 characters and backtrack at every position. A nested `$(a $(b))` still\n\t\t// matches on the inner substitution.\n\t\tpattern: /\\$\\([^()]{1,200}\\)/,\n\t},\n\t{\n\t\tid: \"CMD-BACKTICK-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Backtick command substitution\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern: /`[^`]{1,200}`/,\n\t},\n\t{\n\t\tid: \"CMD-CHAIN-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Command separator followed by a state-changing or network command\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern:\n\t\t\t/[;&|]{1,2}\\s{0,8}(?:rm|del|cat|type|wget|curl|nc|ncat|bash|sh|zsh|powershell|pwsh|python[23]?|perl|ruby|chmod|chown|kill|shutdown|reboot|mkfifo|base64|openssl)\\b/i,\n\t},\n\t{\n\t\tid: \"CMD-PIPE-SHELL-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Output piped into a shell interpreter\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern: /\\|\\s{0,8}(?:sh|bash|zsh|dash|cmd|powershell|pwsh)\\b/i,\n\t},\n\t{\n\t\tid: \"CMD-REDIRECT-SYSTEM-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Redirection into a system directory\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern: />\\s{0,8}\\/(?:etc|dev|proc|sys|boot)\\//,\n\t},\n\t{\n\t\t// Commix documents both as filter bypasses against defences that block spaces:\n\t\t// each expands to a word separator, so a command survives a filter that only\n\t\t// looks for whitespace between tokens.\n\t\tid: \"CMD-WORD-SPLIT-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Shell word-splitting construct standing in for a space\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern: /\\$\\{IFS\\}|\\{\\s{0,4},\\s{0,4}\\}/,\n\t},\n\t{\n\t\t// The shell removes an empty quote pair before execution, so a command name split\n\t\t// by one still runs while defeating a literal match on that name.\n\t\t//\n\t\t// Deliberately narrower than Commix's full set: the equivalent backslash form is\n\t\t// NOT matched, because it is indistinguishable from an ordinary Windows-style\n\t\t// relative path, and a rule firing on those would be disabled by the first person\n\t\t// it inconvenienced.\n\t\tid: \"CMD-QUOTE-SPLIT-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Empty quote pair inside a word, removed by the shell before execution\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern: /[a-z](?:\"\"|'')[a-z]/i,\n\t},\n\t{\n\t\tid: \"CMD-ENV-EXPANSION-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"Shell parameter expansion\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\t// `{` excluded alongside `}` for the same backtracking reason as above.\n\t\tpattern: /\\$\\{[^{}]{1,200}\\}/,\n\t},\n\t{\n\t\tid: \"CMD-ARG-NEWLINE-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Line break in a value destined for a command line\",\n\t\tcwe: 88,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern: /[\\r\\n]/,\n\t},\n];\n\n//#endregion\n\n//#region Path traversal\n\n/** Repeated across every path-traversal rule. */\nconst PATH_CONTROL =\n\t\"Resolve the candidate against the base directory and verify containment (resolveContainedPath); resolve symlinks before use\";\n\n/** Directory-traversal and sensitive-path signatures. Scoped to `filesystem`. */\nexport const PATH_TRAVERSAL_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"PATH-TRAVERSAL-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Parent-directory segment\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\t// Anchored to segment boundaries so `my..file/` and `v1..v2` do not fire.\n\t\tpattern: /(?:^|[/\\\\])\\.\\.(?:[/\\\\]|$)/,\n\t},\n\t{\n\t\tid: \"PATH-TRAVERSAL-ENCODED-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Percent-encoded parent-directory traversal\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\t// The previous catalog wrote `/%2e%2e[%2f%5c]/`, whose character class matches\n\t\t// a single character from {%, 2, f, 5, c} — so it fired on `%2e%2e%` and on\n\t\t// `%2e%2e2`, and expressed nothing about the intended separator alternation.\n\t\tpattern: /%2e%2e(?:%2f|%5c|\\/|\\\\)/i,\n\t\tvariants: [\"raw\"],\n\t},\n\t{\n\t\tid: \"PATH-TRAVERSAL-MIXED-ENCODED-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Literal dots with a percent-encoded separator\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\tpattern: /\\.\\.(?:%2f|%5c)/i,\n\t\tvariants: [\"raw\"],\n\t},\n\t{\n\t\tid: \"PATH-TRAVERSAL-DOUBLE-ENCODED-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Double percent-encoded parent-directory traversal\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\tpattern: /%252e%252e(?:%252f|%255c)/i,\n\t\tvariants: [\"raw\"],\n\t},\n\t{\n\t\tid: \"PATH-TRAVERSAL-OVERLONG-UTF8-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Overlong UTF-8 encoding of a path separator\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\tpattern: /%c0%(?:af|2f)|%e0%80%af|%c1%9c/i,\n\t\tvariants: [\"raw\"],\n\t},\n\t{\n\t\tid: \"PATH-NULL-BYTE-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"NUL byte, used to truncate a path before its extension check\",\n\t\tcwe: 158,\n\t\tprimaryControl: PATH_CONTROL,\n\t\t// biome-ignore lint/suspicious/noControlCharactersInRegex: matching the NUL byte is this rule's entire purpose\n\t\tpattern: /%00|\\u0000/,\n\t},\n\t{\n\t\tid: \"PATH-TRAVERSAL-FILTER-STRIP-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Dot run that survives a naive ../ stripping filter\",\n\t\tcwe: 22,\n\t\tprimaryControl:\n\t\t\t\"Resolve the candidate against the base directory and verify containment (resolveContainedPath). Never sanitize by stripping '../' — stripping is precisely what this payload targets: '....//' becomes '../' after one pass.\",\n\t\t// PATH-TRAVERSAL-001 requires exactly two dots between boundaries, so `....//`\n\t\t// and `.../` walked past it. Bounded at 200 because a longer run is already\n\t\t// reported by the engine's repetition check.\n\t\tpattern: /(?:^|[/\\\\])\\.{3,200}[/\\\\]/,\n\t},\n\t{\n\t\tid: \"PATH-TRAVERSAL-PARAM-BYPASS-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Path parameter segment used to bypass a gateway path ACL\",\n\t\tcwe: 22,\n\t\tprimaryControl:\n\t\t\t\"Ensure the proxy and the origin agree on path normalization before authorization is applied; resolve and verify containment with resolveContainedPath\",\n\t\t// `..;/` — Tomcat and Spring treat `;` as starting a path parameter, so the\n\t\t// segment normalizes to `..` at the origin while the gateway sees a literal name.\n\t\tpattern: /(?:^|[/\\\\])\\.\\.;[^/\\\\]{0,64}[/\\\\]/,\n\t},\n\t{\n\t\tid: \"PATH-SENSITIVE-UNIX-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Reference to a sensitive Unix path\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\tpattern: /\\/(?:etc\\/(?:passwd|shadow|sudoers|hosts)|proc\\/self|root\\/\\.ssh)\\b/i,\n\t},\n\t{\n\t\tid: \"PATH-SENSITIVE-WINDOWS-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Reference to a sensitive Windows path\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\t// Requires a system subdirectory. The old rule was a bare `/C:\\\\Windows/gi`,\n\t\t// which fired on any documentation that merely mentioned the folder.\n\t\tpattern: /[a-z]:[\\\\/]{1,2}(?:windows|winnt)[\\\\/]{1,2}(?:system32|win\\.ini|repair)/i,\n\t},\n\t{\n\t\tid: \"PATH-ABSOLUTE-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"Absolute path where a relative one is expected\",\n\t\tcwe: 36,\n\t\tprimaryControl: PATH_CONTROL,\n\t\tpattern: /^(?:\\/|[a-z]:[\\\\/]|\\\\\\\\)/i,\n\t},\n];\n\n//#endregion\n\n//#region File inclusion\n\n/** Repeated across every file-inclusion rule. */\nconst LFI_CONTROL =\n\t\"Map the request to a fixed allowlist of loadable resources; never derive a module or template path from input\";\n\n/** Adds the remote-fetch half of the control, for the RFI rules. */\nconst RFI_CONTROL =\n\t\"Map the request to a fixed allowlist of loadable resources; never derive a module, template, or file path from input. Disable remote stream wrappers (PHP allow_url_fopen/allow_url_include) and reject any candidate not relative to the configured base directory\";\n\n/** Local/remote file inclusion via stream wrappers and alternate schemes. */\nexport const FILE_INCLUSION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"LFI-PHP-WRAPPER-001\",\n\t\ttype: \"file_inclusion\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"PHP stream wrapper used to read or execute source\",\n\t\tcwe: 98,\n\t\tprimaryControl: LFI_CONTROL,\n\t\tpattern: /\\bphp:\\/\\/(?:filter|input|memory|temp|stdin)/i,\n\t},\n\t{\n\t\tid: \"LFI-WRAPPER-001\",\n\t\ttype: \"file_inclusion\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Alternate stream wrapper capable of loading remote or archived content\",\n\t\tcwe: 98,\n\t\tprimaryControl: LFI_CONTROL,\n\t\tpattern: /\\b(?:expect|zip|phar|glob|dict|gopher|netdoc|jar|ogg):\\/\\//i,\n\t},\n\t{\n\t\tid: \"RFI-REMOTE-SCHEME-001\",\n\t\ttype: \"file_inclusion\",\n\t\t// `filesystem` only. The SSRF rules already own `url`; this is about a value\n\t\t// that was supposed to be a *path* and instead names a remote resource.\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Remote scheme where a local path was expected\",\n\t\tcwe: 98,\n\t\tprimaryControl: RFI_CONTROL,\n\t\tpattern:\n\t\t\t/(?:^\\s{0,8}|[/\\\\=])(?:https?|ftps?|smb|cifs|nfs|webdav|ssh2|rar|zlib|compress\\.(?:zlib|bzip2)):\\/\\//i,\n\t},\n\t{\n\t\tid: \"RFI-DATA-URI-001\",\n\t\ttype: \"file_inclusion\",\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"data: URI where a local path was expected\",\n\t\tcwe: 98,\n\t\tprimaryControl: RFI_CONTROL,\n\t\t// The class is ordered `[a-z0-9+.-]` deliberately: writing `[a-z0-9.+-]` embeds\n\t\t// the literal substring `.+`, which the ReDoS structural test rejects.\n\t\tpattern:\n\t\t\t/(?:^\\s{0,8}|[/\\\\=])data:(?:\\/\\/)?(?:[a-z][a-z0-9+.-]{0,32}\\/[a-z0-9+.-]{1,32}[a-z0-9;=+.-]{0,32}|;base64|),/i,\n\t},\n\t{\n\t\tid: \"RFI-REMOTE-HOST-PATH-001\",\n\t\ttype: \"file_inclusion\",\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Protocol-relative or UNC host path where a local path was expected\",\n\t\tcwe: 98,\n\t\tprimaryControl: RFI_CONTROL,\n\t\t// Re-grades rather than newly detects: PATH-ABSOLUTE-001 already matches these\n\t\t// at low confidence, contributing 1.0 — not enough to leave the allow band.\n\t\tpattern: /^\\s{0,8}(?:\\/\\/|\\\\\\\\)[a-z0-9][a-z0-9-]{0,62}(?:\\.[a-z0-9-]{1,63}){1,4}[/\\\\]/i,\n\t},\n];\n\n//#endregion\n\n//#region SSRF\n\n/** Repeated across every SSRF rule. */\nconst SSRF_CONTROL =\n\t\"Gate the request with assertOutboundUrl, re-validate after every redirect, and constrain egress at the network layer\";\n\n/**\n * Server-side request forgery.\n *\n * These signatures cover literal addresses only. A hostname whose DNS record\n * resolves to `169.254.169.254` passes every rule here — which is exactly why\n * {@link SSRF_CONTROL} names resolution and egress control as the real defense.\n */\nexport const SSRF_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"SSRF-LOOPBACK-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"URL host is a loopback address\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /\\/\\/(?:127(?:\\.\\d{1,3}){3}|localhost|0\\.0\\.0\\.0|\\[?::1\\]?)(?::\\d{1,5})?(?:[/?#]|$)/i,\n\t},\n\t{\n\t\tid: \"SSRF-PRIVATE-RANGE-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"URL host is in an RFC 1918 private range\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern:\n\t\t\t/\\/\\/(?:10(?:\\.\\d{1,3}){3}|192\\.168(?:\\.\\d{1,3}){2}|172\\.(?:1[6-9]|2\\d|3[01])(?:\\.\\d{1,3}){2})/,\n\t},\n\t{\n\t\tid: \"SSRF-METADATA-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"URL targets a cloud instance metadata endpoint\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /\\/\\/(?:169\\.254(?:\\.\\d{1,3}){2}|metadata\\.google\\.internal|metadata\\.goog)\\b/i,\n\t},\n\t{\n\t\tid: \"SSRF-ALT-IP-ENCODING-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Host written as a decimal, octal, or hex integer to evade address filters\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /\\/\\/(?:0x[0-9a-f]{6,8}|0\\d{6,11}|\\d{8,10})(?:[:/?#]|$)/i,\n\t},\n\t{\n\t\tid: \"SSRF-USERINFO-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Userinfo segment, used to disguise the true host\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /\\/\\/[^/@\\s]{1,128}@/,\n\t},\n\t{\n\t\tid: \"SSRF-NON-HTTP-SCHEME-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Non-HTTP scheme in a fetchable URL\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /^\\s{0,8}(?:file|gopher|dict|tftp|ldaps?|jar|netdoc|sftp):/i,\n\t},\n\t{\n\t\tid: \"SSRF-USERINFO-INTERNAL-HOST-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Internal address in the host position behind a userinfo segment\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\t// The textbook bypass. SSRF-LOOPBACK-001 and SSRF-METADATA-001 both anchor on\n\t\t// `//` immediately before the address, so `http://good.com@169.254.169.254/`\n\t\t// matched only the generic userinfo rule and scored 2.0 — an allow verdict on a\n\t\t// cloud-credential read.\n\t\tpattern:\n\t\t\t/@(?:127(?:\\.\\d{1,3}){3}|localhost|0\\.0\\.0\\.0|169\\.254(?:\\.\\d{1,3}){2}|10(?:\\.\\d{1,3}){3}|192\\.168(?:\\.\\d{1,3}){2}|172\\.(?:1[6-9]|2\\d|3[01])(?:\\.\\d{1,3}){2})(?=[:/?#]|$)/i,\n\t},\n\t{\n\t\tid: \"SSRF-SUFFIXED-LOOPBACK-HOST-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Loopback literal used as a hostname prefix\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /\\/\\/(?:127(?:\\.\\d{1,3}){3}|localhost|0\\.0\\.0\\.0)\\.[a-z]/i,\n\t},\n\t{\n\t\tid: \"SSRF-METADATA-ALT-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Alibaba Cloud or Oracle Cloud instance metadata endpoint\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /\\/\\/(?:100\\.100\\.100\\.200|192\\.0\\.0\\.192)(?=[:/?#]|$)/,\n\t},\n\t{\n\t\tid: \"SSRF-IPV6-INTERNAL-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"IPv6 loopback, unique-local, or link-local literal\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern:\n\t\t\t/(?:\\/\\/|@)\\[(?:::|(?:0{1,4}:){2,7}0{0,3}[01]|f[cd][0-9a-f]{0,2}:[0-9a-f:]{0,32}|fe[89ab][0-9a-f]?:[0-9a-f:]{0,32})\\]/i,\n\t},\n\t{\n\t\tid: \"SSRF-IPV4-MAPPED-INTERNAL-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"IPv4-mapped IPv6 literal carrying an internal address\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern:\n\t\t\t/(?:\\/\\/|@)\\[::ffff:(?:127\\.|10\\.|169\\.254\\.|192\\.168\\.|0\\.0\\.0\\.0|172\\.(?:1[6-9]|2\\d|3[01])\\.|7f[0-9a-f]{2}:|a9fe:|c0a8:)/i,\n\t},\n\t{\n\t\tid: \"SSRF-SHORT-IP-FORM-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Abbreviated or octal IPv4 form resolving to a loopback address\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\t// `127.1`, `0`, and `0177.0.0.1` all resolve to loopback through inet_aton.\n\t\tpattern: /\\/\\/(?:127\\.\\d{1,3}(?:\\.\\d{1,3})?|0|0\\d{2,3}(?:\\.0*\\d{1,3}){3})(?=[:/?#]|$)/,\n\t},\n\t{\n\t\tid: \"SSRF-WILDCARD-DNS-HOST-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Wildcard-DNS service that resolves an arbitrary address from the hostname\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\t// Coverage is incomplete by construction — a new wildcard-DNS service needs a\n\t\t// catalog edit. Host resolution plus egress control is what actually holds.\n\t\tpattern:\n\t\t\t/\\/\\/(?:[^/\\s]{0,64}\\.)?(?:nip\\.io|sslip\\.io|localtest\\.me|lvh\\.me|vcap\\.me|xip\\.io|localho\\.st|traefik\\.me)(?=[:/?#]|$)/i,\n\t},\n];\n\n//#endregion\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"system.mjs","names":[],"sources":["../../../src/threats/rules/system.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\n * SPDX-License-Identifier: Apache-2.0\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @fileoverview Host-facing signatures: OS command injection, path traversal, file\n * inclusion, and server-side request forgery.\n *\n * Two of these categories are only weakly served by signatures, and the rules say so\n * in their `primaryControl`:\n *\n * - **Path traversal** is prevented by canonicalizing the candidate path and\n * verifying it stays beneath the allowed base directory — see `resolveContainedPath`\n * in `@resq-systems/security/paths`. Detection catches `../`; it does not catch a\n * symlink, nor an absolute path that happens to resolve inside the base.\n * - **SSRF** is prevented by parsing the URL, resolving the host, rejecting private\n * and link-local ranges, re-validating after every redirect, and constraining\n * network egress. A regex over the URL string catches a literal `127.0.0.1` but\n * never a hostname whose DNS record points there.\n *\n * @module @resq-systems/security/threats/rules/system\n */\n\nimport type { ThreatRule } from \"../types.js\";\n\n//#region Command injection\n\n/** Repeated across every command-injection rule. */\nconst CMD_CONTROL =\n\t\"Spawn with an argv array and shell:false; never build a shell string from input\";\n\n/** OS command injection. Scoped to the `shell` context — never runs elsewhere. */\nexport const COMMAND_INJECTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"CMD-SUBSTITUTION-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Shell command substitution $(...)\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\t// `(` excluded alongside `)` so a run of `$(` cannot make the engine consume\n\t\t// 200 characters and backtrack at every position. A nested `$(a $(b))` still\n\t\t// matches on the inner substitution.\n\t\tpattern: /\\$\\([^()]{1,200}\\)/,\n\t},\n\t{\n\t\tid: \"CMD-BACKTICK-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Backtick command substitution\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern: /`[^`]{1,200}`/,\n\t},\n\t{\n\t\tid: \"CMD-CHAIN-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Command separator followed by a state-changing or network command\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern:\n\t\t\t/[;&|]{1,2}\\s{0,8}(?:rm|del|cat|type|wget|curl|nc|ncat|bash|sh|zsh|powershell|pwsh|python[23]?|perl|ruby|chmod|chown|kill|shutdown|reboot|mkfifo|base64|openssl)\\b/i,\n\t},\n\t{\n\t\tid: \"CMD-PIPE-SHELL-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Output piped into a shell interpreter\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern: /\\|\\s{0,8}(?:sh|bash|zsh|dash|cmd|powershell|pwsh)\\b/i,\n\t},\n\t{\n\t\tid: \"CMD-REDIRECT-SYSTEM-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Redirection into a system directory\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern: />\\s{0,8}\\/(?:etc|dev|proc|sys|boot)\\//,\n\t},\n\t{\n\t\t// Commix documents both as filter bypasses against defences that block spaces:\n\t\t// each expands to a word separator, so a command survives a filter that only\n\t\t// looks for whitespace between tokens.\n\t\tid: \"CMD-WORD-SPLIT-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Shell word-splitting construct standing in for a space\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern: /\\$\\{IFS\\}|\\{\\s{0,4},\\s{0,4}\\}/,\n\t},\n\t{\n\t\t// The shell removes an empty quote pair before execution, so a command name split\n\t\t// by one still runs while defeating a literal match on that name.\n\t\t//\n\t\t// Deliberately narrower than Commix's full set: the equivalent backslash form is\n\t\t// NOT matched, because it is indistinguishable from an ordinary Windows-style\n\t\t// relative path, and a rule firing on those would be disabled by the first person\n\t\t// it inconvenienced.\n\t\tid: \"CMD-QUOTE-SPLIT-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Empty quote pair inside a word, removed by the shell before execution\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern: /[a-z](?:\"\"|'')[a-z]/i,\n\t},\n\t{\n\t\tid: \"CMD-ENV-EXPANSION-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"Shell parameter expansion\",\n\t\tcwe: 78,\n\t\tprimaryControl: CMD_CONTROL,\n\t\t// `{` excluded alongside `}` for the same backtracking reason as above.\n\t\tpattern: /\\$\\{[^{}]{1,200}\\}/,\n\t},\n\t{\n\t\tid: \"CMD-ARG-NEWLINE-001\",\n\t\ttype: \"command_injection\",\n\t\tcontexts: [\"shell\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Line break in a value destined for a command line\",\n\t\tcwe: 88,\n\t\tprimaryControl: CMD_CONTROL,\n\t\tpattern: /[\\r\\n]/,\n\t},\n];\n\n//#endregion\n\n//#region Path traversal\n\n/** Repeated across every path-traversal rule. */\nconst PATH_CONTROL =\n\t\"Resolve the candidate against the base directory and verify containment (resolveContainedPath); resolve symlinks before use\";\n\n/** Directory-traversal and sensitive-path signatures. Scoped to `filesystem`. */\nexport const PATH_TRAVERSAL_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"PATH-TRAVERSAL-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Parent-directory segment\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\t// Anchored to segment boundaries so `my..file/` and `v1..v2` do not fire.\n\t\tpattern: /(?:^|[/\\\\])\\.\\.(?:[/\\\\]|$)/,\n\t},\n\t{\n\t\tid: \"PATH-TRAVERSAL-ENCODED-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Percent-encoded parent-directory traversal\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\t// The previous catalog wrote `/%2e%2e[%2f%5c]/`, whose character class matches\n\t\t// a single character from {%, 2, f, 5, c} — so it fired on `%2e%2e%` and on\n\t\t// `%2e%2e2`, and expressed nothing about the intended separator alternation.\n\t\tpattern: /%2e%2e(?:%2f|%5c|\\/|\\\\)/i,\n\t\tvariants: [\"raw\"],\n\t},\n\t{\n\t\tid: \"PATH-TRAVERSAL-MIXED-ENCODED-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Literal dots with a percent-encoded separator\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\tpattern: /\\.\\.(?:%2f|%5c)/i,\n\t\tvariants: [\"raw\"],\n\t},\n\t{\n\t\tid: \"PATH-TRAVERSAL-DOUBLE-ENCODED-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Double percent-encoded parent-directory traversal\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\tpattern: /%252e%252e(?:%252f|%255c)/i,\n\t\tvariants: [\"raw\"],\n\t},\n\t{\n\t\tid: \"PATH-TRAVERSAL-OVERLONG-UTF8-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Overlong UTF-8 encoding of a path separator\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\tpattern: /%c0%(?:af|2f)|%e0%80%af|%c1%9c/i,\n\t\tvariants: [\"raw\"],\n\t},\n\t{\n\t\tid: \"PATH-NULL-BYTE-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"NUL byte, used to truncate a path before its extension check\",\n\t\tcwe: 158,\n\t\tprimaryControl: PATH_CONTROL,\n\t\t// biome-ignore lint/suspicious/noControlCharactersInRegex: matching the NUL byte is this rule's entire purpose\n\t\tpattern: /%00|\\u0000/,\n\t},\n\t{\n\t\tid: \"PATH-TRAVERSAL-FILTER-STRIP-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Dot run that survives a naive ../ stripping filter\",\n\t\tcwe: 22,\n\t\tprimaryControl:\n\t\t\t\"Resolve the candidate against the base directory and verify containment (resolveContainedPath). Never sanitize by stripping '../' — stripping is precisely what this payload targets: '....//' becomes '../' after one pass.\",\n\t\t// PATH-TRAVERSAL-001 requires exactly two dots between boundaries, so `....//`\n\t\t// and `.../` walked past it. Bounded at 200 because a longer run is already\n\t\t// reported by the engine's repetition check.\n\t\tpattern: /(?:^|[/\\\\])\\.{3,200}[/\\\\]/,\n\t},\n\t{\n\t\tid: \"PATH-TRAVERSAL-PARAM-BYPASS-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Path parameter segment used to bypass a gateway path ACL\",\n\t\tcwe: 22,\n\t\tprimaryControl:\n\t\t\t\"Ensure the proxy and the origin agree on path normalization before authorization is applied; resolve and verify containment with resolveContainedPath\",\n\t\t// `..;/` — Tomcat and Spring treat `;` as starting a path parameter, so the\n\t\t// segment normalizes to `..` at the origin while the gateway sees a literal name.\n\t\tpattern: /(?:^|[/\\\\])\\.\\.;[^/\\\\]{0,64}[/\\\\]/,\n\t},\n\t{\n\t\tid: \"PATH-SENSITIVE-UNIX-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Reference to a sensitive Unix path\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\tpattern: /\\/(?:etc\\/(?:passwd|shadow|sudoers|hosts)|proc\\/self|root\\/\\.ssh)\\b/i,\n\t},\n\t{\n\t\tid: \"PATH-SENSITIVE-WINDOWS-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Reference to a sensitive Windows path\",\n\t\tcwe: 22,\n\t\tprimaryControl: PATH_CONTROL,\n\t\t// Requires a system subdirectory. The old rule was a bare `/C:\\\\Windows/gi`,\n\t\t// which fired on any documentation that merely mentioned the folder.\n\t\tpattern: /[a-z]:[\\\\/]{1,2}(?:windows|winnt)[\\\\/]{1,2}(?:system32|win\\.ini|repair)/i,\n\t},\n\t{\n\t\tid: \"PATH-ABSOLUTE-001\",\n\t\ttype: \"path_traversal\",\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"Absolute path where a relative one is expected\",\n\t\tcwe: 36,\n\t\tprimaryControl: PATH_CONTROL,\n\t\tpattern: /^(?:\\/|[a-z]:[\\\\/]|\\\\\\\\)/i,\n\t},\n];\n\n//#endregion\n\n//#region File inclusion\n\n/** Repeated across every file-inclusion rule. */\nconst LFI_CONTROL =\n\t\"Map the request to a fixed allowlist of loadable resources; never derive a module or template path from input\";\n\n/** Adds the remote-fetch half of the control, for the RFI rules. */\nconst RFI_CONTROL =\n\t\"Map the request to a fixed allowlist of loadable resources; never derive a module, template, or file path from input. Disable remote stream wrappers (PHP allow_url_fopen/allow_url_include) and reject any candidate not relative to the configured base directory\";\n\n/** Local/remote file inclusion via stream wrappers and alternate schemes. */\nexport const FILE_INCLUSION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"LFI-PHP-WRAPPER-001\",\n\t\ttype: \"file_inclusion\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"PHP stream wrapper used to read or execute source\",\n\t\tcwe: 98,\n\t\tprimaryControl: LFI_CONTROL,\n\t\tpattern: /\\bphp:\\/\\/(?:filter|input|memory|temp|stdin)/i,\n\t},\n\t{\n\t\tid: \"LFI-WRAPPER-001\",\n\t\ttype: \"file_inclusion\",\n\t\tcontexts: [\"filesystem\", \"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Alternate stream wrapper capable of loading remote or archived content\",\n\t\tcwe: 98,\n\t\tprimaryControl: LFI_CONTROL,\n\t\tpattern: /\\b(?:expect|zip|phar|glob|dict|gopher|netdoc|jar|ogg):\\/\\//i,\n\t},\n\t{\n\t\tid: \"RFI-REMOTE-SCHEME-001\",\n\t\ttype: \"file_inclusion\",\n\t\t// `filesystem` only. The SSRF rules already own `url`; this is about a value\n\t\t// that was supposed to be a *path* and instead names a remote resource.\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Remote scheme where a local path was expected\",\n\t\tcwe: 98,\n\t\tprimaryControl: RFI_CONTROL,\n\t\t// The leading run is unbounded, because a reader that strips leading whitespace\n\t\t// strips all of it. Without `/m`, `^` matches only at index 0, so the run is entered\n\t\t// once and backtracks at most its own length: linear.\n\t\tpattern:\n\t\t\t/(?:^\\s*|[/\\\\=])(?:https?|ftps?|smb|cifs|nfs|webdav|ssh2|rar|zlib|compress\\.(?:zlib|bzip2)):\\/\\//i,\n\t},\n\t{\n\t\tid: \"RFI-DATA-URI-001\",\n\t\ttype: \"file_inclusion\",\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"data: URI where a local path was expected\",\n\t\tcwe: 98,\n\t\tprimaryControl: RFI_CONTROL,\n\t\t// The class is ordered `[a-z0-9+.-]` deliberately: writing `[a-z0-9.+-]` embeds\n\t\t// the literal substring `.+`, which the ReDoS structural test rejects. The leading\n\t\t// run is unbounded and linear, as in RFI-REMOTE-SCHEME-001.\n\t\tpattern:\n\t\t\t/(?:^\\s*|[/\\\\=])data:(?:\\/\\/)?(?:[a-z][a-z0-9+.-]{0,32}\\/[a-z0-9+.-]{1,32}[a-z0-9;=+.-]{0,32}|;base64|),/i,\n\t},\n\t{\n\t\tid: \"RFI-REMOTE-HOST-PATH-001\",\n\t\ttype: \"file_inclusion\",\n\t\tcontexts: [\"filesystem\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Protocol-relative or UNC host path where a local path was expected\",\n\t\tcwe: 98,\n\t\tprimaryControl: RFI_CONTROL,\n\t\t// Re-grades rather than newly detects: PATH-ABSOLUTE-001 already matches these\n\t\t// at low confidence, contributing 1.0 — not enough to leave the allow band. The\n\t\t// leading run is unbounded and linear, as in RFI-REMOTE-SCHEME-001.\n\t\tpattern: /^\\s*(?:\\/\\/|\\\\\\\\)[a-z0-9][a-z0-9-]{0,62}(?:\\.[a-z0-9-]{1,63}){1,4}[/\\\\]/i,\n\t},\n];\n\n//#endregion\n\n//#region SSRF\n\n/** Repeated across every SSRF rule. */\nconst SSRF_CONTROL =\n\t\"Gate the request with assertOutboundUrl, re-validate after every redirect, and constrain egress at the network layer\";\n\n/**\n * Server-side request forgery.\n *\n * These signatures cover literal addresses only. A hostname whose DNS record\n * resolves to `169.254.169.254` passes every rule here — which is exactly why\n * {@link SSRF_CONTROL} names resolution and egress control as the real defense.\n */\nexport const SSRF_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"SSRF-LOOPBACK-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"URL host is a loopback address\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /\\/\\/(?:127(?:\\.\\d{1,3}){3}|localhost|0\\.0\\.0\\.0|\\[?::1\\]?)(?::\\d{1,5})?(?:[/?#]|$)/i,\n\t},\n\t{\n\t\tid: \"SSRF-PRIVATE-RANGE-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"URL host is in an RFC 1918 private range\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern:\n\t\t\t/\\/\\/(?:10(?:\\.\\d{1,3}){3}|192\\.168(?:\\.\\d{1,3}){2}|172\\.(?:1[6-9]|2\\d|3[01])(?:\\.\\d{1,3}){2})/,\n\t},\n\t{\n\t\tid: \"SSRF-METADATA-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"URL targets a cloud instance metadata endpoint\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /\\/\\/(?:169\\.254(?:\\.\\d{1,3}){2}|metadata\\.google\\.internal|metadata\\.goog)\\b/i,\n\t},\n\t{\n\t\tid: \"SSRF-ALT-IP-ENCODING-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Host written as a decimal, octal, or hex integer to evade address filters\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /\\/\\/(?:0x[0-9a-f]{6,8}|0\\d{6,11}|\\d{8,10})(?:[:/?#]|$)/i,\n\t},\n\t{\n\t\tid: \"SSRF-USERINFO-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Userinfo segment, used to disguise the true host\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /\\/\\/[^/@\\s]{1,128}@/,\n\t},\n\t{\n\t\tid: \"SSRF-NON-HTTP-SCHEME-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Non-HTTP scheme in a fetchable URL\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\t// The leading run is unbounded, because a URL parser strips all of it, not eight.\n\t\t// Without `/m`, `^` matches only at index 0, so the run is entered once: linear.\n\t\tpattern: /^\\s*(?:file|gopher|dict|tftp|ldaps?|jar|netdoc|sftp):/i,\n\t},\n\t{\n\t\tid: \"SSRF-USERINFO-INTERNAL-HOST-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Internal address in the host position behind a userinfo segment\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\t// The textbook bypass. SSRF-LOOPBACK-001 and SSRF-METADATA-001 both anchor on\n\t\t// `//` immediately before the address, so `http://good.com@169.254.169.254/`\n\t\t// matched only the generic userinfo rule and scored 2.0 — an allow verdict on a\n\t\t// cloud-credential read.\n\t\tpattern:\n\t\t\t/@(?:127(?:\\.\\d{1,3}){3}|localhost|0\\.0\\.0\\.0|169\\.254(?:\\.\\d{1,3}){2}|10(?:\\.\\d{1,3}){3}|192\\.168(?:\\.\\d{1,3}){2}|172\\.(?:1[6-9]|2\\d|3[01])(?:\\.\\d{1,3}){2})(?=[:/?#]|$)/i,\n\t},\n\t{\n\t\tid: \"SSRF-SUFFIXED-LOOPBACK-HOST-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Loopback literal used as a hostname prefix\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /\\/\\/(?:127(?:\\.\\d{1,3}){3}|localhost|0\\.0\\.0\\.0)\\.[a-z]/i,\n\t},\n\t{\n\t\tid: \"SSRF-METADATA-ALT-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Alibaba Cloud or Oracle Cloud instance metadata endpoint\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern: /\\/\\/(?:100\\.100\\.100\\.200|192\\.0\\.0\\.192)(?=[:/?#]|$)/,\n\t},\n\t{\n\t\tid: \"SSRF-IPV6-INTERNAL-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"IPv6 loopback, unique-local, or link-local literal\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern:\n\t\t\t/(?:\\/\\/|@)\\[(?:::|(?:0{1,4}:){2,7}0{0,3}[01]|f[cd][0-9a-f]{0,2}:[0-9a-f:]{0,32}|fe[89ab][0-9a-f]?:[0-9a-f:]{0,32})\\]/i,\n\t},\n\t{\n\t\tid: \"SSRF-IPV4-MAPPED-INTERNAL-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"IPv4-mapped IPv6 literal carrying an internal address\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\tpattern:\n\t\t\t/(?:\\/\\/|@)\\[::ffff:(?:127\\.|10\\.|169\\.254\\.|192\\.168\\.|0\\.0\\.0\\.0|172\\.(?:1[6-9]|2\\d|3[01])\\.|7f[0-9a-f]{2}:|a9fe:|c0a8:)/i,\n\t},\n\t{\n\t\tid: \"SSRF-SHORT-IP-FORM-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Abbreviated or octal IPv4 form resolving to a loopback address\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\t// `127.1`, `0`, and `0177.0.0.1` all resolve to loopback through inet_aton.\n\t\tpattern: /\\/\\/(?:127\\.\\d{1,3}(?:\\.\\d{1,3})?|0|0\\d{2,3}(?:\\.0*\\d{1,3}){3})(?=[:/?#]|$)/,\n\t},\n\t{\n\t\tid: \"SSRF-WILDCARD-DNS-HOST-001\",\n\t\ttype: \"ssrf\",\n\t\tcontexts: [\"url\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Wildcard-DNS service that resolves an arbitrary address from the hostname\",\n\t\tcwe: 918,\n\t\tprimaryControl: SSRF_CONTROL,\n\t\t// Coverage is incomplete by construction — a new wildcard-DNS service needs a\n\t\t// catalog edit. Host resolution plus egress control is what actually holds.\n\t\tpattern:\n\t\t\t/\\/\\/(?:[^/\\s]{0,64}\\.)?(?:nip\\.io|sslip\\.io|localtest\\.me|lvh\\.me|vcap\\.me|xip\\.io|localho\\.st|traefik\\.me)(?=[:/?#]|$)/i,\n\t},\n];\n\n//#endregion\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@resq-systems/security",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Security utilities: encryption, input validation, schemas, and PII sanitization",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"test": "vitest run"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"effect": "
|
|
61
|
+
"effect": "^4.0.0",
|
|
62
62
|
"jsdom": ">=22.0.0"
|
|
63
63
|
},
|
|
64
64
|
"peerDependenciesMeta": {
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"devDependencies": {
|
|
73
73
|
"@total-typescript/ts-reset": "^0.6.1",
|
|
74
74
|
"@types/node": "^26.6.3",
|
|
75
|
-
"effect": "4.0.0
|
|
75
|
+
"effect": "4.0.0",
|
|
76
76
|
"jsdom": "^30.1.1",
|
|
77
77
|
"tsdown": "^0.23.0",
|
|
78
78
|
"typescript": "7.0.2",
|