@resq-systems/security 1.0.5 → 2.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 +236 -33
- package/lib/controls/address.d.mts +142 -0
- package/lib/controls/address.d.mts.map +1 -0
- package/lib/controls/address.mjs +533 -0
- package/lib/controls/address.mjs.map +1 -0
- package/lib/controls/csrf.d.mts +91 -0
- package/lib/controls/csrf.d.mts.map +1 -0
- package/lib/controls/csrf.mjs +200 -0
- package/lib/controls/csrf.mjs.map +1 -0
- package/lib/controls/index.d.mts +8 -0
- package/lib/controls/index.mjs +8 -0
- package/lib/controls/origin.d.mts +95 -0
- package/lib/controls/origin.d.mts.map +1 -0
- package/lib/controls/origin.mjs +156 -0
- package/lib/controls/origin.mjs.map +1 -0
- package/lib/controls/payload.d.mts +84 -0
- package/lib/controls/payload.d.mts.map +1 -0
- package/lib/controls/payload.mjs +147 -0
- package/lib/controls/payload.mjs.map +1 -0
- package/lib/controls/query.d.mts +157 -0
- package/lib/controls/query.d.mts.map +1 -0
- package/lib/controls/query.mjs +368 -0
- package/lib/controls/query.mjs.map +1 -0
- package/lib/controls/redirect.d.mts +92 -0
- package/lib/controls/redirect.d.mts.map +1 -0
- package/lib/controls/redirect.mjs +110 -0
- package/lib/controls/redirect.mjs.map +1 -0
- package/lib/controls/upload.d.mts +108 -0
- package/lib/controls/upload.d.mts.map +1 -0
- package/lib/controls/upload.mjs +374 -0
- package/lib/controls/upload.mjs.map +1 -0
- package/lib/crypto.d.mts +18 -5
- package/lib/crypto.d.mts.map +1 -1
- package/lib/crypto.mjs +35 -24
- package/lib/crypto.mjs.map +1 -1
- package/lib/hash.d.mts +51 -6
- package/lib/hash.d.mts.map +1 -1
- package/lib/hash.mjs +51 -6
- package/lib/hash.mjs.map +1 -1
- package/lib/index.d.mts +17 -2
- package/lib/index.mjs +19 -2
- package/lib/paths.d.mts +92 -0
- package/lib/paths.d.mts.map +1 -0
- package/lib/paths.mjs +140 -0
- package/lib/paths.mjs.map +1 -0
- package/lib/sanitize.d.mts +137 -35
- package/lib/sanitize.d.mts.map +1 -1
- package/lib/sanitize.mjs +170 -46
- package/lib/sanitize.mjs.map +1 -1
- package/lib/threats/capec.generated.d.mts +59 -0
- package/lib/threats/capec.generated.d.mts.map +1 -0
- package/lib/threats/capec.generated.mjs +644 -0
- package/lib/threats/capec.generated.mjs.map +1 -0
- package/lib/threats/engine.d.mts +94 -0
- package/lib/threats/engine.d.mts.map +1 -0
- package/lib/threats/engine.mjs +167 -0
- package/lib/threats/engine.mjs.map +1 -0
- package/lib/threats/index.d.mts +11 -0
- package/lib/threats/index.mjs +11 -0
- package/lib/threats/rules/datastore.d.mts +13 -0
- package/lib/threats/rules/datastore.d.mts.map +1 -0
- package/lib/threats/rules/datastore.mjs +366 -0
- package/lib/threats/rules/datastore.mjs.map +1 -0
- package/lib/threats/rules/index.d.mts +54 -0
- package/lib/threats/rules/index.d.mts.map +1 -0
- package/lib/threats/rules/index.mjs +121 -0
- package/lib/threats/rules/index.mjs.map +1 -0
- package/lib/threats/rules/markup.d.mts +28 -0
- package/lib/threats/rules/markup.d.mts.map +1 -0
- package/lib/threats/rules/markup.mjs +373 -0
- package/lib/threats/rules/markup.mjs.map +1 -0
- package/lib/threats/rules/protocol.d.mts +49 -0
- package/lib/threats/rules/protocol.d.mts.map +1 -0
- package/lib/threats/rules/protocol.mjs +175 -0
- package/lib/threats/rules/protocol.mjs.map +1 -0
- package/lib/threats/rules/system.d.mts +19 -0
- package/lib/threats/rules/system.d.mts.map +1 -0
- package/lib/threats/rules/system.mjs +455 -0
- package/lib/threats/rules/system.mjs.map +1 -0
- package/lib/threats/rules/web.d.mts +26 -0
- package/lib/threats/rules/web.d.mts.map +1 -0
- package/lib/threats/rules/web.mjs +412 -0
- package/lib/threats/rules/web.mjs.map +1 -0
- package/lib/threats/scoring.d.mts +59 -0
- package/lib/threats/scoring.d.mts.map +1 -0
- package/lib/threats/scoring.mjs +111 -0
- package/lib/threats/scoring.mjs.map +1 -0
- package/lib/threats/types.d.mts +245 -0
- package/lib/threats/types.d.mts.map +1 -0
- package/lib/threats/types.mjs +52 -0
- package/lib/threats/types.mjs.map +1 -0
- package/lib/threats/variants.d.mts +57 -0
- package/lib/threats/variants.d.mts.map +1 -0
- package/lib/threats/variants.mjs +144 -0
- package/lib/threats/variants.mjs.map +1 -0
- package/lib/unicode/confusables.d.mts +82 -0
- package/lib/unicode/confusables.d.mts.map +1 -0
- package/lib/unicode/confusables.mjs +954 -0
- package/lib/unicode/confusables.mjs.map +1 -0
- package/lib/unicode/index.d.mts +126 -0
- package/lib/unicode/index.d.mts.map +1 -0
- package/lib/unicode/index.mjs +288 -0
- package/lib/unicode/index.mjs.map +1 -0
- package/lib/validators.d.mts +341 -164
- package/lib/validators.d.mts.map +1 -1
- package/lib/validators.mjs +519 -338
- package/lib/validators.mjs.map +1 -1
- package/package.json +35 -8
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.mjs","names":[],"sources":["../../../src/threats/rules/protocol.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\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 Protocol- and token-level signatures: HTTP parameter pollution,\n * credential exposure, and JWT tampering.\n *\n * `credential_exposure` inverts the direction of every other category in the catalog.\n * The rest detect a hostile value arriving; these detect the application's own secret\n * *leaving* — in a URL it is about to fetch, or a line it is about to log. Both are\n * outbound sinks, so the context model still fits, but the user-facing message must\n * not accuse the submitter of anything.\n *\n * @module @resq-systems/security/threats/rules/protocol\n */\n\nimport type { ThreatRule } from \"../types.js\";\n\n//#region HTTP parameter pollution\n\n/**\n * Parameter pollution.\n *\n * Only the *injected-parameter* half is detectable here. Duplicate-key pollution —\n * `?id=1&id=2` exploiting parser disagreement between ASP.NET, PHP, and Servlet\n * stacks — is not: the engine sees one string and never the parameter map, and\n * duplicate keys are legal and routine (`?tag=a&tag=b`). That half is a parsing\n * decision, documented as such rather than faked with a signature.\n */\nexport const PARAMETER_POLLUTION_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"PARAM-POLLUTION-PRIVILEGED-001\",\n\t\ttype: \"parameter_pollution\",\n\t\t// `url_parameter`, never `url`. In a whole URL `&role=` is ordinary grammar;\n\t\t// inside a single value about to be concatenated into a query string it is an\n\t\t// injected parameter. Attaching these to `url` produced measured false positives\n\t\t// on `?sku=99&color=red` and on OAuth authorize URLs.\n\t\tcontexts: [\"url_parameter\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Injected parameter naming a privilege, price, or credential field\",\n\t\tcwe: 235,\n\t\tprimaryControl:\n\t\t\t\"Build query strings with URLSearchParams or encodeURIComponent per value; read parameters through one parser and reject duplicate keys server-side\",\n\t\t// `client_id`, `scope`, `state`, `next`, `redirect`, `callback`, and `lang` are\n\t\t// deliberately absent — that exclusion is what lets nested OAuth URLs pass.\n\t\tpattern:\n\t\t\t/[&;][a-z0-9_.[-]{0,24}(?:role|is_?admin|admin|user_?id|userid|uid|account_?id|amount|price|total|quantity|qty|access_?token|api_?key|apikey|client_?secret|secret|password|passwd|pwd|session_?id|sessionid|signature|hmac|permissions?|privilege|is_?staff|superuser|debug)[\\]\"']{0,2}\\s{0,4}=/i,\n\t},\n\t{\n\t\tid: \"PARAM-POLLUTION-APPEND-001\",\n\t\ttype: \"parameter_pollution\",\n\t\tcontexts: [\"url_parameter\"],\n\t\tseverity: \"medium\",\n\t\tconfidence: \"low\",\n\t\tdescription: \"Value appends an additional query parameter\",\n\t\tcwe: 235,\n\t\tprimaryControl:\n\t\t\t\"Build query strings with URLSearchParams or encodeURIComponent per value; read parameters through one parser and reject duplicate keys server-side\",\n\t\t// `&`-only: including `;` fired on connection strings (`Server=a;Database=b`),\n\t\t// so `;`-separated pollution against a non-privileged name stays undetected.\n\t\t// The `^[^?]{0,256}` guard keeps nested `redirect_uri` values from firing, but\n\t\t// it is positional — a bare sub-query with no leading `?` still matches. Graded\n\t\t// medium/low (1.0) so it never reaches review on its own.\n\t\tpattern: /^[^?]{0,256}&[a-z_][a-z0-9_.-]{1,48}(?:\\[[a-z0-9_.-]{0,32}\\])?\\s{0,4}=/i,\n\t},\n];\n\n//#endregion\n\n//#region Credential exposure\n\n/** Repeated across the URL-borne credential rules. */\nconst CRED_URL_CONTROL =\n\t\"Carry credentials in the Authorization header or a POST body, never a query string; set Referrer-Policy: no-referrer and rotate any token that reached a URL\";\n\n/** Repeated across the log-borne credential rules. */\nconst CRED_LOG_CONTROL =\n\t\"Log structured records with an allowlist of loggable fields; redact by key with safeStringify\";\n\n/**\n * Credential material appearing where it will be stored, logged, or sent onward.\n *\n * A generic `password=…` / `api_key: …` assignment rule was written and then dropped:\n * it produced false positives on every realistic log line of the form\n * `\"api_key: required for this endpoint\"` and `\"validation failed: password: too_short\"`.\n * Shipping it weakened would trade the category's credibility for coverage.\n */\nexport const CREDENTIAL_EXPOSURE_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"CRED-URL-QUERY-001\",\n\t\ttype: \"credential_exposure\",\n\t\tcontexts: [\"url\", \"log\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Credential-bearing query parameter — leaks via Referer, proxies, and logs\",\n\t\tcwe: 598,\n\t\tprimaryControl: CRED_URL_CONTROL,\n\t\t// `sid`, `auth`, `key`, and bare `token` are excluded: `?sid=` is routinely a\n\t\t// store identifier. `sig`/`signature`/`X-Amz-*` are excluded because S3\n\t\t// presigned URLs legitimately carry them.\n\t\tpattern:\n\t\t\t/[?&](?:access[_-]?token|auth[_-]?token|id[_-]?token|refresh[_-]?token|api[_-]?key|api[_-]?secret|client[_-]?secret|secret[_-]?key|session[_-]?id|session[_-]?token|jsessionid|phpsessid|password|passwd|pwd)=[^&#\\s]{4,256}/i,\n\t},\n\t{\n\t\tid: \"CRED-AUTH-SCHEME-001\",\n\t\ttype: \"credential_exposure\",\n\t\tcontexts: [\"log\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Authorization header value written to a log\",\n\t\tcwe: 532,\n\t\tprimaryControl: CRED_LOG_CONTROL,\n\t\tpattern:\n\t\t\t/\\b(?:proxy-)?authorization\\s{0,4}:\\s{0,4}(?:bearer|basic|digest|token|apikey)\\s{0,4}[A-Za-z0-9+/=._~-]{8,512}/i,\n\t},\n\t{\n\t\tid: \"CRED-JWT-001\",\n\t\ttype: \"credential_exposure\",\n\t\tcontexts: [\"url\", \"log\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"JSON Web Token in a URL or log line\",\n\t\tcwe: 522,\n\t\tprimaryControl: CRED_URL_CONTROL,\n\t\tpattern: /\\beyJ[A-Za-z0-9_-]{10,400}\\.eyJ[A-Za-z0-9_-]{10,400}\\./,\n\t},\n\t{\n\t\tid: \"CRED-PROVIDER-KEY-001\",\n\t\ttype: \"credential_exposure\",\n\t\tcontexts: [\"url\", \"log\"],\n\t\tseverity: \"critical\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Provider API key or personal access token\",\n\t\tcwe: 532,\n\t\tprimaryControl:\n\t\t\t\"Load provider keys from a secret manager, never from source or a URL; rotate immediately on exposure\",\n\t\t// Case-sensitive by design — an `i` flag collides with ordinary hex digests.\n\t\tpattern:\n\t\t\t/\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b|\\bgh[pousr]_[A-Za-z0-9]{36}\\b|\\bgithub_pat_[A-Za-z0-9_]{22,82}\\b|\\bxox[abprs]-[A-Za-z0-9-]{10,120}|\\bsk_live_[A-Za-z0-9]{16,64}\\b|\\bsk-(?:ant|proj)-[A-Za-z0-9_-]{20,120}/,\n\t},\n];\n\n//#endregion\n\n//#region Double encoding\n\n/**\n * Double percent-encoding.\n *\n * `%253Cscript%253E` decodes once to `%3Cscript%3E` and twice to `<script>`. The\n * scanner decodes once, so the payload was invisible to every signature — verified at\n * 0/allow before this rule existed. The traversal case had an explicit\n * `%252e%252e%252f` rule; nothing covered the general class.\n *\n * Its own category rather than a subtype of whatever it eventually decodes to: at scan\n * time the eventual sink is unknown, and the signal — \"this input is shaped to survive\n * one decoding pass\" — is itself the finding. The real fix is to decode exactly once,\n * at a defined boundary, and never again.\n */\nexport const DOUBLE_ENCODING_RULES: readonly ThreatRule[] = [\n\t{\n\t\t// The sibling rule below matches the form where only the percent sign is\n\t\t// re-encoded. Encoding the hex digits as well yields the same byte after two\n\t\t// decodes, and is invisible to a pattern that expects those digits to follow the\n\t\t// escaped percent literally. Measured: the half-encoded spelling of a script tag\n\t\t// scored review, while the fully-encoded spelling of the identical payload\n\t\t// scored zero.\n\t\tid: \"ENCODING-DOUBLE-PERCENT-002\",\n\t\ttype: \"double_encoding\",\n\t\tcontexts: [\"url\", \"url_parameter\", \"html\", \"filesystem\", \"sql\", \"http_header\", \"xml\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Percent sign and its hex digits each encoded separately\",\n\t\tcwe: 177,\n\t\tprimaryControl:\n\t\t\t\"Decode exactly once at a defined boundary and never re-decode; reject input that still contains an escape prefix after decoding\",\n\t\tpattern: /%25%[0-9a-f]{2}%[0-9a-f]{2}/i,\n\t},\n\n\t{\n\t\tid: \"ENCODING-DOUBLE-PERCENT-001\",\n\t\ttype: \"double_encoding\",\n\t\tcontexts: [\"url\", \"url_parameter\", \"html\", \"filesystem\", \"sql\", \"http_header\", \"xml\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"medium\",\n\t\tdescription: \"Escape prefix that decodes to a metacharacter only after a second pass\",\n\t\tcwe: 177,\n\t\tprimaryControl:\n\t\t\t\"Decode exactly once at a defined boundary and never re-decode; reject input that still contains an escape prefix after decoding\",\n\t\t// Only sequences decoding to a character with syntactic meaning: space, quote,\n\t\t// percent, apostrophe, parens, dot, slash, angle brackets, semicolon, ampersand,\n\t\t// equals, backslash, NUL, CR, LF. A bare `%25` is not enough — \"100%25 off\" is\n\t\t// an ordinary encoded string and must not fire.\n\t\tvariants: [\"raw\"],\n\t\tpattern: /%25(?:2[0257CEFcef]|3[CEce]|22|26|27|28|29|3[BbDd]|5[Cc]|00|0[ADad])/,\n\t},\n];\n\n//#endregion\n\n//#region JWT\n\n/** Repeated across the JWT rules. */\nconst JWT_CONTROL =\n\t\"Verify with an explicit algorithm allowlist (jwtVerify(token, key, { algorithms: ['RS256'] })); never let the token's own header select the algorithm, and reject 'none' unconditionally\";\n\n/**\n * JWT tampering.\n *\n * Scoped to the unsecured-token case alone. `kid`, `jku`, and `x5u` injection are\n * *already covered* — extract the claim and declare its real sink, and the existing\n * rules fire: `kid=../../../../dev/null` with `filesystem` hits PATH-TRAVERSAL-001,\n * `jku=http://169.254.169.254/` with `url` hits SSRF-METADATA-001. Re-scanning a whole\n * opaque token with every detector would be the run-everything-against-everything\n * anti-pattern the engine exists to avoid.\n */\nexport const JWT_RULES: readonly ThreatRule[] = [\n\t{\n\t\tid: \"JWT-ALG-NONE-UNSECURED-001\",\n\t\ttype: \"jwt_tampering\",\n\t\tcontexts: [\"jwt\", \"http_header\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Unsecured JWT — three segments with an empty signature\",\n\t\tcwe: 347,\n\t\tprimaryControl: JWT_CONTROL,\n\t\t// Structural, not lexical. RFC 7519 §6.1 requires an empty signature segment,\n\t\t// so this is immune to base64 alignment, key ordering, whitespace, and `alg`\n\t\t// casing — all of which change how `\"alg\":\"none\"` encodes (`hbGciOiJub25l`,\n\t\t// `YWxnIjoibm9uZ`, `ImFsZyI6Im5vbmUi`, depending on offset mod 3).\n\t\t// Requiring `eyJ` on *both* segments, not just `ey`, removes false positives on\n\t\t// filenames such as `eyewitness_statement_final.v2.`.\n\t\tpattern:\n\t\t\t/(?:^|[\\s,;=(\"'[])eyJ[A-Za-z0-9_-]{16,2000}\\.eyJ[A-Za-z0-9_-]{8,4000}\\.(?:$|[\\s,;)\"'\\]&#])/,\n\t},\n\t{\n\t\tid: \"JWT-ALG-NONE-HEADER-001\",\n\t\ttype: \"jwt_tampering\",\n\t\tcontexts: [\"jwt\"],\n\t\tseverity: \"high\",\n\t\tconfidence: \"high\",\n\t\tdescription: \"Decoded JWT header selecting the 'none' algorithm\",\n\t\tcwe: 347,\n\t\tprimaryControl: JWT_CONTROL,\n\t\t// Covers what the structural rule cannot: `alg:none` carrying a non-empty\n\t\t// signature, which naive verifiers still accept.\n\t\tpattern: /\"alg\"\\s{0,8}:\\s{0,8}\"\\s{0,8}none\\s{0,8}\"/i,\n\t},\n];\n\n//#endregion\n"],"mappings":";;;;;;;;;;AA0CA,MAAa,4BAAmD,CAC/D;CACC,IAAI;CACJ,MAAM;CAKN,UAAU,CAAC,eAAe;CAC1B,UAAU;CACV,YAAY;CACZ,aAAa;CACb,KAAK;CACL,gBACC;CAGD,SACC;AACF,GACA;CACC,IAAI;CACJ,MAAM;CACN,UAAU,CAAC,eAAe;CAC1B,UAAU;CACV,YAAY;CACZ,aAAa;CACb,KAAK;CACL,gBACC;CAMD,SAAS;AACV,CACD;;AAOA,MAAM,mBACL;;;;;;;;;AAcD,MAAa,4BAAmD;CAC/D;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,OAAO,KAAK;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAIhB,SACC;CACF;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,KAAK;EAChB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SACC;CACF;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,OAAO,KAAK;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBAAgB;EAChB,SAAS;CACV;CACA;EACC,IAAI;EACJ,MAAM;EACN,UAAU,CAAC,OAAO,KAAK;EACvB,UAAU;EACV,YAAY;EACZ,aAAa;EACb,KAAK;EACL,gBACC;EAED,SACC;CACF;AACD;;;;;;;;;;;;;;AAmBA,MAAa,wBAA+C,CAC3D;CAOC,IAAI;CACJ,MAAM;CACN,UAAU;EAAC;EAAO;EAAiB;EAAQ;EAAc;EAAO;EAAe;CAAK;CACpF,UAAU;CACV,YAAY;CACZ,aAAa;CACb,KAAK;CACL,gBACC;CACD,SAAS;AACV,GAEA;CACC,IAAI;CACJ,MAAM;CACN,UAAU;EAAC;EAAO;EAAiB;EAAQ;EAAc;EAAO;EAAe;CAAK;CACpF,UAAU;CACV,YAAY;CACZ,aAAa;CACb,KAAK;CACL,gBACC;CAKD,UAAU,CAAC,KAAK;CAChB,SAAS;AACV,CACD;;AAOA,MAAM,cACL;;;;;;;;;;;AAYD,MAAa,YAAmC,CAC/C;CACC,IAAI;CACJ,MAAM;CACN,UAAU,CAAC,OAAO,aAAa;CAC/B,UAAU;CACV,YAAY;CACZ,aAAa;CACb,KAAK;CACL,gBAAgB;CAOhB,SACC;AACF,GACA;CACC,IAAI;CACJ,MAAM;CACN,UAAU,CAAC,KAAK;CAChB,UAAU;CACV,YAAY;CACZ,aAAa;CACb,KAAK;CACL,gBAAgB;CAGhB,SAAS;AACV,CACD"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ThreatRule } from "../types.mjs";
|
|
2
|
+
//#region src/threats/rules/system.d.ts
|
|
3
|
+
/** OS command injection. Scoped to the `shell` context — never runs elsewhere. */
|
|
4
|
+
declare const COMMAND_INJECTION_RULES: readonly ThreatRule[];
|
|
5
|
+
/** Directory-traversal and sensitive-path signatures. Scoped to `filesystem`. */
|
|
6
|
+
declare const PATH_TRAVERSAL_RULES: readonly ThreatRule[];
|
|
7
|
+
/** Local/remote file inclusion via stream wrappers and alternate schemes. */
|
|
8
|
+
declare const FILE_INCLUSION_RULES: readonly ThreatRule[];
|
|
9
|
+
/**
|
|
10
|
+
* Server-side request forgery.
|
|
11
|
+
*
|
|
12
|
+
* These signatures cover literal addresses only. A hostname whose DNS record
|
|
13
|
+
* resolves to `169.254.169.254` passes every rule here — which is exactly why
|
|
14
|
+
* {@link SSRF_CONTROL} names resolution and egress control as the real defense.
|
|
15
|
+
*/
|
|
16
|
+
declare const SSRF_RULES: readonly ThreatRule[];
|
|
17
|
+
//#endregion
|
|
18
|
+
export { COMMAND_INJECTION_RULES, FILE_INCLUSION_RULES, PATH_TRAVERSAL_RULES, SSRF_RULES };
|
|
19
|
+
//# sourceMappingURL=system.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"system.d.mts","names":[],"sources":["../../../src/threats/rules/system.ts"],"mappings":";;;cA4Ca,kCAAkC;;cA8HlC,+BAA+B;;cA2J/B,+BAA+B;;;;;;;;cAiF/B,qBAAqB"}
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
//#region src/threats/rules/system.ts
|
|
2
|
+
/** Repeated across every command-injection rule. */
|
|
3
|
+
const CMD_CONTROL = "Spawn with an argv array and shell:false; never build a shell string from input";
|
|
4
|
+
/** OS command injection. Scoped to the `shell` context — never runs elsewhere. */
|
|
5
|
+
const COMMAND_INJECTION_RULES = [
|
|
6
|
+
{
|
|
7
|
+
id: "CMD-SUBSTITUTION-001",
|
|
8
|
+
type: "command_injection",
|
|
9
|
+
contexts: ["shell"],
|
|
10
|
+
severity: "critical",
|
|
11
|
+
confidence: "high",
|
|
12
|
+
description: "Shell command substitution $(...)",
|
|
13
|
+
cwe: 78,
|
|
14
|
+
primaryControl: CMD_CONTROL,
|
|
15
|
+
pattern: /\$\([^()]{1,200}\)/
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "CMD-BACKTICK-001",
|
|
19
|
+
type: "command_injection",
|
|
20
|
+
contexts: ["shell"],
|
|
21
|
+
severity: "high",
|
|
22
|
+
confidence: "medium",
|
|
23
|
+
description: "Backtick command substitution",
|
|
24
|
+
cwe: 78,
|
|
25
|
+
primaryControl: CMD_CONTROL,
|
|
26
|
+
pattern: /`[^`]{1,200}`/
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "CMD-CHAIN-001",
|
|
30
|
+
type: "command_injection",
|
|
31
|
+
contexts: ["shell"],
|
|
32
|
+
severity: "critical",
|
|
33
|
+
confidence: "high",
|
|
34
|
+
description: "Command separator followed by a state-changing or network command",
|
|
35
|
+
cwe: 78,
|
|
36
|
+
primaryControl: CMD_CONTROL,
|
|
37
|
+
pattern: /[;&|]{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
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "CMD-PIPE-SHELL-001",
|
|
41
|
+
type: "command_injection",
|
|
42
|
+
contexts: ["shell"],
|
|
43
|
+
severity: "critical",
|
|
44
|
+
confidence: "high",
|
|
45
|
+
description: "Output piped into a shell interpreter",
|
|
46
|
+
cwe: 78,
|
|
47
|
+
primaryControl: CMD_CONTROL,
|
|
48
|
+
pattern: /\|\s{0,8}(?:sh|bash|zsh|dash|cmd|powershell|pwsh)\b/i
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "CMD-REDIRECT-SYSTEM-001",
|
|
52
|
+
type: "command_injection",
|
|
53
|
+
contexts: ["shell"],
|
|
54
|
+
severity: "high",
|
|
55
|
+
confidence: "high",
|
|
56
|
+
description: "Redirection into a system directory",
|
|
57
|
+
cwe: 78,
|
|
58
|
+
primaryControl: CMD_CONTROL,
|
|
59
|
+
pattern: />\s{0,8}\/(?:etc|dev|proc|sys|boot)\//
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "CMD-WORD-SPLIT-001",
|
|
63
|
+
type: "command_injection",
|
|
64
|
+
contexts: ["shell"],
|
|
65
|
+
severity: "high",
|
|
66
|
+
confidence: "medium",
|
|
67
|
+
description: "Shell word-splitting construct standing in for a space",
|
|
68
|
+
cwe: 78,
|
|
69
|
+
primaryControl: CMD_CONTROL,
|
|
70
|
+
pattern: /\$\{IFS\}|\{\s{0,4},\s{0,4}\}/
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "CMD-QUOTE-SPLIT-001",
|
|
74
|
+
type: "command_injection",
|
|
75
|
+
contexts: ["shell"],
|
|
76
|
+
severity: "medium",
|
|
77
|
+
confidence: "medium",
|
|
78
|
+
description: "Empty quote pair inside a word, removed by the shell before execution",
|
|
79
|
+
cwe: 78,
|
|
80
|
+
primaryControl: CMD_CONTROL,
|
|
81
|
+
pattern: /[a-z](?:""|'')[a-z]/i
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "CMD-ENV-EXPANSION-001",
|
|
85
|
+
type: "command_injection",
|
|
86
|
+
contexts: ["shell"],
|
|
87
|
+
severity: "medium",
|
|
88
|
+
confidence: "low",
|
|
89
|
+
description: "Shell parameter expansion",
|
|
90
|
+
cwe: 78,
|
|
91
|
+
primaryControl: CMD_CONTROL,
|
|
92
|
+
pattern: /\$\{[^{}]{1,200}\}/
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "CMD-ARG-NEWLINE-001",
|
|
96
|
+
type: "command_injection",
|
|
97
|
+
contexts: ["shell"],
|
|
98
|
+
severity: "medium",
|
|
99
|
+
confidence: "medium",
|
|
100
|
+
description: "Line break in a value destined for a command line",
|
|
101
|
+
cwe: 88,
|
|
102
|
+
primaryControl: CMD_CONTROL,
|
|
103
|
+
pattern: /[\r\n]/
|
|
104
|
+
}
|
|
105
|
+
];
|
|
106
|
+
/** Repeated across every path-traversal rule. */
|
|
107
|
+
const PATH_CONTROL = "Resolve the candidate against the base directory and verify containment (resolveContainedPath); resolve symlinks before use";
|
|
108
|
+
/** Directory-traversal and sensitive-path signatures. Scoped to `filesystem`. */
|
|
109
|
+
const PATH_TRAVERSAL_RULES = [
|
|
110
|
+
{
|
|
111
|
+
id: "PATH-TRAVERSAL-001",
|
|
112
|
+
type: "path_traversal",
|
|
113
|
+
contexts: ["filesystem"],
|
|
114
|
+
severity: "high",
|
|
115
|
+
confidence: "high",
|
|
116
|
+
description: "Parent-directory segment",
|
|
117
|
+
cwe: 22,
|
|
118
|
+
primaryControl: PATH_CONTROL,
|
|
119
|
+
pattern: /(?:^|[/\\])\.\.(?:[/\\]|$)/
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "PATH-TRAVERSAL-ENCODED-001",
|
|
123
|
+
type: "path_traversal",
|
|
124
|
+
contexts: ["filesystem", "url"],
|
|
125
|
+
severity: "high",
|
|
126
|
+
confidence: "high",
|
|
127
|
+
description: "Percent-encoded parent-directory traversal",
|
|
128
|
+
cwe: 22,
|
|
129
|
+
primaryControl: PATH_CONTROL,
|
|
130
|
+
pattern: /%2e%2e(?:%2f|%5c|\/|\\)/i,
|
|
131
|
+
variants: ["raw"]
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "PATH-TRAVERSAL-MIXED-ENCODED-001",
|
|
135
|
+
type: "path_traversal",
|
|
136
|
+
contexts: ["filesystem", "url"],
|
|
137
|
+
severity: "high",
|
|
138
|
+
confidence: "high",
|
|
139
|
+
description: "Literal dots with a percent-encoded separator",
|
|
140
|
+
cwe: 22,
|
|
141
|
+
primaryControl: PATH_CONTROL,
|
|
142
|
+
pattern: /\.\.(?:%2f|%5c)/i,
|
|
143
|
+
variants: ["raw"]
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "PATH-TRAVERSAL-DOUBLE-ENCODED-001",
|
|
147
|
+
type: "path_traversal",
|
|
148
|
+
contexts: ["filesystem", "url"],
|
|
149
|
+
severity: "high",
|
|
150
|
+
confidence: "high",
|
|
151
|
+
description: "Double percent-encoded parent-directory traversal",
|
|
152
|
+
cwe: 22,
|
|
153
|
+
primaryControl: PATH_CONTROL,
|
|
154
|
+
pattern: /%252e%252e(?:%252f|%255c)/i,
|
|
155
|
+
variants: ["raw"]
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "PATH-TRAVERSAL-OVERLONG-UTF8-001",
|
|
159
|
+
type: "path_traversal",
|
|
160
|
+
contexts: ["filesystem", "url"],
|
|
161
|
+
severity: "high",
|
|
162
|
+
confidence: "high",
|
|
163
|
+
description: "Overlong UTF-8 encoding of a path separator",
|
|
164
|
+
cwe: 22,
|
|
165
|
+
primaryControl: PATH_CONTROL,
|
|
166
|
+
pattern: /%c0%(?:af|2f)|%e0%80%af|%c1%9c/i,
|
|
167
|
+
variants: ["raw"]
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: "PATH-NULL-BYTE-001",
|
|
171
|
+
type: "path_traversal",
|
|
172
|
+
contexts: ["filesystem", "url"],
|
|
173
|
+
severity: "high",
|
|
174
|
+
confidence: "high",
|
|
175
|
+
description: "NUL byte, used to truncate a path before its extension check",
|
|
176
|
+
cwe: 158,
|
|
177
|
+
primaryControl: PATH_CONTROL,
|
|
178
|
+
pattern: /%00|\u0000/
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: "PATH-TRAVERSAL-FILTER-STRIP-001",
|
|
182
|
+
type: "path_traversal",
|
|
183
|
+
contexts: ["filesystem", "url"],
|
|
184
|
+
severity: "high",
|
|
185
|
+
confidence: "medium",
|
|
186
|
+
description: "Dot run that survives a naive ../ stripping filter",
|
|
187
|
+
cwe: 22,
|
|
188
|
+
primaryControl: "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.",
|
|
189
|
+
pattern: /(?:^|[/\\])\.{3,200}[/\\]/
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
id: "PATH-TRAVERSAL-PARAM-BYPASS-001",
|
|
193
|
+
type: "path_traversal",
|
|
194
|
+
contexts: ["filesystem", "url"],
|
|
195
|
+
severity: "high",
|
|
196
|
+
confidence: "high",
|
|
197
|
+
description: "Path parameter segment used to bypass a gateway path ACL",
|
|
198
|
+
cwe: 22,
|
|
199
|
+
primaryControl: "Ensure the proxy and the origin agree on path normalization before authorization is applied; resolve and verify containment with resolveContainedPath",
|
|
200
|
+
pattern: /(?:^|[/\\])\.\.;[^/\\]{0,64}[/\\]/
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: "PATH-SENSITIVE-UNIX-001",
|
|
204
|
+
type: "path_traversal",
|
|
205
|
+
contexts: ["filesystem"],
|
|
206
|
+
severity: "high",
|
|
207
|
+
confidence: "medium",
|
|
208
|
+
description: "Reference to a sensitive Unix path",
|
|
209
|
+
cwe: 22,
|
|
210
|
+
primaryControl: PATH_CONTROL,
|
|
211
|
+
pattern: /\/(?:etc\/(?:passwd|shadow|sudoers|hosts)|proc\/self|root\/\.ssh)\b/i
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: "PATH-SENSITIVE-WINDOWS-001",
|
|
215
|
+
type: "path_traversal",
|
|
216
|
+
contexts: ["filesystem"],
|
|
217
|
+
severity: "high",
|
|
218
|
+
confidence: "medium",
|
|
219
|
+
description: "Reference to a sensitive Windows path",
|
|
220
|
+
cwe: 22,
|
|
221
|
+
primaryControl: PATH_CONTROL,
|
|
222
|
+
pattern: /[a-z]:[\\/]{1,2}(?:windows|winnt)[\\/]{1,2}(?:system32|win\.ini|repair)/i
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: "PATH-ABSOLUTE-001",
|
|
226
|
+
type: "path_traversal",
|
|
227
|
+
contexts: ["filesystem"],
|
|
228
|
+
severity: "medium",
|
|
229
|
+
confidence: "low",
|
|
230
|
+
description: "Absolute path where a relative one is expected",
|
|
231
|
+
cwe: 36,
|
|
232
|
+
primaryControl: PATH_CONTROL,
|
|
233
|
+
pattern: /^(?:\/|[a-z]:[\\/]|\\\\)/i
|
|
234
|
+
}
|
|
235
|
+
];
|
|
236
|
+
/** Repeated across every file-inclusion rule. */
|
|
237
|
+
const LFI_CONTROL = "Map the request to a fixed allowlist of loadable resources; never derive a module or template path from input";
|
|
238
|
+
/** Adds the remote-fetch half of the control, for the RFI rules. */
|
|
239
|
+
const RFI_CONTROL = "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";
|
|
240
|
+
/** Local/remote file inclusion via stream wrappers and alternate schemes. */
|
|
241
|
+
const FILE_INCLUSION_RULES = [
|
|
242
|
+
{
|
|
243
|
+
id: "LFI-PHP-WRAPPER-001",
|
|
244
|
+
type: "file_inclusion",
|
|
245
|
+
contexts: ["filesystem", "url"],
|
|
246
|
+
severity: "critical",
|
|
247
|
+
confidence: "high",
|
|
248
|
+
description: "PHP stream wrapper used to read or execute source",
|
|
249
|
+
cwe: 98,
|
|
250
|
+
primaryControl: LFI_CONTROL,
|
|
251
|
+
pattern: /\bphp:\/\/(?:filter|input|memory|temp|stdin)/i
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
id: "LFI-WRAPPER-001",
|
|
255
|
+
type: "file_inclusion",
|
|
256
|
+
contexts: ["filesystem", "url"],
|
|
257
|
+
severity: "high",
|
|
258
|
+
confidence: "high",
|
|
259
|
+
description: "Alternate stream wrapper capable of loading remote or archived content",
|
|
260
|
+
cwe: 98,
|
|
261
|
+
primaryControl: LFI_CONTROL,
|
|
262
|
+
pattern: /\b(?:expect|zip|phar|glob|dict|gopher|netdoc|jar|ogg):\/\//i
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
id: "RFI-REMOTE-SCHEME-001",
|
|
266
|
+
type: "file_inclusion",
|
|
267
|
+
contexts: ["filesystem"],
|
|
268
|
+
severity: "high",
|
|
269
|
+
confidence: "high",
|
|
270
|
+
description: "Remote scheme where a local path was expected",
|
|
271
|
+
cwe: 98,
|
|
272
|
+
primaryControl: RFI_CONTROL,
|
|
273
|
+
pattern: /(?:^\s{0,8}|[/\\=])(?:https?|ftps?|smb|cifs|nfs|webdav|ssh2|rar|zlib|compress\.(?:zlib|bzip2)):\/\//i
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
id: "RFI-DATA-URI-001",
|
|
277
|
+
type: "file_inclusion",
|
|
278
|
+
contexts: ["filesystem"],
|
|
279
|
+
severity: "high",
|
|
280
|
+
confidence: "high",
|
|
281
|
+
description: "data: URI where a local path was expected",
|
|
282
|
+
cwe: 98,
|
|
283
|
+
primaryControl: RFI_CONTROL,
|
|
284
|
+
pattern: /(?:^\s{0,8}|[/\\=])data:(?:\/\/)?(?:[a-z][a-z0-9+.-]{0,32}\/[a-z0-9+.-]{1,32}[a-z0-9;=+.-]{0,32}|;base64|),/i
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
id: "RFI-REMOTE-HOST-PATH-001",
|
|
288
|
+
type: "file_inclusion",
|
|
289
|
+
contexts: ["filesystem"],
|
|
290
|
+
severity: "high",
|
|
291
|
+
confidence: "medium",
|
|
292
|
+
description: "Protocol-relative or UNC host path where a local path was expected",
|
|
293
|
+
cwe: 98,
|
|
294
|
+
primaryControl: RFI_CONTROL,
|
|
295
|
+
pattern: /^\s{0,8}(?:\/\/|\\\\)[a-z0-9][a-z0-9-]{0,62}(?:\.[a-z0-9-]{1,63}){1,4}[/\\]/i
|
|
296
|
+
}
|
|
297
|
+
];
|
|
298
|
+
/** Repeated across every SSRF rule. */
|
|
299
|
+
const SSRF_CONTROL = "Gate the request with assertOutboundUrl, re-validate after every redirect, and constrain egress at the network layer";
|
|
300
|
+
/**
|
|
301
|
+
* Server-side request forgery.
|
|
302
|
+
*
|
|
303
|
+
* These signatures cover literal addresses only. A hostname whose DNS record
|
|
304
|
+
* resolves to `169.254.169.254` passes every rule here — which is exactly why
|
|
305
|
+
* {@link SSRF_CONTROL} names resolution and egress control as the real defense.
|
|
306
|
+
*/
|
|
307
|
+
const SSRF_RULES = [
|
|
308
|
+
{
|
|
309
|
+
id: "SSRF-LOOPBACK-001",
|
|
310
|
+
type: "ssrf",
|
|
311
|
+
contexts: ["url"],
|
|
312
|
+
severity: "high",
|
|
313
|
+
confidence: "high",
|
|
314
|
+
description: "URL host is a loopback address",
|
|
315
|
+
cwe: 918,
|
|
316
|
+
primaryControl: SSRF_CONTROL,
|
|
317
|
+
pattern: /\/\/(?:127(?:\.\d{1,3}){3}|localhost|0\.0\.0\.0|\[?::1\]?)(?::\d{1,5})?(?:[/?#]|$)/i
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
id: "SSRF-PRIVATE-RANGE-001",
|
|
321
|
+
type: "ssrf",
|
|
322
|
+
contexts: ["url"],
|
|
323
|
+
severity: "high",
|
|
324
|
+
confidence: "high",
|
|
325
|
+
description: "URL host is in an RFC 1918 private range",
|
|
326
|
+
cwe: 918,
|
|
327
|
+
primaryControl: SSRF_CONTROL,
|
|
328
|
+
pattern: /\/\/(?:10(?:\.\d{1,3}){3}|192\.168(?:\.\d{1,3}){2}|172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2})/
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
id: "SSRF-METADATA-001",
|
|
332
|
+
type: "ssrf",
|
|
333
|
+
contexts: ["url"],
|
|
334
|
+
severity: "critical",
|
|
335
|
+
confidence: "high",
|
|
336
|
+
description: "URL targets a cloud instance metadata endpoint",
|
|
337
|
+
cwe: 918,
|
|
338
|
+
primaryControl: SSRF_CONTROL,
|
|
339
|
+
pattern: /\/\/(?:169\.254(?:\.\d{1,3}){2}|metadata\.google\.internal|metadata\.goog)\b/i
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
id: "SSRF-ALT-IP-ENCODING-001",
|
|
343
|
+
type: "ssrf",
|
|
344
|
+
contexts: ["url"],
|
|
345
|
+
severity: "high",
|
|
346
|
+
confidence: "high",
|
|
347
|
+
description: "Host written as a decimal, octal, or hex integer to evade address filters",
|
|
348
|
+
cwe: 918,
|
|
349
|
+
primaryControl: SSRF_CONTROL,
|
|
350
|
+
pattern: /\/\/(?:0x[0-9a-f]{6,8}|0\d{6,11}|\d{8,10})(?:[:/?#]|$)/i
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
id: "SSRF-USERINFO-001",
|
|
354
|
+
type: "ssrf",
|
|
355
|
+
contexts: ["url"],
|
|
356
|
+
severity: "medium",
|
|
357
|
+
confidence: "medium",
|
|
358
|
+
description: "Userinfo segment, used to disguise the true host",
|
|
359
|
+
cwe: 918,
|
|
360
|
+
primaryControl: SSRF_CONTROL,
|
|
361
|
+
pattern: /\/\/[^/@\s]{1,128}@/
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
id: "SSRF-NON-HTTP-SCHEME-001",
|
|
365
|
+
type: "ssrf",
|
|
366
|
+
contexts: ["url"],
|
|
367
|
+
severity: "high",
|
|
368
|
+
confidence: "high",
|
|
369
|
+
description: "Non-HTTP scheme in a fetchable URL",
|
|
370
|
+
cwe: 918,
|
|
371
|
+
primaryControl: SSRF_CONTROL,
|
|
372
|
+
pattern: /^\s{0,8}(?:file|gopher|dict|tftp|ldaps?|jar|netdoc|sftp):/i
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
id: "SSRF-USERINFO-INTERNAL-HOST-001",
|
|
376
|
+
type: "ssrf",
|
|
377
|
+
contexts: ["url"],
|
|
378
|
+
severity: "high",
|
|
379
|
+
confidence: "high",
|
|
380
|
+
description: "Internal address in the host position behind a userinfo segment",
|
|
381
|
+
cwe: 918,
|
|
382
|
+
primaryControl: SSRF_CONTROL,
|
|
383
|
+
pattern: /@(?: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
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
id: "SSRF-SUFFIXED-LOOPBACK-HOST-001",
|
|
387
|
+
type: "ssrf",
|
|
388
|
+
contexts: ["url"],
|
|
389
|
+
severity: "high",
|
|
390
|
+
confidence: "high",
|
|
391
|
+
description: "Loopback literal used as a hostname prefix",
|
|
392
|
+
cwe: 918,
|
|
393
|
+
primaryControl: SSRF_CONTROL,
|
|
394
|
+
pattern: /\/\/(?:127(?:\.\d{1,3}){3}|localhost|0\.0\.0\.0)\.[a-z]/i
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
id: "SSRF-METADATA-ALT-001",
|
|
398
|
+
type: "ssrf",
|
|
399
|
+
contexts: ["url"],
|
|
400
|
+
severity: "critical",
|
|
401
|
+
confidence: "high",
|
|
402
|
+
description: "Alibaba Cloud or Oracle Cloud instance metadata endpoint",
|
|
403
|
+
cwe: 918,
|
|
404
|
+
primaryControl: SSRF_CONTROL,
|
|
405
|
+
pattern: /\/\/(?:100\.100\.100\.200|192\.0\.0\.192)(?=[:/?#]|$)/
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
id: "SSRF-IPV6-INTERNAL-001",
|
|
409
|
+
type: "ssrf",
|
|
410
|
+
contexts: ["url"],
|
|
411
|
+
severity: "high",
|
|
412
|
+
confidence: "high",
|
|
413
|
+
description: "IPv6 loopback, unique-local, or link-local literal",
|
|
414
|
+
cwe: 918,
|
|
415
|
+
primaryControl: SSRF_CONTROL,
|
|
416
|
+
pattern: /(?:\/\/|@)\[(?:::|(?: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
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
id: "SSRF-IPV4-MAPPED-INTERNAL-001",
|
|
420
|
+
type: "ssrf",
|
|
421
|
+
contexts: ["url"],
|
|
422
|
+
severity: "high",
|
|
423
|
+
confidence: "high",
|
|
424
|
+
description: "IPv4-mapped IPv6 literal carrying an internal address",
|
|
425
|
+
cwe: 918,
|
|
426
|
+
primaryControl: SSRF_CONTROL,
|
|
427
|
+
pattern: /(?:\/\/|@)\[::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
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
id: "SSRF-SHORT-IP-FORM-001",
|
|
431
|
+
type: "ssrf",
|
|
432
|
+
contexts: ["url"],
|
|
433
|
+
severity: "high",
|
|
434
|
+
confidence: "medium",
|
|
435
|
+
description: "Abbreviated or octal IPv4 form resolving to a loopback address",
|
|
436
|
+
cwe: 918,
|
|
437
|
+
primaryControl: SSRF_CONTROL,
|
|
438
|
+
pattern: /\/\/(?:127\.\d{1,3}(?:\.\d{1,3})?|0|0\d{2,3}(?:\.0*\d{1,3}){3})(?=[:/?#]|$)/
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
id: "SSRF-WILDCARD-DNS-HOST-001",
|
|
442
|
+
type: "ssrf",
|
|
443
|
+
contexts: ["url"],
|
|
444
|
+
severity: "high",
|
|
445
|
+
confidence: "high",
|
|
446
|
+
description: "Wildcard-DNS service that resolves an arbitrary address from the hostname",
|
|
447
|
+
cwe: 918,
|
|
448
|
+
primaryControl: SSRF_CONTROL,
|
|
449
|
+
pattern: /\/\/(?:[^/\s]{0,64}\.)?(?:nip\.io|sslip\.io|localtest\.me|lvh\.me|vcap\.me|xip\.io|localho\.st|traefik\.me)(?=[:/?#]|$)/i
|
|
450
|
+
}
|
|
451
|
+
];
|
|
452
|
+
//#endregion
|
|
453
|
+
export { COMMAND_INJECTION_RULES, FILE_INCLUSION_RULES, PATH_TRAVERSAL_RULES, SSRF_RULES };
|
|
454
|
+
|
|
455
|
+
//# sourceMappingURL=system.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"system.mjs","names":[],"sources":["../../../src/threats/rules/system.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ Systems, Inc.\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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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ThreatRule } from "../types.mjs";
|
|
2
|
+
//#region src/threats/rules/web.d.ts
|
|
3
|
+
/** XSS signatures. Meaningful only where the value is interpolated into markup. */
|
|
4
|
+
declare const XSS_RULES: readonly ThreatRule[];
|
|
5
|
+
/**
|
|
6
|
+
* Prototype-pollution signatures.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately its own {@link ThreatRule.type} rather than a subcategory of XSS:
|
|
9
|
+
* this is an object-graph manipulation weakness, not a markup one, and it is fixed
|
|
10
|
+
* by different controls (`Object.create(null)` dictionaries, schema validation that
|
|
11
|
+
* rejects unknown keys, merge helpers that skip inherited and forbidden keys).
|
|
12
|
+
*
|
|
13
|
+
* The patterns match structural property paths, so `__proto__` reached via bracket
|
|
14
|
+
* notation or as a quoted JSON key is caught while prose that merely mentions the
|
|
15
|
+
* word is not.
|
|
16
|
+
*/
|
|
17
|
+
declare const PROTOTYPE_POLLUTION_RULES: readonly ThreatRule[];
|
|
18
|
+
/** CRLF injection into HTTP or SMTP header values (response splitting). */
|
|
19
|
+
declare const HEADER_INJECTION_RULES: readonly ThreatRule[];
|
|
20
|
+
/** CSV/XLSX formula injection — the payload executes in the *recipient's* spreadsheet. */
|
|
21
|
+
declare const FORMULA_INJECTION_RULES: readonly ThreatRule[];
|
|
22
|
+
/** Log injection — forged entries, and terminal escape sequences in logged content. */
|
|
23
|
+
declare const LOG_INJECTION_RULES: readonly ThreatRule[];
|
|
24
|
+
//#endregion
|
|
25
|
+
export { FORMULA_INJECTION_RULES, HEADER_INJECTION_RULES, LOG_INJECTION_RULES, PROTOTYPE_POLLUTION_RULES, XSS_RULES };
|
|
26
|
+
//# sourceMappingURL=web.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web.d.mts","names":[],"sources":["../../../src/threats/rules/web.ts"],"mappings":";;;cAqCa,oBAAoB;;;;;;;;;;;;;cAuQpB,oCAAoC;;cAgDpC,iCAAiC;;cA4EjC,kCAAkC;;cAmClC,8BAA8B"}
|