getdoorman 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +181 -0
- package/bin/doorman.js +444 -0
- package/package.json +74 -0
- package/src/ai-fixer.js +559 -0
- package/src/ast-scanner.js +434 -0
- package/src/auth.js +149 -0
- package/src/baseline.js +48 -0
- package/src/compliance.js +539 -0
- package/src/config.js +466 -0
- package/src/custom-rules.js +32 -0
- package/src/dashboard.js +202 -0
- package/src/detector.js +142 -0
- package/src/fix-engine.js +48 -0
- package/src/fix-registry-extra.js +95 -0
- package/src/fix-registry-go-rust.js +77 -0
- package/src/fix-registry-java-csharp.js +77 -0
- package/src/fix-registry-js.js +99 -0
- package/src/fix-registry-mcp-ai.js +57 -0
- package/src/fix-registry-python.js +87 -0
- package/src/fixer-ruby-php.js +608 -0
- package/src/fixer.js +2113 -0
- package/src/hooks.js +115 -0
- package/src/ignore.js +176 -0
- package/src/index.js +384 -0
- package/src/metrics.js +126 -0
- package/src/monorepo.js +65 -0
- package/src/presets.js +54 -0
- package/src/reporter.js +975 -0
- package/src/rule-worker.js +36 -0
- package/src/rules/ast-rules.js +756 -0
- package/src/rules/bugs/accessibility.js +235 -0
- package/src/rules/bugs/ai-codegen-fixable.js +172 -0
- package/src/rules/bugs/ai-codegen.js +365 -0
- package/src/rules/bugs/code-smell-bugs.js +247 -0
- package/src/rules/bugs/crypto-bugs.js +195 -0
- package/src/rules/bugs/docker-bugs.js +158 -0
- package/src/rules/bugs/general.js +361 -0
- package/src/rules/bugs/go-bugs.js +279 -0
- package/src/rules/bugs/index.js +73 -0
- package/src/rules/bugs/js-api.js +257 -0
- package/src/rules/bugs/js-array-object.js +210 -0
- package/src/rules/bugs/js-async-fixable.js +223 -0
- package/src/rules/bugs/js-async.js +211 -0
- package/src/rules/bugs/js-closure-scope.js +182 -0
- package/src/rules/bugs/js-database.js +203 -0
- package/src/rules/bugs/js-error-handling.js +148 -0
- package/src/rules/bugs/js-logic.js +261 -0
- package/src/rules/bugs/js-memory.js +214 -0
- package/src/rules/bugs/js-node.js +361 -0
- package/src/rules/bugs/js-react.js +373 -0
- package/src/rules/bugs/js-regex.js +200 -0
- package/src/rules/bugs/js-state.js +272 -0
- package/src/rules/bugs/js-type-coercion.js +318 -0
- package/src/rules/bugs/nextjs-bugs.js +242 -0
- package/src/rules/bugs/nextjs-fixable.js +120 -0
- package/src/rules/bugs/node-fixable.js +178 -0
- package/src/rules/bugs/python-advanced.js +245 -0
- package/src/rules/bugs/python-fixable.js +98 -0
- package/src/rules/bugs/python.js +284 -0
- package/src/rules/bugs/react-fixable.js +207 -0
- package/src/rules/bugs/ruby-bugs.js +182 -0
- package/src/rules/bugs/shell-bugs.js +181 -0
- package/src/rules/bugs/silent-failures.js +261 -0
- package/src/rules/bugs/ts-bugs.js +235 -0
- package/src/rules/bugs/unused-vars.js +65 -0
- package/src/rules/compliance/accessibility-ext.js +468 -0
- package/src/rules/compliance/education.js +322 -0
- package/src/rules/compliance/financial.js +421 -0
- package/src/rules/compliance/frameworks.js +507 -0
- package/src/rules/compliance/healthcare.js +520 -0
- package/src/rules/compliance/index.js +2714 -0
- package/src/rules/compliance/regional-eu.js +480 -0
- package/src/rules/compliance/regional-international.js +903 -0
- package/src/rules/cost/index.js +1993 -0
- package/src/rules/data/index.js +2503 -0
- package/src/rules/dependencies/index.js +1684 -0
- package/src/rules/deployment/index.js +2050 -0
- package/src/rules/index.js +71 -0
- package/src/rules/infrastructure/index.js +3048 -0
- package/src/rules/performance/index.js +3455 -0
- package/src/rules/quality/index.js +3175 -0
- package/src/rules/reliability/index.js +3040 -0
- package/src/rules/scope-rules.js +815 -0
- package/src/rules/security/ai-api.js +1177 -0
- package/src/rules/security/auth.js +1328 -0
- package/src/rules/security/cors.js +127 -0
- package/src/rules/security/crypto.js +527 -0
- package/src/rules/security/csharp.js +862 -0
- package/src/rules/security/csrf.js +193 -0
- package/src/rules/security/dart.js +835 -0
- package/src/rules/security/deserialization.js +291 -0
- package/src/rules/security/file-upload.js +187 -0
- package/src/rules/security/go.js +850 -0
- package/src/rules/security/headers.js +235 -0
- package/src/rules/security/index.js +65 -0
- package/src/rules/security/injection.js +1639 -0
- package/src/rules/security/mcp-server.js +71 -0
- package/src/rules/security/misconfiguration.js +660 -0
- package/src/rules/security/oauth-jwt.js +329 -0
- package/src/rules/security/path-traversal.js +295 -0
- package/src/rules/security/php.js +1054 -0
- package/src/rules/security/prototype-pollution.js +283 -0
- package/src/rules/security/rate-limiting.js +208 -0
- package/src/rules/security/ruby.js +1061 -0
- package/src/rules/security/rust.js +693 -0
- package/src/rules/security/secrets.js +747 -0
- package/src/rules/security/shell.js +647 -0
- package/src/rules/security/ssrf.js +298 -0
- package/src/rules/security/supply-chain-advanced.js +393 -0
- package/src/rules/security/supply-chain.js +734 -0
- package/src/rules/security/swift.js +835 -0
- package/src/rules/security/taint.js +27 -0
- package/src/rules/security/xss.js +520 -0
- package/src/scan-cache.js +71 -0
- package/src/scanner.js +710 -0
- package/src/scope-analyzer.js +685 -0
- package/src/share.js +88 -0
- package/src/taint.js +300 -0
- package/src/telemetry.js +183 -0
- package/src/tracer.js +190 -0
- package/src/upload.js +35 -0
- package/src/worker.js +31 -0
|
@@ -0,0 +1,1639 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injection Detection Rules (SEC-INJ-001 through SEC-INJ-020)
|
|
3
|
+
*
|
|
4
|
+
* Each rule scans source files for injection vulnerabilities and returns
|
|
5
|
+
* an array of findings.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
|
|
9
|
+
const isJS = (f) => JS_EXT.some(ext => f.endsWith(ext));
|
|
10
|
+
const isPy = (f) => f.endsWith('.py');
|
|
11
|
+
|
|
12
|
+
const SKIP_PATH = /[/\\](test|tests|__tests__|__mocks__|mocks|fixtures|__fixtures__|spec|__snapshots__|node_modules|vendor|dist|build)[/\\]/i;
|
|
13
|
+
const COMMENT_LINE = /^\s*(\/\/|#|\/\*|\*)/;
|
|
14
|
+
|
|
15
|
+
function scanLines(content, regex, file, rule) {
|
|
16
|
+
const findings = [];
|
|
17
|
+
const lines = content.split('\n');
|
|
18
|
+
for (let i = 0; i < lines.length; i++) {
|
|
19
|
+
const line = lines[i];
|
|
20
|
+
if (COMMENT_LINE.test(line)) continue;
|
|
21
|
+
if (regex.test(line)) {
|
|
22
|
+
findings.push({
|
|
23
|
+
ruleId: rule.id,
|
|
24
|
+
category: rule.category,
|
|
25
|
+
severity: rule.severity,
|
|
26
|
+
title: rule.title,
|
|
27
|
+
description: rule.description,
|
|
28
|
+
confidence: rule.confidence,
|
|
29
|
+
file,
|
|
30
|
+
line: i + 1,
|
|
31
|
+
fix: rule.fix || null,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return findings;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const rules = [
|
|
39
|
+
// ---------------------------------------------------------------
|
|
40
|
+
// SEC-INJ-001: SQL injection via string concatenation
|
|
41
|
+
// ---------------------------------------------------------------
|
|
42
|
+
{
|
|
43
|
+
id: 'SEC-INJ-001',
|
|
44
|
+
category: 'security',
|
|
45
|
+
severity: 'critical',
|
|
46
|
+
confidence: 'likely',
|
|
47
|
+
title: 'SQL Injection via String Concatenation',
|
|
48
|
+
description:
|
|
49
|
+
'SQL query built with string concatenation or template literals allows attacker-controlled input to alter the query structure.',
|
|
50
|
+
fix: { suggestion: 'Use parameterized queries or prepared statements instead of string interpolation.' },
|
|
51
|
+
check({ files }) {
|
|
52
|
+
const findings = [];
|
|
53
|
+
const jsPattern = /(?:query|execute|raw|\.sql)\s*\(\s*`[^`]*\$\{/;
|
|
54
|
+
const jsConcatPattern = /(?:query|execute|raw|\.sql)\s*\(\s*(?:['"][^'"]*['"]\s*\+|\w+\s*\+\s*['"])/;
|
|
55
|
+
// Match Python f-strings, .format(), string concatenation, and % formatting
|
|
56
|
+
// in execute() calls. Exclude psycopg2-style parameterized queries where
|
|
57
|
+
// %s is a DB placeholder (execute("...%s...", (val,))).
|
|
58
|
+
const pyFString = /(?:execute|cursor\.execute|\.raw)\s*\(\s*f['"]/;
|
|
59
|
+
const pyFormat = /(?:execute|cursor\.execute|\.raw)\s*\(\s*['"].*\bformat\b/;
|
|
60
|
+
const pyConcat = /(?:execute|cursor\.execute|\.raw)\s*\(\s*['"].*\+/;
|
|
61
|
+
const pyPercent = /(?:execute|cursor\.execute|\.raw)\s*\(\s*['"].*%s.*['"]\s*%/;
|
|
62
|
+
|
|
63
|
+
for (const [path, content] of files) {
|
|
64
|
+
if (SKIP_PATH.test(path)) continue;
|
|
65
|
+
if (isJS(path)) {
|
|
66
|
+
findings.push(...scanLines(content, jsPattern, path, this));
|
|
67
|
+
findings.push(...scanLines(content, jsConcatPattern, path, this));
|
|
68
|
+
} else if (isPy(path)) {
|
|
69
|
+
findings.push(...scanLines(content, pyFString, path, this));
|
|
70
|
+
findings.push(...scanLines(content, pyFormat, path, this));
|
|
71
|
+
findings.push(...scanLines(content, pyConcat, path, this));
|
|
72
|
+
findings.push(...scanLines(content, pyPercent, path, this));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return findings;
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------
|
|
80
|
+
// SEC-INJ-002: SQL injection via ORM raw queries
|
|
81
|
+
// ---------------------------------------------------------------
|
|
82
|
+
{
|
|
83
|
+
id: 'SEC-INJ-002',
|
|
84
|
+
category: 'security',
|
|
85
|
+
severity: 'critical',
|
|
86
|
+
confidence: 'likely',
|
|
87
|
+
title: 'SQL Injection via ORM Raw Queries',
|
|
88
|
+
description:
|
|
89
|
+
'Raw query methods in ORMs (Sequelize.query, prisma.$queryRaw, knex.raw) used with string interpolation bypass ORM protections.',
|
|
90
|
+
fix: { suggestion: 'Use tagged template literals (e.g., Prisma.sql``) or pass bind parameters as a second argument.' },
|
|
91
|
+
check({ files }) {
|
|
92
|
+
const findings = [];
|
|
93
|
+
const pattern = /(?:sequelize\.query|prisma\.\$queryRaw(?:Unsafe)?|knex\.raw|typeorm\.query)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+)/i;
|
|
94
|
+
|
|
95
|
+
for (const [path, content] of files) {
|
|
96
|
+
if (SKIP_PATH.test(path)) continue;
|
|
97
|
+
if (isJS(path)) {
|
|
98
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return findings;
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------
|
|
106
|
+
// SEC-INJ-003: NoSQL injection via MongoDB
|
|
107
|
+
// ---------------------------------------------------------------
|
|
108
|
+
{
|
|
109
|
+
id: 'SEC-INJ-003',
|
|
110
|
+
category: 'security',
|
|
111
|
+
severity: 'critical',
|
|
112
|
+
confidence: 'likely',
|
|
113
|
+
title: 'NoSQL Injection via MongoDB',
|
|
114
|
+
description:
|
|
115
|
+
'Passing unsanitized user input to MongoDB query operators ($where, $regex) or directly to find/findOne enables NoSQL injection.',
|
|
116
|
+
fix: { suggestion: 'Validate and sanitize input. Avoid passing req.body directly to query methods. Use mongo-sanitize or express-mongo-sanitize.' },
|
|
117
|
+
check({ files }) {
|
|
118
|
+
const findings = [];
|
|
119
|
+
const wherePattern = /\$where\s*:/;
|
|
120
|
+
const regexFromInput = /\$regex\s*:\s*(?:req\.|params|query|body|input|user)/;
|
|
121
|
+
const directBody = /\.(?:find|findOne|findOneAndUpdate|updateOne|updateMany|deleteOne|deleteMany|aggregate)\s*\(\s*req\.body/;
|
|
122
|
+
|
|
123
|
+
for (const [path, content] of files) {
|
|
124
|
+
if (SKIP_PATH.test(path)) continue;
|
|
125
|
+
if (isJS(path)) {
|
|
126
|
+
findings.push(...scanLines(content, wherePattern, path, this));
|
|
127
|
+
findings.push(...scanLines(content, regexFromInput, path, this));
|
|
128
|
+
findings.push(...scanLines(content, directBody, path, this));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return findings;
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------
|
|
136
|
+
// SEC-INJ-004: Command injection via exec
|
|
137
|
+
// ---------------------------------------------------------------
|
|
138
|
+
{
|
|
139
|
+
id: 'SEC-INJ-004',
|
|
140
|
+
category: 'security',
|
|
141
|
+
severity: 'critical',
|
|
142
|
+
confidence: 'likely',
|
|
143
|
+
title: 'Command Injection via exec',
|
|
144
|
+
description:
|
|
145
|
+
'Using child_process.exec or execSync with template literals or string concatenation allows arbitrary command execution.',
|
|
146
|
+
fix: { suggestion: 'Use execFile or spawn (without shell: true) with an argument array instead of exec.' },
|
|
147
|
+
check({ files }) {
|
|
148
|
+
const findings = [];
|
|
149
|
+
const templatePattern = /(?:exec|execSync)\s*\(\s*`[^`]*\$\{/;
|
|
150
|
+
const concatPattern = /(?:exec|execSync)\s*\(\s*(?:['"][^'"]*['"]\s*\+|\w+\s*\+\s*['"])/;
|
|
151
|
+
const pyPattern = /(?:os\.system|os\.popen|subprocess\.call|subprocess\.run|subprocess\.Popen)\s*\(\s*(?:f['"]|['"].*%|['"].*\+|['"].*\.format)/;
|
|
152
|
+
|
|
153
|
+
for (const [path, content] of files) {
|
|
154
|
+
if (SKIP_PATH.test(path)) continue;
|
|
155
|
+
if (isJS(path)) {
|
|
156
|
+
findings.push(...scanLines(content, templatePattern, path, this));
|
|
157
|
+
findings.push(...scanLines(content, concatPattern, path, this));
|
|
158
|
+
} else if (isPy(path)) {
|
|
159
|
+
findings.push(...scanLines(content, pyPattern, path, this));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return findings;
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------
|
|
167
|
+
// SEC-INJ-005: Command injection via shell spawn
|
|
168
|
+
// ---------------------------------------------------------------
|
|
169
|
+
{
|
|
170
|
+
id: 'SEC-INJ-005',
|
|
171
|
+
category: 'security',
|
|
172
|
+
severity: 'critical',
|
|
173
|
+
confidence: 'likely',
|
|
174
|
+
title: 'Command Injection via Shell Spawn',
|
|
175
|
+
description:
|
|
176
|
+
'Using spawn or spawnSync with shell: true and variable input allows command injection through shell metacharacters.',
|
|
177
|
+
fix: { suggestion: 'Remove shell: true and pass arguments as an array. Validate and sanitize all input.' },
|
|
178
|
+
check({ files }) {
|
|
179
|
+
const findings = [];
|
|
180
|
+
const pattern = /spawn(?:Sync)?\s*\([^)]*shell\s*:\s*true/;
|
|
181
|
+
|
|
182
|
+
for (const [path, content] of files) {
|
|
183
|
+
if (SKIP_PATH.test(path)) continue;
|
|
184
|
+
if (isJS(path)) {
|
|
185
|
+
// Multi-line aware: check blocks of code for spawn with shell:true
|
|
186
|
+
const lines = content.split('\n');
|
|
187
|
+
let inSpawn = false;
|
|
188
|
+
let spawnLine = 0;
|
|
189
|
+
for (let i = 0; i < lines.length; i++) {
|
|
190
|
+
const line = lines[i];
|
|
191
|
+
if (COMMENT_LINE.test(line)) continue;
|
|
192
|
+
if (/spawn(?:Sync)?\s*\(/.test(line)) {
|
|
193
|
+
inSpawn = true;
|
|
194
|
+
spawnLine = i;
|
|
195
|
+
}
|
|
196
|
+
if (inSpawn && /shell\s*:\s*true/.test(line)) {
|
|
197
|
+
findings.push({
|
|
198
|
+
ruleId: this.id,
|
|
199
|
+
category: this.category,
|
|
200
|
+
severity: this.severity,
|
|
201
|
+
title: this.title,
|
|
202
|
+
description: this.description,
|
|
203
|
+
confidence: this.confidence,
|
|
204
|
+
file: path,
|
|
205
|
+
line: spawnLine + 1,
|
|
206
|
+
fix: this.fix,
|
|
207
|
+
});
|
|
208
|
+
inSpawn = false;
|
|
209
|
+
}
|
|
210
|
+
if (inSpawn && /[)}];/.test(line)) {
|
|
211
|
+
inSpawn = false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Also catch single-line occurrences
|
|
215
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Deduplicate by file+line
|
|
219
|
+
const seen = new Set();
|
|
220
|
+
return findings.filter((f) => {
|
|
221
|
+
const key = `${f.file}:${f.line}`;
|
|
222
|
+
if (seen.has(key)) return false;
|
|
223
|
+
seen.add(key);
|
|
224
|
+
return true;
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
// ---------------------------------------------------------------
|
|
230
|
+
// SEC-INJ-006: LDAP injection
|
|
231
|
+
// ---------------------------------------------------------------
|
|
232
|
+
{
|
|
233
|
+
id: 'SEC-INJ-006',
|
|
234
|
+
category: 'security',
|
|
235
|
+
severity: 'high',
|
|
236
|
+
confidence: 'likely',
|
|
237
|
+
title: 'LDAP Injection',
|
|
238
|
+
description:
|
|
239
|
+
'LDAP search filters built with user input concatenation allow attackers to modify LDAP queries.',
|
|
240
|
+
fix: { suggestion: 'Use ldap-escape or parameterized LDAP filter construction.' },
|
|
241
|
+
check({ files }) {
|
|
242
|
+
const findings = [];
|
|
243
|
+
const templatePattern = /(?:ldap|client)\.search\s*\([^)]*`[^`]*\$\{/i;
|
|
244
|
+
const concatPattern = /(?:filter|ldapFilter|searchFilter)\s*[:=]\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+)/i;
|
|
245
|
+
|
|
246
|
+
for (const [path, content] of files) {
|
|
247
|
+
if (SKIP_PATH.test(path)) continue;
|
|
248
|
+
if (isJS(path) || isPy(path)) {
|
|
249
|
+
findings.push(...scanLines(content, templatePattern, path, this));
|
|
250
|
+
findings.push(...scanLines(content, concatPattern, path, this));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return findings;
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
// ---------------------------------------------------------------
|
|
258
|
+
// SEC-INJ-007: XPath injection
|
|
259
|
+
// ---------------------------------------------------------------
|
|
260
|
+
{
|
|
261
|
+
id: 'SEC-INJ-007',
|
|
262
|
+
category: 'security',
|
|
263
|
+
severity: 'high',
|
|
264
|
+
confidence: 'likely',
|
|
265
|
+
title: 'XPath Injection',
|
|
266
|
+
description:
|
|
267
|
+
'XPath expressions built with string concatenation or template literals allow attackers to manipulate XML queries.',
|
|
268
|
+
fix: { suggestion: 'Use parameterized XPath queries or validate/escape user input before embedding in XPath expressions.' },
|
|
269
|
+
check({ files }) {
|
|
270
|
+
const findings = [];
|
|
271
|
+
const pattern = /(?:xpath\.(?:evaluate|select)|\.evaluate)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+)/i;
|
|
272
|
+
const concatPattern = /(?:xpathQuery|xpathExpr|xpathExpression)\s*[:=]\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+)/i;
|
|
273
|
+
|
|
274
|
+
for (const [path, content] of files) {
|
|
275
|
+
if (SKIP_PATH.test(path)) continue;
|
|
276
|
+
if (isJS(path)) {
|
|
277
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
278
|
+
findings.push(...scanLines(content, concatPattern, path, this));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return findings;
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
// ---------------------------------------------------------------
|
|
286
|
+
// SEC-INJ-008: Server-side template injection (SSTI)
|
|
287
|
+
// ---------------------------------------------------------------
|
|
288
|
+
{
|
|
289
|
+
id: 'SEC-INJ-008',
|
|
290
|
+
category: 'security',
|
|
291
|
+
severity: 'critical',
|
|
292
|
+
confidence: 'likely',
|
|
293
|
+
title: 'Server-Side Template Injection (SSTI)',
|
|
294
|
+
description:
|
|
295
|
+
'Rendering templates from user-controlled strings (ejs.render, pug.render, nunjucks.renderString) can lead to remote code execution.',
|
|
296
|
+
fix: { suggestion: 'Never pass user input as the template string. Use pre-compiled templates and pass user data as context variables.' },
|
|
297
|
+
check({ files }) {
|
|
298
|
+
const findings = [];
|
|
299
|
+
const pattern = /(?:ejs\.render|pug\.render|nunjucks\.renderString|handlebars\.compile|mustache\.render|doT\.template)\s*\(\s*(?:req\.|params|query|body|input|user|\w*[Ii]nput|\w*[Dd]ata)/;
|
|
300
|
+
const templateFromVar = /(?:ejs\.render|pug\.render|nunjucks\.renderString)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+)/;
|
|
301
|
+
|
|
302
|
+
for (const [path, content] of files) {
|
|
303
|
+
if (SKIP_PATH.test(path)) continue;
|
|
304
|
+
if (isJS(path)) {
|
|
305
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
306
|
+
findings.push(...scanLines(content, templateFromVar, path, this));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return findings;
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
// ---------------------------------------------------------------
|
|
314
|
+
// SEC-INJ-009: Header injection (CRLF)
|
|
315
|
+
// ---------------------------------------------------------------
|
|
316
|
+
{
|
|
317
|
+
id: 'SEC-INJ-009',
|
|
318
|
+
category: 'security',
|
|
319
|
+
severity: 'high',
|
|
320
|
+
confidence: 'likely',
|
|
321
|
+
title: 'HTTP Header Injection (CRLF)',
|
|
322
|
+
description:
|
|
323
|
+
'Setting HTTP headers with unsanitized user input can allow CRLF injection to add arbitrary headers or split the response.',
|
|
324
|
+
fix: { suggestion: 'Validate and sanitize header values. Strip \\r and \\n characters from any user-controlled header values.' },
|
|
325
|
+
check({ files }) {
|
|
326
|
+
const findings = [];
|
|
327
|
+
const pattern = /(?:setHeader|writeHead|res\.set|res\.header)\s*\([^)]*(?:req\.(?:params|query|body|headers)|input|user)/i;
|
|
328
|
+
const redirectPattern = /(?:res\.redirect|res\.location)\s*\(\s*(?:req\.(?:params|query|body)|input)/;
|
|
329
|
+
|
|
330
|
+
for (const [path, content] of files) {
|
|
331
|
+
if (SKIP_PATH.test(path)) continue;
|
|
332
|
+
if (isJS(path)) {
|
|
333
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
334
|
+
findings.push(...scanLines(content, redirectPattern, path, this));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return findings;
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
// ---------------------------------------------------------------
|
|
342
|
+
// SEC-INJ-010: Log injection
|
|
343
|
+
// ---------------------------------------------------------------
|
|
344
|
+
{
|
|
345
|
+
id: 'SEC-INJ-010',
|
|
346
|
+
category: 'security',
|
|
347
|
+
severity: 'medium',
|
|
348
|
+
confidence: 'likely',
|
|
349
|
+
title: 'Log Injection',
|
|
350
|
+
description:
|
|
351
|
+
'Logging unsanitized user input (req.body, req.params, req.query) can allow log forging, log poisoning, or CRLF in log entries.',
|
|
352
|
+
fix: { suggestion: 'Sanitize or encode user input before logging. Strip newlines and control characters.' },
|
|
353
|
+
check({ files }) {
|
|
354
|
+
const findings = [];
|
|
355
|
+
const pattern = /(?:console\.(?:log|info|warn|error|debug)|logger\.(?:log|info|warn|error|debug)|log\.(?:info|warn|error|debug))\s*\([^)]*req\.(?:body|params|query)/;
|
|
356
|
+
|
|
357
|
+
for (const [path, content] of files) {
|
|
358
|
+
if (SKIP_PATH.test(path)) continue;
|
|
359
|
+
if (isJS(path)) {
|
|
360
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return findings;
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
// ---------------------------------------------------------------
|
|
368
|
+
// SEC-INJ-011: eval() and similar dynamic code execution
|
|
369
|
+
// ---------------------------------------------------------------
|
|
370
|
+
{
|
|
371
|
+
id: 'SEC-INJ-011',
|
|
372
|
+
category: 'security',
|
|
373
|
+
severity: 'critical',
|
|
374
|
+
confidence: 'definite',
|
|
375
|
+
title: 'Dynamic Code Execution (eval)',
|
|
376
|
+
description:
|
|
377
|
+
'eval(), new Function(), and setTimeout/setInterval with string arguments allow arbitrary code execution.',
|
|
378
|
+
fix: { suggestion: 'Avoid eval and new Function entirely. Use JSON.parse for data, and function references for setTimeout/setInterval.' },
|
|
379
|
+
check({ files }) {
|
|
380
|
+
const findings = [];
|
|
381
|
+
const evalPattern = /\beval\s*\(/;
|
|
382
|
+
const newFunctionPattern = /new\s+Function\s*\(/;
|
|
383
|
+
const timerStringPattern = /(?:setTimeout|setInterval)\s*\(\s*['"`]/;
|
|
384
|
+
|
|
385
|
+
for (const [path, content] of files) {
|
|
386
|
+
if (SKIP_PATH.test(path)) continue;
|
|
387
|
+
if (isJS(path)) {
|
|
388
|
+
findings.push(...scanLines(content, evalPattern, path, this));
|
|
389
|
+
findings.push(...scanLines(content, newFunctionPattern, path, this));
|
|
390
|
+
findings.push(...scanLines(content, timerStringPattern, path, this));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return findings;
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
// ---------------------------------------------------------------
|
|
398
|
+
// SEC-INJ-012: Regular expression injection (ReDoS)
|
|
399
|
+
// ---------------------------------------------------------------
|
|
400
|
+
{
|
|
401
|
+
id: 'SEC-INJ-012',
|
|
402
|
+
category: 'security',
|
|
403
|
+
severity: 'high',
|
|
404
|
+
confidence: 'likely',
|
|
405
|
+
title: 'Regular Expression Injection (ReDoS)',
|
|
406
|
+
description:
|
|
407
|
+
'Constructing RegExp from user input can cause ReDoS (catastrophic backtracking) or allow regex injection.',
|
|
408
|
+
fix: { suggestion: 'Use a safe regex library (e.g., escape-string-regexp) to escape user input before using in RegExp, or use re2.' },
|
|
409
|
+
check({ files }) {
|
|
410
|
+
const findings = [];
|
|
411
|
+
const pattern = /new\s+RegExp\s*\(\s*(?:req\.|params|query|body|input|user|\w*[Ii]nput|\w*[Pp]attern|\w*[Ss]earch)/;
|
|
412
|
+
const templatePattern = /new\s+RegExp\s*\(\s*`[^`]*\$\{/;
|
|
413
|
+
const concatPattern = /new\s+RegExp\s*\(\s*(?:['"][^'"]*['"]\s*\+|\w+\s*\+)/;
|
|
414
|
+
|
|
415
|
+
for (const [path, content] of files) {
|
|
416
|
+
if (SKIP_PATH.test(path)) continue;
|
|
417
|
+
if (isJS(path)) {
|
|
418
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
419
|
+
findings.push(...scanLines(content, templatePattern, path, this));
|
|
420
|
+
findings.push(...scanLines(content, concatPattern, path, this));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// Deduplicate by file+line
|
|
424
|
+
const seen = new Set();
|
|
425
|
+
return findings.filter((f) => {
|
|
426
|
+
const key = `${f.file}:${f.line}`;
|
|
427
|
+
if (seen.has(key)) return false;
|
|
428
|
+
seen.add(key);
|
|
429
|
+
return true;
|
|
430
|
+
});
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
|
|
434
|
+
// ---------------------------------------------------------------
|
|
435
|
+
// SEC-INJ-013: GraphQL injection
|
|
436
|
+
// ---------------------------------------------------------------
|
|
437
|
+
{
|
|
438
|
+
id: 'SEC-INJ-013',
|
|
439
|
+
category: 'security',
|
|
440
|
+
severity: 'high',
|
|
441
|
+
confidence: 'likely',
|
|
442
|
+
title: 'GraphQL Injection',
|
|
443
|
+
description:
|
|
444
|
+
'Building GraphQL queries with template literals or string concatenation allows query manipulation.',
|
|
445
|
+
fix: { suggestion: 'Use GraphQL variables and parameterized queries. Never interpolate user input into query strings.' },
|
|
446
|
+
check({ files }) {
|
|
447
|
+
const findings = [];
|
|
448
|
+
const templatePattern = /(?:graphql|gql|query|mutation)\s*(?:=|:|\()?\s*`[^`]*\$\{[^}]*(?:req\.|input|param|arg|user|body|query)/i;
|
|
449
|
+
const concatPattern = /(?:graphql|gql)\s*\(\s*['"][^'"]*['"]\s*\+/i;
|
|
450
|
+
const fetchPattern = /(?:body|query)\s*:\s*(?:`[^`]*(?:query|mutation)[^`]*\$\{|['"][^'"]*(?:query|mutation)[^'"]*['"]\s*\+)/;
|
|
451
|
+
|
|
452
|
+
for (const [path, content] of files) {
|
|
453
|
+
if (SKIP_PATH.test(path)) continue;
|
|
454
|
+
if (isJS(path)) {
|
|
455
|
+
findings.push(...scanLines(content, templatePattern, path, this));
|
|
456
|
+
findings.push(...scanLines(content, concatPattern, path, this));
|
|
457
|
+
findings.push(...scanLines(content, fetchPattern, path, this));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return findings;
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
// ---------------------------------------------------------------
|
|
465
|
+
// SEC-INJ-014: ORM dynamic column names
|
|
466
|
+
// ---------------------------------------------------------------
|
|
467
|
+
{
|
|
468
|
+
id: 'SEC-INJ-014',
|
|
469
|
+
category: 'security',
|
|
470
|
+
severity: 'high',
|
|
471
|
+
confidence: 'likely',
|
|
472
|
+
title: 'ORM Dynamic Column Names',
|
|
473
|
+
description:
|
|
474
|
+
'Using user-supplied values as column names in orderBy, groupBy, or where clauses can lead to SQL injection through the ORM.',
|
|
475
|
+
fix: { suggestion: 'Whitelist allowed column names. Map user input to known-safe column identifiers.' },
|
|
476
|
+
check({ files }) {
|
|
477
|
+
const findings = [];
|
|
478
|
+
const bracketPattern = /(?:orderBy|order|groupBy|group|where|select)\s*(?:\(|\[|:)\s*\[?\s*(?:req\.(?:body|query|params)|input|user)/i;
|
|
479
|
+
const dynamicColumn = /(?:orderBy|sortBy|groupBy)\s*\(\s*(?:req\.(?:body|query|params)|input|user)/;
|
|
480
|
+
const bracketAccess = /\[(?:req\.(?:body|query|params)|input|user)[^\]]*\]/;
|
|
481
|
+
|
|
482
|
+
for (const [path, content] of files) {
|
|
483
|
+
if (SKIP_PATH.test(path)) continue;
|
|
484
|
+
if (isJS(path)) {
|
|
485
|
+
findings.push(...scanLines(content, bracketPattern, path, this));
|
|
486
|
+
findings.push(...scanLines(content, dynamicColumn, path, this));
|
|
487
|
+
const lines = content.split('\n');
|
|
488
|
+
for (let i = 0; i < lines.length; i++) {
|
|
489
|
+
const line = lines[i];
|
|
490
|
+
if (COMMENT_LINE.test(line)) continue;
|
|
491
|
+
if (bracketAccess.test(line) && /(?:orderBy|groupBy|where|column|field)/i.test(line)) {
|
|
492
|
+
findings.push({
|
|
493
|
+
ruleId: this.id,
|
|
494
|
+
category: this.category,
|
|
495
|
+
severity: this.severity,
|
|
496
|
+
title: this.title,
|
|
497
|
+
description: this.description,
|
|
498
|
+
confidence: this.confidence,
|
|
499
|
+
file: path,
|
|
500
|
+
line: i + 1,
|
|
501
|
+
fix: this.fix,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const seen = new Set();
|
|
508
|
+
return findings.filter((f) => {
|
|
509
|
+
const key = `${f.file}:${f.line}`;
|
|
510
|
+
if (seen.has(key)) return false;
|
|
511
|
+
seen.add(key);
|
|
512
|
+
return true;
|
|
513
|
+
});
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
|
|
517
|
+
// ---------------------------------------------------------------
|
|
518
|
+
// SEC-INJ-015: XXE (XML External Entity)
|
|
519
|
+
// ---------------------------------------------------------------
|
|
520
|
+
{
|
|
521
|
+
id: 'SEC-INJ-015',
|
|
522
|
+
category: 'security',
|
|
523
|
+
severity: 'critical',
|
|
524
|
+
confidence: 'likely',
|
|
525
|
+
title: 'XML External Entity (XXE) Injection',
|
|
526
|
+
description:
|
|
527
|
+
'Parsing XML without disabling external entities allows XXE attacks that can read local files or perform SSRF.',
|
|
528
|
+
fix: { suggestion: 'Disable external entity processing. For xml2js use {xmldec: {standalone: true}}. For libxmljs set noent: false. Consider using a safe parser.' },
|
|
529
|
+
check({ files }) {
|
|
530
|
+
const findings = [];
|
|
531
|
+
// Detect xml2js parseString without explicit entity disable
|
|
532
|
+
const xml2jsPattern = /(?:parseString|parseStringPromise|xml2js\.Parser)\s*\(/;
|
|
533
|
+
// Detect libxmljs with noent: true (unsafe)
|
|
534
|
+
const libxmljsUnsafe = /libxmljs\.parseXml(?:String)?\s*\([^)]*noent\s*:\s*true/;
|
|
535
|
+
// Detect DOMParser or generic XML parse
|
|
536
|
+
const domParserPattern = /new\s+DOMParser\s*\(\s*\)\.parseFromString\s*\([^)]*(?:req\.|input|body|user)/;
|
|
537
|
+
// Detect DOCTYPE in template literals (potential XXE payload construction)
|
|
538
|
+
const doctypePattern = /<!DOCTYPE[^>]*<!ENTITY/i;
|
|
539
|
+
// Detect expat/sax without entity handling
|
|
540
|
+
const pyXmlPattern = /(?:etree\.parse|minidom\.parse|sax\.parse|xml\.dom\.minidom|xml\.etree\.ElementTree)\s*\(/;
|
|
541
|
+
|
|
542
|
+
for (const [path, content] of files) {
|
|
543
|
+
if (SKIP_PATH.test(path)) continue;
|
|
544
|
+
if (isJS(path)) {
|
|
545
|
+
findings.push(...scanLines(content, libxmljsUnsafe, path, this));
|
|
546
|
+
findings.push(...scanLines(content, domParserPattern, path, this));
|
|
547
|
+
findings.push(...scanLines(content, doctypePattern, path, this));
|
|
548
|
+
// xml2js: only flag if parsing user input
|
|
549
|
+
const lines = content.split('\n');
|
|
550
|
+
for (let i = 0; i < lines.length; i++) {
|
|
551
|
+
const line = lines[i];
|
|
552
|
+
if (COMMENT_LINE.test(line)) continue;
|
|
553
|
+
if (xml2jsPattern.test(line) && /(?:req\.|body|input|user|data)/.test(line)) {
|
|
554
|
+
findings.push({
|
|
555
|
+
ruleId: this.id,
|
|
556
|
+
category: this.category,
|
|
557
|
+
severity: this.severity,
|
|
558
|
+
title: this.title,
|
|
559
|
+
description: this.description,
|
|
560
|
+
confidence: this.confidence,
|
|
561
|
+
file: path,
|
|
562
|
+
line: i + 1,
|
|
563
|
+
fix: this.fix,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
} else if (isPy(path)) {
|
|
568
|
+
findings.push(...scanLines(content, pyXmlPattern, path, this));
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return findings;
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
|
|
575
|
+
// ---------------------------------------------------------------
|
|
576
|
+
// SEC-INJ-016: CSV injection
|
|
577
|
+
// ---------------------------------------------------------------
|
|
578
|
+
{
|
|
579
|
+
id: 'SEC-INJ-016',
|
|
580
|
+
category: 'security',
|
|
581
|
+
severity: 'medium',
|
|
582
|
+
confidence: 'likely',
|
|
583
|
+
title: 'CSV Injection',
|
|
584
|
+
description:
|
|
585
|
+
'Writing user-controlled data to CSV files without escaping formula characters (=, +, -, @) enables CSV injection attacks when opened in spreadsheet software.',
|
|
586
|
+
fix: { suggestion: 'Prefix cell values with a single quote or tab character. Escape =, +, -, @ at the start of cell values.' },
|
|
587
|
+
check({ files }) {
|
|
588
|
+
const findings = [];
|
|
589
|
+
const csvWritePattern = /(?:csv|createCsvWriter|createObjectCsvWriter|csvStringify|stringify|writeToPath|fast-csv|papaparse).*(?:write|push|pipe|stringify)/i;
|
|
590
|
+
const directCsv = /(?:\.csv|\.write|createWriteStream).*(?:req\.|body|input|user|data)/i;
|
|
591
|
+
const joinComma = /\.(?:join|map)\s*\([^)]*[,'"].*(?:req\.|body|input|user)/;
|
|
592
|
+
|
|
593
|
+
for (const [path, content] of files) {
|
|
594
|
+
if (SKIP_PATH.test(path)) continue;
|
|
595
|
+
if (isJS(path)) {
|
|
596
|
+
const lines = content.split('\n');
|
|
597
|
+
for (let i = 0; i < lines.length; i++) {
|
|
598
|
+
const line = lines[i];
|
|
599
|
+
if (COMMENT_LINE.test(line)) continue;
|
|
600
|
+
if (csvWritePattern.test(line) && /(?:req\.|body|input|user|data)/.test(line)) {
|
|
601
|
+
findings.push({
|
|
602
|
+
ruleId: this.id,
|
|
603
|
+
category: this.category,
|
|
604
|
+
severity: this.severity,
|
|
605
|
+
title: this.title,
|
|
606
|
+
description: this.description,
|
|
607
|
+
confidence: this.confidence,
|
|
608
|
+
file: path,
|
|
609
|
+
line: i + 1,
|
|
610
|
+
fix: this.fix,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return findings;
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
|
|
620
|
+
// ---------------------------------------------------------------
|
|
621
|
+
// SEC-INJ-017: Email header injection
|
|
622
|
+
// ---------------------------------------------------------------
|
|
623
|
+
{
|
|
624
|
+
id: 'SEC-INJ-017',
|
|
625
|
+
category: 'security',
|
|
626
|
+
severity: 'high',
|
|
627
|
+
confidence: 'likely',
|
|
628
|
+
title: 'Email Header Injection',
|
|
629
|
+
description:
|
|
630
|
+
'Passing unsanitized user input to email fields (to, cc, bcc, subject) allows attackers to inject additional headers or recipients.',
|
|
631
|
+
fix: { suggestion: 'Validate email addresses with a proper regex or library. Strip newlines and carriage returns from all email header fields.' },
|
|
632
|
+
check({ files }) {
|
|
633
|
+
const findings = [];
|
|
634
|
+
const pattern = /(?:sendMail|send|transport\.sendMail|transporter\.sendMail)\s*\(\s*\{[^}]*(?:to|cc|bcc|subject)\s*:\s*(?:req\.|params|query|body|input|user)/i;
|
|
635
|
+
const fieldAssign = /(?:to|cc|bcc|subject)\s*:\s*(?:req\.(?:body|query|params)|input|user)/;
|
|
636
|
+
|
|
637
|
+
for (const [path, content] of files) {
|
|
638
|
+
if (SKIP_PATH.test(path)) continue;
|
|
639
|
+
if (isJS(path)) {
|
|
640
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
641
|
+
const lines = content.split('\n');
|
|
642
|
+
for (let i = 0; i < lines.length; i++) {
|
|
643
|
+
const line = lines[i];
|
|
644
|
+
if (COMMENT_LINE.test(line)) continue;
|
|
645
|
+
if (fieldAssign.test(line) && /(?:mail|email|send|smtp|nodemailer)/i.test(content.slice(Math.max(0, content.indexOf(line) - 500), content.indexOf(line) + line.length))) {
|
|
646
|
+
findings.push({
|
|
647
|
+
ruleId: this.id,
|
|
648
|
+
category: this.category,
|
|
649
|
+
severity: this.severity,
|
|
650
|
+
title: this.title,
|
|
651
|
+
description: this.description,
|
|
652
|
+
confidence: this.confidence,
|
|
653
|
+
file: path,
|
|
654
|
+
line: i + 1,
|
|
655
|
+
fix: this.fix,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const seen = new Set();
|
|
662
|
+
return findings.filter((f) => {
|
|
663
|
+
const key = `${f.file}:${f.line}`;
|
|
664
|
+
if (seen.has(key)) return false;
|
|
665
|
+
seen.add(key);
|
|
666
|
+
return true;
|
|
667
|
+
});
|
|
668
|
+
},
|
|
669
|
+
},
|
|
670
|
+
|
|
671
|
+
// ---------------------------------------------------------------
|
|
672
|
+
// SEC-INJ-018: Unsafe deserialization
|
|
673
|
+
// ---------------------------------------------------------------
|
|
674
|
+
{
|
|
675
|
+
id: 'SEC-INJ-018',
|
|
676
|
+
category: 'security',
|
|
677
|
+
severity: 'critical',
|
|
678
|
+
confidence: 'definite',
|
|
679
|
+
title: 'Unsafe Deserialization',
|
|
680
|
+
description:
|
|
681
|
+
'Using node-serialize unserialize, js-yaml.load (not safeLoad), or JSON.parse on untrusted external input without try/catch can lead to code execution or crashes.',
|
|
682
|
+
fix: { suggestion: 'Use js-yaml.safeLoad (or yaml.load with DEFAULT_SAFE_SCHEMA). Wrap JSON.parse in try/catch. Avoid node-serialize entirely.' },
|
|
683
|
+
check({ files }) {
|
|
684
|
+
const findings = [];
|
|
685
|
+
const nodeSerialize = /(?:serialize\.unserialize|unserialize)\s*\(/;
|
|
686
|
+
const yamlUnsafe = /yaml\.load\s*\([^)]*(?!.*(?:safe|SAFE|DEFAULT_SAFE))/;
|
|
687
|
+
const yamlLoad = /(?:js-yaml|yaml).*\.load\s*\(/;
|
|
688
|
+
const pyPickle = /(?:pickle\.loads?|cPickle\.loads?|shelve\.open|marshal\.loads?)\s*\(/;
|
|
689
|
+
|
|
690
|
+
for (const [path, content] of files) {
|
|
691
|
+
if (SKIP_PATH.test(path)) continue;
|
|
692
|
+
if (isJS(path)) {
|
|
693
|
+
findings.push(...scanLines(content, nodeSerialize, path, this));
|
|
694
|
+
// Detect yaml.load that isn't safeLoad
|
|
695
|
+
const lines = content.split('\n');
|
|
696
|
+
for (let i = 0; i < lines.length; i++) {
|
|
697
|
+
const line = lines[i];
|
|
698
|
+
if (COMMENT_LINE.test(line)) continue;
|
|
699
|
+
if (/yaml\.load\s*\(/.test(line) && !/safeLoad|SAFE_SCHEMA|safe_load/.test(line)) {
|
|
700
|
+
// Check if the file imports js-yaml
|
|
701
|
+
if (/require\s*\(\s*['"]js-yaml['"]\s*\)|from\s+['"]js-yaml['"]/.test(content)) {
|
|
702
|
+
findings.push({
|
|
703
|
+
ruleId: this.id,
|
|
704
|
+
category: this.category,
|
|
705
|
+
severity: this.severity,
|
|
706
|
+
title: this.title,
|
|
707
|
+
description: this.description,
|
|
708
|
+
confidence: this.confidence,
|
|
709
|
+
file: path,
|
|
710
|
+
line: i + 1,
|
|
711
|
+
fix: this.fix,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
} else if (isPy(path)) {
|
|
717
|
+
findings.push(...scanLines(content, pyPickle, path, this));
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return findings;
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
|
|
724
|
+
// ---------------------------------------------------------------
|
|
725
|
+
// SEC-INJ-019: Prototype pollution
|
|
726
|
+
// ---------------------------------------------------------------
|
|
727
|
+
{
|
|
728
|
+
id: 'SEC-INJ-019',
|
|
729
|
+
category: 'security',
|
|
730
|
+
severity: 'critical',
|
|
731
|
+
confidence: 'likely',
|
|
732
|
+
title: 'Prototype Pollution',
|
|
733
|
+
description:
|
|
734
|
+
'Using Object.assign, lodash.merge, or deep merge utilities with user input (req.body) allows attackers to inject __proto__ or constructor properties.',
|
|
735
|
+
fix: { suggestion: 'Use Object.create(null) as target, or use a safe merge library. Validate input keys and reject __proto__, constructor, and prototype.' },
|
|
736
|
+
check({ files }) {
|
|
737
|
+
const findings = [];
|
|
738
|
+
const objectAssign = /Object\.assign\s*\(\s*(?:\{[^}]*\}|[^,]+),\s*(?:req\.body|req\.query|req\.params|input|user)/;
|
|
739
|
+
const lodashMerge = /(?:_\.merge|_\.defaultsDeep|lodash\.merge|merge|deepmerge|deepExtend)\s*\(\s*[^,]+,\s*(?:req\.body|req\.query|req\.params|input|user)/;
|
|
740
|
+
const spreadFromBody = /\{\s*\.\.\.(?:req\.body|req\.query|req\.params)\s*\}/;
|
|
741
|
+
|
|
742
|
+
for (const [path, content] of files) {
|
|
743
|
+
if (SKIP_PATH.test(path)) continue;
|
|
744
|
+
if (isJS(path)) {
|
|
745
|
+
findings.push(...scanLines(content, objectAssign, path, this));
|
|
746
|
+
findings.push(...scanLines(content, lodashMerge, path, this));
|
|
747
|
+
findings.push(...scanLines(content, spreadFromBody, path, this));
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return findings;
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
|
|
754
|
+
// ---------------------------------------------------------------
|
|
755
|
+
// SEC-INJ-020: Path traversal
|
|
756
|
+
// ---------------------------------------------------------------
|
|
757
|
+
{
|
|
758
|
+
id: 'SEC-INJ-020',
|
|
759
|
+
category: 'security',
|
|
760
|
+
severity: 'critical',
|
|
761
|
+
confidence: 'likely',
|
|
762
|
+
title: 'Path Traversal',
|
|
763
|
+
description:
|
|
764
|
+
'Using req.params, req.query, or user input directly in filesystem operations (fs.readFile, fs.writeFile, etc.) without sanitization allows directory traversal attacks.',
|
|
765
|
+
fix: { suggestion: 'Use path.resolve and verify the resolved path starts with the intended base directory. Never use user input directly in file paths.' },
|
|
766
|
+
check({ files }) {
|
|
767
|
+
const findings = [];
|
|
768
|
+
const fsOps = /(?:fs\.(?:readFile|writeFile|readFileSync|writeFileSync|createReadStream|createWriteStream|unlink|unlinkSync|access|accessSync|stat|statSync|open|openSync|rename|renameSync|copyFile|copyFileSync|mkdir|mkdirSync|rmdir|rmdirSync|readdir|readdirSync))\s*\(\s*(?:`[^`]*\$\{[^}]*(?:req\.|params|query|body|input|user)|[^)]*(?:req\.(?:params|query|body))|[^)]*\+\s*(?:req\.(?:params|query|body)))/;
|
|
769
|
+
const pathJoinUnsafe = /path\.(?:join|resolve)\s*\([^)]*(?:req\.(?:params|query|body)|input|user)/;
|
|
770
|
+
const templatePath = /(?:fs\.\w+)\s*\(\s*`[^`]*\$\{.*(?:req\.|params|query|body)/;
|
|
771
|
+
const pyPath = /(?:open|os\.path\.join|shutil(?:\.\w+)?)\s*\(\s*(?:request\.|input|user)/;
|
|
772
|
+
|
|
773
|
+
for (const [path, content] of files) {
|
|
774
|
+
if (SKIP_PATH.test(path)) continue;
|
|
775
|
+
if (isJS(path)) {
|
|
776
|
+
findings.push(...scanLines(content, fsOps, path, this));
|
|
777
|
+
findings.push(...scanLines(content, pathJoinUnsafe, path, this));
|
|
778
|
+
findings.push(...scanLines(content, templatePath, path, this));
|
|
779
|
+
} else if (isPy(path)) {
|
|
780
|
+
findings.push(...scanLines(content, pyPath, path, this));
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
// Deduplicate by file+line
|
|
784
|
+
const seen = new Set();
|
|
785
|
+
return findings.filter((f) => {
|
|
786
|
+
const key = `${f.file}:${f.line}`;
|
|
787
|
+
if (seen.has(key)) return false;
|
|
788
|
+
seen.add(key);
|
|
789
|
+
return true;
|
|
790
|
+
});
|
|
791
|
+
},
|
|
792
|
+
},
|
|
793
|
+
|
|
794
|
+
// ---------------------------------------------------------------
|
|
795
|
+
// SEC-INJ-021: SSRF via user-controlled URL in fetch/axios
|
|
796
|
+
// ---------------------------------------------------------------
|
|
797
|
+
{
|
|
798
|
+
id: 'SEC-INJ-021',
|
|
799
|
+
category: 'security',
|
|
800
|
+
severity: 'critical',
|
|
801
|
+
confidence: 'likely',
|
|
802
|
+
title: 'SSRF: fetch/axios called with user-controlled URL',
|
|
803
|
+
description: 'Making HTTP requests to URLs derived from user input enables Server-Side Request Forgery (SSRF), allowing attackers to probe internal services, cloud metadata endpoints, or exfiltrate data.',
|
|
804
|
+
fix: { suggestion: 'Validate and allowlist target URLs. Reject requests to private IP ranges and metadata endpoints.' },
|
|
805
|
+
check({ files }) {
|
|
806
|
+
const findings = [];
|
|
807
|
+
const pattern = /(?:fetch|axios(?:\.get|\.post|\.put|\.delete|\.request)?)\s*\(\s*(?:req\.|request\.|params\.|query\.|body\.|input|user|url\s*\+|`[^`]*\$\{[^}]*(?:req|url|host|domain))/;
|
|
808
|
+
for (const [path, content] of files) {
|
|
809
|
+
if (SKIP_PATH.test(path)) continue;
|
|
810
|
+
if (isJS(path)) findings.push(...scanLines(content, pattern, path, this));
|
|
811
|
+
}
|
|
812
|
+
return findings;
|
|
813
|
+
},
|
|
814
|
+
},
|
|
815
|
+
|
|
816
|
+
// ---------------------------------------------------------------
|
|
817
|
+
// SEC-INJ-022: SSRF via http.request with user-controlled host
|
|
818
|
+
// ---------------------------------------------------------------
|
|
819
|
+
{
|
|
820
|
+
id: 'SEC-INJ-022',
|
|
821
|
+
category: 'security',
|
|
822
|
+
severity: 'critical',
|
|
823
|
+
confidence: 'likely',
|
|
824
|
+
title: 'SSRF: http.request with user-controlled host option',
|
|
825
|
+
description: 'Node.js http.request/https.request called with options derived from user input allows SSRF attacks targeting internal services.',
|
|
826
|
+
fix: { suggestion: 'Validate hostname against an allowlist before making outbound requests.' },
|
|
827
|
+
check({ files }) {
|
|
828
|
+
const findings = [];
|
|
829
|
+
const pattern = /https?\.request\s*\(\s*\{[^}]*host\s*:\s*(?:req\.|params\.|query\.|body\.|input|user)/;
|
|
830
|
+
for (const [path, content] of files) {
|
|
831
|
+
if (SKIP_PATH.test(path)) continue;
|
|
832
|
+
if (isJS(path)) findings.push(...scanLines(content, pattern, path, this));
|
|
833
|
+
}
|
|
834
|
+
return findings;
|
|
835
|
+
},
|
|
836
|
+
},
|
|
837
|
+
|
|
838
|
+
// ---------------------------------------------------------------
|
|
839
|
+
// SEC-INJ-023: ReDoS — catastrophic nested quantifiers
|
|
840
|
+
// ---------------------------------------------------------------
|
|
841
|
+
{
|
|
842
|
+
id: 'SEC-INJ-023',
|
|
843
|
+
category: 'security',
|
|
844
|
+
severity: 'high',
|
|
845
|
+
confidence: 'likely',
|
|
846
|
+
title: 'ReDoS: Regex with nested quantifiers susceptible to catastrophic backtracking',
|
|
847
|
+
description: 'Regex patterns with nested quantifiers (e.g., (a+)+ or (a|aa)+) can take exponential time on crafted input, causing Denial of Service.',
|
|
848
|
+
fix: { suggestion: 'Rewrite the regex to avoid ambiguous repetition. Use atomic groups or possessive quantifiers, or use a safe-regex library.' },
|
|
849
|
+
check({ files }) {
|
|
850
|
+
const findings = [];
|
|
851
|
+
// Detect patterns like (X+)+ or (X|Y)+ or (.+)+ that cause backtracking
|
|
852
|
+
const nestedQuantifier = /\/[^/]*(?:\([^)]*[+*][^)]*\)[+*?]|\([^)]*\|[^)]*\)[+*]\+)[^/]*/;
|
|
853
|
+
for (const [path, content] of files) {
|
|
854
|
+
if (SKIP_PATH.test(path)) continue;
|
|
855
|
+
if (isJS(path)) {
|
|
856
|
+
const lines = content.split('\n');
|
|
857
|
+
for (let i = 0; i < lines.length; i++) {
|
|
858
|
+
if (COMMENT_LINE.test(lines[i])) continue;
|
|
859
|
+
if (nestedQuantifier.test(lines[i]) && /\.test\s*\(|\.match\s*\(|\.exec\s*\(/.test(lines[i])) {
|
|
860
|
+
findings.push({ ruleId: this.id, category: this.category, severity: this.severity, title: this.title, description: this.description, file: path, line: i + 1, fix: null });
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return findings;
|
|
866
|
+
},
|
|
867
|
+
},
|
|
868
|
+
|
|
869
|
+
// ---------------------------------------------------------------
|
|
870
|
+
// SEC-INJ-024: GraphQL injection — user input in query string
|
|
871
|
+
// ---------------------------------------------------------------
|
|
872
|
+
{
|
|
873
|
+
id: 'SEC-INJ-024',
|
|
874
|
+
category: 'security',
|
|
875
|
+
severity: 'high',
|
|
876
|
+
confidence: 'likely',
|
|
877
|
+
title: 'GraphQL Injection: user input interpolated into query string',
|
|
878
|
+
description: 'Interpolating user-controlled values into GraphQL query strings bypasses type safety and allows query manipulation or introspection abuse.',
|
|
879
|
+
fix: { suggestion: 'Use GraphQL variables instead of string interpolation: pass { query: QUERY_STRING, variables: { id: userInput } }.' },
|
|
880
|
+
check({ files }) {
|
|
881
|
+
const findings = [];
|
|
882
|
+
const pattern = /(?:gql|graphql|client\.query|client\.mutate|fetch.*graphql)\s*[(`{][^`)]*\$\{[^}]*(?:req\.|params|query|body|input|user)/i;
|
|
883
|
+
for (const [path, content] of files) {
|
|
884
|
+
if (SKIP_PATH.test(path)) continue;
|
|
885
|
+
if (isJS(path)) findings.push(...scanLines(content, pattern, path, this));
|
|
886
|
+
}
|
|
887
|
+
return findings;
|
|
888
|
+
},
|
|
889
|
+
},
|
|
890
|
+
|
|
891
|
+
// ---------------------------------------------------------------
|
|
892
|
+
// SEC-INJ-025: XML/XXE injection — user input in XML parsing
|
|
893
|
+
// ---------------------------------------------------------------
|
|
894
|
+
{
|
|
895
|
+
id: 'SEC-INJ-025',
|
|
896
|
+
category: 'security',
|
|
897
|
+
severity: 'high',
|
|
898
|
+
confidence: 'likely',
|
|
899
|
+
title: 'XXE Injection: XML parsed from user-controlled input without disabling external entities',
|
|
900
|
+
description: 'XML parsers that process external entities can be exploited to read local files, perform SSRF, or cause DoS (billion laughs attack).',
|
|
901
|
+
fix: { suggestion: 'Disable external entity processing: set resolveExternalEntities: false or use a safe parser like fast-xml-parser with external entity resolution disabled.' },
|
|
902
|
+
check({ files }) {
|
|
903
|
+
const findings = [];
|
|
904
|
+
const parsePattern = /(?:parseXML|parseString|xml2js|DOMParser|libxmljs|sax\.createSimpleStream|xmldom)\s*[\.(]/;
|
|
905
|
+
const userInputPattern = /req\.|body\.|params\.|query\.|input\b/;
|
|
906
|
+
for (const [path, content] of files) {
|
|
907
|
+
if (SKIP_PATH.test(path)) continue;
|
|
908
|
+
if (!isJS(path)) continue;
|
|
909
|
+
const lines = content.split('\n');
|
|
910
|
+
for (let i = 0; i < lines.length; i++) {
|
|
911
|
+
if (COMMENT_LINE.test(lines[i])) continue;
|
|
912
|
+
if (parsePattern.test(lines[i])) {
|
|
913
|
+
const ctx = lines.slice(Math.max(0, i - 3), Math.min(lines.length, i + 3)).join('\n');
|
|
914
|
+
if (userInputPattern.test(ctx) && !/resolveExternalEntities.*false|explicitCharkey|DOCTYPE.*SYSTEM.*false/i.test(ctx)) {
|
|
915
|
+
findings.push({ ruleId: this.id, category: this.category, severity: this.severity, title: this.title, description: this.description, file: path, line: i + 1, fix: null });
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return findings;
|
|
921
|
+
},
|
|
922
|
+
},
|
|
923
|
+
|
|
924
|
+
// ---------------------------------------------------------------
|
|
925
|
+
// SEC-INJ-026: Server-side template injection (EJS)
|
|
926
|
+
// ---------------------------------------------------------------
|
|
927
|
+
{
|
|
928
|
+
id: 'SEC-INJ-026',
|
|
929
|
+
category: 'security',
|
|
930
|
+
severity: 'critical',
|
|
931
|
+
confidence: 'likely',
|
|
932
|
+
title: 'SSTI: EJS template rendered with user-controlled template string',
|
|
933
|
+
description: 'Passing user input as the template string to ejs.render() allows arbitrary JavaScript execution on the server.',
|
|
934
|
+
fix: { suggestion: 'Never pass user input as the template string. Use ejs.render(STATIC_TEMPLATE, userDataAsVariables) instead.' },
|
|
935
|
+
check({ files }) {
|
|
936
|
+
const findings = [];
|
|
937
|
+
const pattern = /ejs\.render\s*\(\s*(?:req\.|params\.|query\.|body\.|input|user|`[^`]*\$\{)/;
|
|
938
|
+
for (const [path, content] of files) {
|
|
939
|
+
if (SKIP_PATH.test(path)) continue;
|
|
940
|
+
if (isJS(path)) findings.push(...scanLines(content, pattern, path, this));
|
|
941
|
+
}
|
|
942
|
+
return findings;
|
|
943
|
+
},
|
|
944
|
+
},
|
|
945
|
+
|
|
946
|
+
// ---------------------------------------------------------------
|
|
947
|
+
// SEC-INJ-027: Server-side template injection (Pug/Jade)
|
|
948
|
+
// ---------------------------------------------------------------
|
|
949
|
+
{
|
|
950
|
+
id: 'SEC-INJ-027',
|
|
951
|
+
category: 'security',
|
|
952
|
+
severity: 'critical',
|
|
953
|
+
confidence: 'likely',
|
|
954
|
+
title: 'SSTI: Pug template compiled from user-controlled input',
|
|
955
|
+
description: 'pug.compile() or pug.render() with user-supplied template strings allows arbitrary code execution.',
|
|
956
|
+
fix: { suggestion: 'Always compile from static template strings. Pass user data only as local variables to the render function.' },
|
|
957
|
+
check({ files }) {
|
|
958
|
+
const findings = [];
|
|
959
|
+
const pattern = /pug\.(?:compile|render|renderFile)\s*\(\s*(?:req\.|params\.|query\.|body\.|input|user)/;
|
|
960
|
+
for (const [path, content] of files) {
|
|
961
|
+
if (SKIP_PATH.test(path)) continue;
|
|
962
|
+
if (isJS(path)) findings.push(...scanLines(content, pattern, path, this));
|
|
963
|
+
}
|
|
964
|
+
return findings;
|
|
965
|
+
},
|
|
966
|
+
},
|
|
967
|
+
|
|
968
|
+
// ---------------------------------------------------------------
|
|
969
|
+
// SEC-INJ-028: Server-side template injection (Handlebars)
|
|
970
|
+
// ---------------------------------------------------------------
|
|
971
|
+
{
|
|
972
|
+
id: 'SEC-INJ-028',
|
|
973
|
+
category: 'security',
|
|
974
|
+
severity: 'critical',
|
|
975
|
+
confidence: 'likely',
|
|
976
|
+
title: 'SSTI: Handlebars template compiled from user-controlled input',
|
|
977
|
+
description: 'Handlebars.compile() called with user input allows template injection leading to remote code execution.',
|
|
978
|
+
fix: { suggestion: 'Compile Handlebars templates from static strings at startup. Use the compiled function with user data as context only.' },
|
|
979
|
+
check({ files }) {
|
|
980
|
+
const findings = [];
|
|
981
|
+
const pattern = /Handlebars\.compile\s*\(\s*(?:req\.|params\.|query\.|body\.|input|user)/;
|
|
982
|
+
for (const [path, content] of files) {
|
|
983
|
+
if (SKIP_PATH.test(path)) continue;
|
|
984
|
+
if (isJS(path)) findings.push(...scanLines(content, pattern, path, this));
|
|
985
|
+
}
|
|
986
|
+
return findings;
|
|
987
|
+
},
|
|
988
|
+
},
|
|
989
|
+
|
|
990
|
+
// ---------------------------------------------------------------
|
|
991
|
+
// SEC-INJ-029: HTTP parameter pollution
|
|
992
|
+
// ---------------------------------------------------------------
|
|
993
|
+
{
|
|
994
|
+
id: 'SEC-INJ-029',
|
|
995
|
+
category: 'security',
|
|
996
|
+
severity: 'medium',
|
|
997
|
+
confidence: 'likely',
|
|
998
|
+
title: 'HTTP Parameter Pollution: no HPP middleware configured',
|
|
999
|
+
description: 'Without hpp middleware, submitting duplicate query parameters can overwrite expected values, bypassing validation or causing unexpected behavior.',
|
|
1000
|
+
fix: { suggestion: 'Use the "hpp" npm package: app.use(hpp()). It deduplicates query parameters and keeps only the last value.' },
|
|
1001
|
+
check({ files }) {
|
|
1002
|
+
const findings = [];
|
|
1003
|
+
const hasExpress = [...files.values()].some(c => /require\s*\(\s*['"]express['"]\)|from\s+['"]express['"]/i.test(c));
|
|
1004
|
+
const hasHPP = [...files.values()].some(c => /require\s*\(\s*['"]hpp['"]\)|from\s+['"]hpp['"]/i.test(c));
|
|
1005
|
+
// helmet() provides comprehensive security headers; treat apps using
|
|
1006
|
+
// helmet as having addressed parameter pollution at the framework level.
|
|
1007
|
+
const hasHelmet = [...files.values()].some(c => /require\s*\(\s*['"]helmet['"]\)|from\s+['"]helmet['"]/i.test(c));
|
|
1008
|
+
// Require an Express app with actual route definitions — not just an
|
|
1009
|
+
// import — so utility modules and minimal setup files are not flagged.
|
|
1010
|
+
const hasRoutes = [...files.values()].some(c => /\.(?:get|post|put|patch|delete)\s*\(\s*['"]\//.test(c));
|
|
1011
|
+
if (hasExpress && hasRoutes && !hasHPP && !hasHelmet) {
|
|
1012
|
+
findings.push({ ruleId: this.id, category: this.category, severity: this.severity, title: this.title, description: this.description, fix: null });
|
|
1013
|
+
}
|
|
1014
|
+
return findings;
|
|
1015
|
+
},
|
|
1016
|
+
},
|
|
1017
|
+
|
|
1018
|
+
// ---------------------------------------------------------------
|
|
1019
|
+
// SEC-INJ-030: Mass assignment vulnerability
|
|
1020
|
+
// ---------------------------------------------------------------
|
|
1021
|
+
{
|
|
1022
|
+
id: 'SEC-INJ-030',
|
|
1023
|
+
category: 'security',
|
|
1024
|
+
severity: 'high',
|
|
1025
|
+
confidence: 'likely',
|
|
1026
|
+
title: 'Mass Assignment: Object.assign or spread with req.body onto model',
|
|
1027
|
+
description: 'Directly assigning req.body to a model object allows attackers to set unexpected fields (e.g., isAdmin, role, price), leading to privilege escalation.',
|
|
1028
|
+
fix: { suggestion: 'Use an allowlist: const safe = { name: req.body.name, email: req.body.email }. Never spread or Object.assign(model, req.body) directly.' },
|
|
1029
|
+
check({ files }) {
|
|
1030
|
+
const findings = [];
|
|
1031
|
+
const spreadPattern = /\{\s*\.\.\.\s*req\.body|\.\.\.\s*req\.body\s*[,}]/;
|
|
1032
|
+
const assignPattern = /Object\.assign\s*\(\s*\w+\s*,\s*req\.body/;
|
|
1033
|
+
for (const [path, content] of files) {
|
|
1034
|
+
if (SKIP_PATH.test(path)) continue;
|
|
1035
|
+
if (isJS(path)) {
|
|
1036
|
+
findings.push(...scanLines(content, spreadPattern, path, this));
|
|
1037
|
+
findings.push(...scanLines(content, assignPattern, path, this));
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return findings;
|
|
1041
|
+
},
|
|
1042
|
+
},
|
|
1043
|
+
|
|
1044
|
+
// ---------------------------------------------------------------
|
|
1045
|
+
// SEC-INJ-031: Insecure random for tokens/sessions
|
|
1046
|
+
// ---------------------------------------------------------------
|
|
1047
|
+
{
|
|
1048
|
+
id: 'SEC-INJ-031',
|
|
1049
|
+
category: 'security',
|
|
1050
|
+
severity: 'high',
|
|
1051
|
+
confidence: 'likely',
|
|
1052
|
+
title: 'Insecure Random: Math.random() used for security tokens',
|
|
1053
|
+
description: 'Math.random() is not cryptographically secure and should never be used for session IDs, CSRF tokens, API keys, or any security-sensitive values.',
|
|
1054
|
+
fix: { suggestion: 'Use crypto.randomBytes(32).toString("hex") or crypto.randomUUID() for security tokens.' },
|
|
1055
|
+
check({ files }) {
|
|
1056
|
+
const findings = [];
|
|
1057
|
+
const pattern = /Math\.random\s*\(\s*\)/;
|
|
1058
|
+
const tokenContext = /token|session|secret|password|key|nonce|csrf|auth|id\s*=/i;
|
|
1059
|
+
for (const [path, content] of files) {
|
|
1060
|
+
if (SKIP_PATH.test(path)) continue;
|
|
1061
|
+
if (!isJS(path)) continue;
|
|
1062
|
+
const lines = content.split('\n');
|
|
1063
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1064
|
+
if (COMMENT_LINE.test(lines[i])) continue;
|
|
1065
|
+
if (pattern.test(lines[i])) {
|
|
1066
|
+
const ctx = lines.slice(Math.max(0, i - 2), Math.min(lines.length, i + 3)).join('\n');
|
|
1067
|
+
if (tokenContext.test(ctx)) {
|
|
1068
|
+
findings.push({ ruleId: this.id, category: this.category, severity: this.severity, title: this.title, description: this.description, file: path, line: i + 1, fix: null });
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
return findings;
|
|
1074
|
+
},
|
|
1075
|
+
},
|
|
1076
|
+
|
|
1077
|
+
// ---------------------------------------------------------------
|
|
1078
|
+
// SEC-INJ-032: Timing attack in string comparison
|
|
1079
|
+
// ---------------------------------------------------------------
|
|
1080
|
+
{
|
|
1081
|
+
id: 'SEC-INJ-032',
|
|
1082
|
+
category: 'security',
|
|
1083
|
+
severity: 'high',
|
|
1084
|
+
confidence: 'likely',
|
|
1085
|
+
title: 'Timing Attack: non-constant-time string comparison for secrets',
|
|
1086
|
+
description: 'Using === or == to compare tokens, API keys, or HMAC signatures leaks secret length via timing differences. Use crypto.timingSafeEqual().',
|
|
1087
|
+
fix: { suggestion: 'Use crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b)) for comparing any secret or token values.' },
|
|
1088
|
+
check({ files }) {
|
|
1089
|
+
const findings = [];
|
|
1090
|
+
const secretContext = /token|hmac|signature|secret|apiKey|api_key|hash/i;
|
|
1091
|
+
const eqPattern = /(?:===|==|!==|!=)\s*(?:req\.|token|secret|hash|sig)|(?:req\.|token|secret|hash|sig)[^=]*(?:===|==)/;
|
|
1092
|
+
for (const [path, content] of files) {
|
|
1093
|
+
if (SKIP_PATH.test(path)) continue;
|
|
1094
|
+
if (!isJS(path)) continue;
|
|
1095
|
+
const lines = content.split('\n');
|
|
1096
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1097
|
+
if (COMMENT_LINE.test(lines[i])) continue;
|
|
1098
|
+
if (secretContext.test(lines[i]) && eqPattern.test(lines[i]) && !/timingSafeEqual/.test(lines[i])) {
|
|
1099
|
+
findings.push({ ruleId: this.id, category: this.category, severity: this.severity, title: this.title, description: this.description, file: path, line: i + 1, fix: null });
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
return findings;
|
|
1104
|
+
},
|
|
1105
|
+
},
|
|
1106
|
+
|
|
1107
|
+
// ---------------------------------------------------------------
|
|
1108
|
+
// SEC-INJ-033: Missing HTTPS enforcement
|
|
1109
|
+
// ---------------------------------------------------------------
|
|
1110
|
+
{
|
|
1111
|
+
id: 'SEC-INJ-033',
|
|
1112
|
+
category: 'security',
|
|
1113
|
+
severity: 'high',
|
|
1114
|
+
confidence: 'suggestion',
|
|
1115
|
+
title: 'Missing HTTPS enforcement: no HTTP-to-HTTPS redirect middleware',
|
|
1116
|
+
description: 'Express app without enforce-https or similar middleware allows plain HTTP connections, exposing credentials and session tokens to eavesdropping.',
|
|
1117
|
+
fix: { suggestion: 'Use the enforce-https or express-sslify package, or add middleware: if (req.headers["x-forwarded-proto"] !== "https") return res.redirect("https://...").' },
|
|
1118
|
+
check({ files }) {
|
|
1119
|
+
const findings = [];
|
|
1120
|
+
const hasExpressServer = [...files.values()].some(c => /app\.listen\s*\(|createServer/.test(c) && /require.*express|from.*express/.test(c));
|
|
1121
|
+
const hasHttpsEnforce = [...files.values()].some(c => /enforce-https|express-sslify|x-forwarded-proto.*https|requireHTTPS|forceSSL/i.test(c));
|
|
1122
|
+
if (hasExpressServer && !hasHttpsEnforce) {
|
|
1123
|
+
findings.push({ ruleId: this.id, category: this.category, severity: this.severity, title: this.title, description: this.description, fix: null });
|
|
1124
|
+
}
|
|
1125
|
+
return findings;
|
|
1126
|
+
},
|
|
1127
|
+
},
|
|
1128
|
+
|
|
1129
|
+
// ---------------------------------------------------------------
|
|
1130
|
+
// SEC-INJ-034: Clickjacking — missing X-Frame-Options
|
|
1131
|
+
// ---------------------------------------------------------------
|
|
1132
|
+
{
|
|
1133
|
+
id: 'SEC-INJ-034',
|
|
1134
|
+
category: 'security',
|
|
1135
|
+
severity: 'medium',
|
|
1136
|
+
confidence: 'suggestion',
|
|
1137
|
+
title: 'Clickjacking: no X-Frame-Options or CSP frame-ancestors header set',
|
|
1138
|
+
description: 'Without X-Frame-Options or Content-Security-Policy frame-ancestors, the application can be embedded in an iframe, enabling clickjacking attacks.',
|
|
1139
|
+
fix: { suggestion: 'Use helmet.js with frameguard option: app.use(helmet.frameguard({ action: "deny" })).' },
|
|
1140
|
+
check({ files }) {
|
|
1141
|
+
const findings = [];
|
|
1142
|
+
// Only fire on the main app file — one that creates an express() instance and calls app.listen
|
|
1143
|
+
const hasExpressApp = [...files.values()].some(c =>
|
|
1144
|
+
/(?:require\s*\(\s*['"]express['"]\s*\)|from\s+['"]express['"])/.test(c) &&
|
|
1145
|
+
/(?:app\.listen\s*\(|express\s*\(\s*\))/.test(c)
|
|
1146
|
+
);
|
|
1147
|
+
const hasFrameProtection = [...files.values()].some(c => /X-Frame-Options|frameguard|frame-ancestors|helmet\s*\(\s*\)|helmet\(\)/i.test(c));
|
|
1148
|
+
if (hasExpressApp && !hasFrameProtection) {
|
|
1149
|
+
findings.push({ ruleId: this.id, category: this.category, severity: this.severity, title: this.title, description: this.description, fix: null });
|
|
1150
|
+
}
|
|
1151
|
+
return findings;
|
|
1152
|
+
},
|
|
1153
|
+
},
|
|
1154
|
+
|
|
1155
|
+
// ---------------------------------------------------------------
|
|
1156
|
+
// SEC-INJ-035: Regex used as user input without validation
|
|
1157
|
+
// ---------------------------------------------------------------
|
|
1158
|
+
{
|
|
1159
|
+
id: 'SEC-INJ-035',
|
|
1160
|
+
category: 'security',
|
|
1161
|
+
severity: 'high',
|
|
1162
|
+
confidence: 'likely',
|
|
1163
|
+
title: 'User-controlled input passed to RegExp constructor',
|
|
1164
|
+
description: 'Creating a RegExp from user input without validation allows ReDoS attacks and may expose internal application behavior.',
|
|
1165
|
+
fix: { suggestion: 'Escape user input with a regex-escape library before passing to RegExp, or validate against an allowlist of patterns.' },
|
|
1166
|
+
check({ files }) {
|
|
1167
|
+
const findings = [];
|
|
1168
|
+
const pattern = /new\s+RegExp\s*\(\s*(?:req\.|params\.|query\.|body\.|input|user)/;
|
|
1169
|
+
for (const [path, content] of files) {
|
|
1170
|
+
if (SKIP_PATH.test(path)) continue;
|
|
1171
|
+
if (isJS(path)) findings.push(...scanLines(content, pattern, path, this));
|
|
1172
|
+
}
|
|
1173
|
+
return findings;
|
|
1174
|
+
},
|
|
1175
|
+
},
|
|
1176
|
+
|
|
1177
|
+
// ---------------------------------------------------------------
|
|
1178
|
+
// SEC-INJ-036: LDAP injection via string concatenation
|
|
1179
|
+
// ---------------------------------------------------------------
|
|
1180
|
+
{
|
|
1181
|
+
id: 'SEC-INJ-036',
|
|
1182
|
+
category: 'security',
|
|
1183
|
+
severity: 'critical',
|
|
1184
|
+
confidence: 'likely',
|
|
1185
|
+
title: 'LDAP Injection via unsanitized user input in LDAP query',
|
|
1186
|
+
description: 'Building LDAP filter strings with user input without escaping special characters allows LDAP injection attacks.',
|
|
1187
|
+
fix: { suggestion: 'Escape LDAP special characters in user input: replace (, ), \\, *, NUL with their escaped equivalents before including in filter strings.' },
|
|
1188
|
+
check({ files }) {
|
|
1189
|
+
const findings = [];
|
|
1190
|
+
const pattern = /(?:ldap|ldapjs|activedirectory).*(?:search|filter|bind)\s*\([^)]*(?:req\.|params|query|body|input|\+\s*\w)/i;
|
|
1191
|
+
for (const [path, content] of files) {
|
|
1192
|
+
if (SKIP_PATH.test(path)) continue;
|
|
1193
|
+
if (isJS(path)) findings.push(...scanLines(content, pattern, path, this));
|
|
1194
|
+
}
|
|
1195
|
+
return findings;
|
|
1196
|
+
},
|
|
1197
|
+
},
|
|
1198
|
+
|
|
1199
|
+
// ---------------------------------------------------------------
|
|
1200
|
+
// SEC-INJ-037: eval() with user-controlled input
|
|
1201
|
+
// ---------------------------------------------------------------
|
|
1202
|
+
{
|
|
1203
|
+
id: 'SEC-INJ-037',
|
|
1204
|
+
category: 'security',
|
|
1205
|
+
severity: 'critical',
|
|
1206
|
+
confidence: 'likely',
|
|
1207
|
+
title: 'eval() called with user-controlled input — arbitrary code execution',
|
|
1208
|
+
description: 'eval() with any user-supplied string allows remote code execution. This is one of the most dangerous patterns in JavaScript.',
|
|
1209
|
+
fix: { suggestion: 'Remove eval() entirely. Use JSON.parse() for data, or refactor to avoid dynamic evaluation.' },
|
|
1210
|
+
check({ files }) {
|
|
1211
|
+
const findings = [];
|
|
1212
|
+
const pattern = /\beval\s*\(\s*(?:req\.|params\.|query\.|body\.|input|user|`[^`]*\$\{)/;
|
|
1213
|
+
for (const [path, content] of files) {
|
|
1214
|
+
if (SKIP_PATH.test(path)) continue;
|
|
1215
|
+
if (isJS(path)) findings.push(...scanLines(content, pattern, path, this));
|
|
1216
|
+
}
|
|
1217
|
+
return findings;
|
|
1218
|
+
},
|
|
1219
|
+
},
|
|
1220
|
+
|
|
1221
|
+
// ---------------------------------------------------------------
|
|
1222
|
+
// SEC-INJ-038: Prototype pollution via merge/assign
|
|
1223
|
+
// ---------------------------------------------------------------
|
|
1224
|
+
{
|
|
1225
|
+
id: 'SEC-INJ-038',
|
|
1226
|
+
category: 'security',
|
|
1227
|
+
severity: 'high',
|
|
1228
|
+
confidence: 'likely',
|
|
1229
|
+
title: 'Prototype Pollution: deep merge with user-controlled object',
|
|
1230
|
+
description: 'Deeply merging user-supplied objects without filtering __proto__ or constructor keys can pollute Object.prototype, affecting all objects in the application.',
|
|
1231
|
+
fix: { suggestion: 'Use a safe merge library that strips __proto__, constructor, and prototype keys. Or use Object.create(null) as the merge target.' },
|
|
1232
|
+
check({ files }) {
|
|
1233
|
+
const findings = [];
|
|
1234
|
+
const pattern = /(?:merge|deepMerge|assign|extend|defaults)\s*\(\s*(?:\w+\s*,\s*)*req\.body/;
|
|
1235
|
+
for (const [path, content] of files) {
|
|
1236
|
+
if (SKIP_PATH.test(path)) continue;
|
|
1237
|
+
if (isJS(path)) findings.push(...scanLines(content, pattern, path, this));
|
|
1238
|
+
}
|
|
1239
|
+
return findings;
|
|
1240
|
+
},
|
|
1241
|
+
},
|
|
1242
|
+
|
|
1243
|
+
// ---------------------------------------------------------------
|
|
1244
|
+
// SEC-INJ-039: Open redirect via user-controlled URL
|
|
1245
|
+
// ---------------------------------------------------------------
|
|
1246
|
+
{
|
|
1247
|
+
id: 'SEC-INJ-039',
|
|
1248
|
+
category: 'security',
|
|
1249
|
+
severity: 'high',
|
|
1250
|
+
confidence: 'likely',
|
|
1251
|
+
title: 'Open Redirect: res.redirect() with user-controlled URL',
|
|
1252
|
+
description: 'Redirecting to a URL from user input without validation allows phishing attacks by crafting URLs like example.com/redirect?to=evil.com.',
|
|
1253
|
+
fix: { suggestion: 'Validate redirect URLs against an allowlist of safe destinations. Reject URLs containing external origins.' },
|
|
1254
|
+
check({ files }) {
|
|
1255
|
+
const findings = [];
|
|
1256
|
+
const pattern = /res\.redirect\s*\(\s*(?:req\.|params\.|query\.|body\.|input|url\s*\+|`[^`]*\$\{[^}]*(?:req|url|redirect))/;
|
|
1257
|
+
for (const [path, content] of files) {
|
|
1258
|
+
if (SKIP_PATH.test(path)) continue;
|
|
1259
|
+
if (isJS(path)) findings.push(...scanLines(content, pattern, path, this));
|
|
1260
|
+
}
|
|
1261
|
+
return findings;
|
|
1262
|
+
},
|
|
1263
|
+
},
|
|
1264
|
+
|
|
1265
|
+
// ---------------------------------------------------------------
|
|
1266
|
+
// SEC-INJ-040: Path traversal via directory listing
|
|
1267
|
+
// ---------------------------------------------------------------
|
|
1268
|
+
{
|
|
1269
|
+
id: 'SEC-INJ-040',
|
|
1270
|
+
category: 'security',
|
|
1271
|
+
severity: 'high',
|
|
1272
|
+
confidence: 'likely',
|
|
1273
|
+
title: 'Path Traversal: express.static serving user-controlled path',
|
|
1274
|
+
description: 'Using express.static with user-controlled directory paths allows directory traversal to read arbitrary files.',
|
|
1275
|
+
fix: { suggestion: 'Use path.resolve() and verify the resolved path starts with the expected base directory before serving.' },
|
|
1276
|
+
check({ files }) {
|
|
1277
|
+
const findings = [];
|
|
1278
|
+
const pattern = /express\.static\s*\(\s*(?:req\.|params\.|query\.|body\.|input|path\.join\s*\([^)]*(?:req\.|params\.))/;
|
|
1279
|
+
for (const [path, content] of files) {
|
|
1280
|
+
if (SKIP_PATH.test(path)) continue;
|
|
1281
|
+
if (isJS(path)) findings.push(...scanLines(content, pattern, path, this));
|
|
1282
|
+
}
|
|
1283
|
+
return findings;
|
|
1284
|
+
},
|
|
1285
|
+
},
|
|
1286
|
+
];
|
|
1287
|
+
|
|
1288
|
+
export default rules;
|
|
1289
|
+
|
|
1290
|
+
// Additional injection/security rules SEC-INJ-041 through SEC-INJ-072
|
|
1291
|
+
|
|
1292
|
+
// SEC-INJ-041: SQL injection via template literal in raw query
|
|
1293
|
+
rules.push({
|
|
1294
|
+
id: 'SEC-INJ-041', category: 'security', severity: 'critical', confidence: 'likely',
|
|
1295
|
+
title: 'SQL Injection via template literal in query string',
|
|
1296
|
+
check({ files }) {
|
|
1297
|
+
const findings = [];
|
|
1298
|
+
const p = /['"`]\s*(?:SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER)\s[^'"`]*\$\{/i;
|
|
1299
|
+
for (const [path, content] of files) {
|
|
1300
|
+
if (SKIP_PATH.test(path) || !isJS(path)) continue;
|
|
1301
|
+
const lines = content.split('\n');
|
|
1302
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1303
|
+
if (COMMENT_LINE.test(lines[i])) continue;
|
|
1304
|
+
if (p.test(lines[i])) findings.push({ ruleId: 'SEC-INJ-041', category: 'security', severity: 'critical', title: 'SQL built with template literal — injection risk', description: 'Template literals in SQL strings allow injection when they contain user input. Use parameterized queries.', file: path, line: i + 1, fix: null });
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
return findings;
|
|
1308
|
+
},
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
// SEC-INJ-042: Unvalidated redirect target in 301/302
|
|
1312
|
+
rules.push({
|
|
1313
|
+
id: 'SEC-INJ-042', category: 'security', severity: 'medium', confidence: 'likely',
|
|
1314
|
+
title: 'Unvalidated redirect with status code',
|
|
1315
|
+
check({ files }) {
|
|
1316
|
+
const findings = [];
|
|
1317
|
+
const p = /res\.(?:redirect|status\s*\(\s*30[12]\s*\)\.(?:header|redirect))\s*\([^)]*(?:req\.|params\.|query\.|body\.)/;
|
|
1318
|
+
// HTTPS enforcement redirects (redirect to https:// + req.hostname) are safe
|
|
1319
|
+
const httpsEnforce = /res\.redirect\s*\(\s*\d+\s*,\s*['"]https:\/\/['"]\s*\+\s*req\.hostname/;
|
|
1320
|
+
for (const [path, content] of files) {
|
|
1321
|
+
if (SKIP_PATH.test(path) || !isJS(path)) continue;
|
|
1322
|
+
const lines = content.split('\n');
|
|
1323
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1324
|
+
if (COMMENT_LINE.test(lines[i])) continue;
|
|
1325
|
+
if (p.test(lines[i]) && !httpsEnforce.test(lines[i])) findings.push({ ruleId: 'SEC-INJ-042', category: 'security', severity: 'medium', title: 'Unvalidated redirect — open redirect enables phishing', description: 'Validate redirect destinations against a list of allowed URLs before redirecting.', file: path, line: i + 1, fix: null });
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
return findings;
|
|
1329
|
+
},
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
// SEC-INJ-043: HTML injection via innerHTML
|
|
1333
|
+
rules.push({
|
|
1334
|
+
id: 'SEC-INJ-043', category: 'security', severity: 'high', confidence: 'likely',
|
|
1335
|
+
title: 'innerHTML set with dynamic value — XSS risk',
|
|
1336
|
+
check({ files }) {
|
|
1337
|
+
const findings = [];
|
|
1338
|
+
const p = /\.innerHTML\s*\+?=\s*(?!\s*['"])/;
|
|
1339
|
+
for (const [path, content] of files) {
|
|
1340
|
+
if (SKIP_PATH.test(path) || !isJS(path)) continue;
|
|
1341
|
+
const lines = content.split('\n');
|
|
1342
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1343
|
+
if (COMMENT_LINE.test(lines[i])) continue;
|
|
1344
|
+
if (p.test(lines[i]) && !/DOMPurify|sanitize|escape/i.test(lines[i])) findings.push({ ruleId: 'SEC-INJ-043', category: 'security', severity: 'high', title: 'innerHTML assigned dynamic value — potential XSS', description: 'Setting innerHTML with unsanitized values enables XSS. Use textContent for plain text, or DOMPurify.sanitize() for HTML content.', file: path, line: i + 1, fix: null });
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
return findings;
|
|
1348
|
+
},
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
// SEC-INJ-044: document.write with dynamic content
|
|
1352
|
+
rules.push({
|
|
1353
|
+
id: 'SEC-INJ-044', category: 'security', severity: 'high', confidence: 'likely',
|
|
1354
|
+
title: 'document.write() with dynamic content — XSS risk',
|
|
1355
|
+
check({ files }) {
|
|
1356
|
+
const findings = [];
|
|
1357
|
+
const p = /document\.write\s*\(\s*(?!['"])/;
|
|
1358
|
+
for (const [path, content] of files) {
|
|
1359
|
+
if (SKIP_PATH.test(path) || !isJS(path)) continue;
|
|
1360
|
+
const lines = content.split('\n');
|
|
1361
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1362
|
+
if (COMMENT_LINE.test(lines[i])) continue;
|
|
1363
|
+
if (p.test(lines[i])) findings.push({ ruleId: 'SEC-INJ-044', category: 'security', severity: 'high', title: 'document.write() with variable content — XSS and parse rewrite risk', description: 'document.write() with variable content can inject scripts. It also rewrites the entire document in some contexts. Use DOM APIs instead.', file: path, line: i + 1, fix: null });
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
return findings;
|
|
1367
|
+
},
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
// SEC-INJ-045: Unsafe deserialization with yaml.load
|
|
1371
|
+
rules.push({
|
|
1372
|
+
id: 'SEC-INJ-045', category: 'security', severity: 'critical', confidence: 'definite',
|
|
1373
|
+
title: 'yaml.load() with untrusted input — code execution risk',
|
|
1374
|
+
check({ files }) {
|
|
1375
|
+
const findings = [];
|
|
1376
|
+
const p = /yaml\.load\s*\(/;
|
|
1377
|
+
for (const [path, content] of files) {
|
|
1378
|
+
if (SKIP_PATH.test(path) || !isJS(path)) continue;
|
|
1379
|
+
const lines = content.split('\n');
|
|
1380
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1381
|
+
if (COMMENT_LINE.test(lines[i])) continue;
|
|
1382
|
+
if (p.test(lines[i]) && !/yaml\.safeLoad|yaml\.load.*\{.*schema/i.test(lines[i])) findings.push({ ruleId: 'SEC-INJ-045', category: 'security', severity: 'critical', title: 'yaml.load() allows code execution — use yaml.safeLoad()', description: 'js-yaml\'s yaml.load() can execute arbitrary JavaScript via !!js/function tags. Use yaml.safeLoad() or load() with { schema: FAILSAFE_SCHEMA }.', file: path, line: i + 1, fix: null });
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
return findings;
|
|
1386
|
+
},
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
// SEC-INJ-046: express-fileupload with parseNested enabled
|
|
1390
|
+
rules.push({
|
|
1391
|
+
id: 'SEC-INJ-046', category: 'security', severity: 'critical', confidence: 'definite',
|
|
1392
|
+
title: 'express-fileupload with parseNested: true — prototype pollution',
|
|
1393
|
+
check({ files }) {
|
|
1394
|
+
const findings = [];
|
|
1395
|
+
const p = /fileUpload\s*\(\s*\{[^}]*parseNested\s*:\s*true/;
|
|
1396
|
+
for (const [path, content] of files) {
|
|
1397
|
+
if (SKIP_PATH.test(path) || !isJS(path)) continue;
|
|
1398
|
+
const lines = content.split('\n');
|
|
1399
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1400
|
+
if (COMMENT_LINE.test(lines[i])) continue;
|
|
1401
|
+
if (p.test(lines[i])) findings.push({ ruleId: 'SEC-INJ-046', category: 'security', severity: 'critical', title: 'express-fileupload parseNested:true — CVE-2020-7699 prototype pollution', description: 'Setting parseNested: true in express-fileupload enables prototype pollution attacks. Remove this option or upgrade to a patched version.', file: path, line: i + 1, fix: null });
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
return findings;
|
|
1405
|
+
},
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
// SEC-INJ-047: HTTP Response Splitting
|
|
1409
|
+
rules.push({
|
|
1410
|
+
id: 'SEC-INJ-047', category: 'security', severity: 'high', confidence: 'likely',
|
|
1411
|
+
title: 'HTTP Response Splitting: user input in response header',
|
|
1412
|
+
check({ files }) {
|
|
1413
|
+
const findings = [];
|
|
1414
|
+
const p = /res\.(?:set|header|setHeader)\s*\([^,]+,\s*(?:req\.|params\.|query\.|body\.|input|user)/;
|
|
1415
|
+
for (const [path, content] of files) {
|
|
1416
|
+
if (SKIP_PATH.test(path) || !isJS(path)) continue;
|
|
1417
|
+
const lines = content.split('\n');
|
|
1418
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1419
|
+
if (COMMENT_LINE.test(lines[i])) continue;
|
|
1420
|
+
if (p.test(lines[i])) findings.push({ ruleId: 'SEC-INJ-047', category: 'security', severity: 'high', title: 'User input in HTTP response header — CRLF injection / response splitting', description: 'User input in response headers enables HTTP response splitting if the input contains CRLF characters. Validate and strip newlines from header values.', file: path, line: i + 1, fix: null });
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
return findings;
|
|
1424
|
+
},
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
// SEC-INJ-048: Unsafe shell execution with variables
|
|
1428
|
+
rules.push({
|
|
1429
|
+
id: 'SEC-INJ-048', category: 'security', severity: 'critical', confidence: 'likely',
|
|
1430
|
+
title: 'Shell command constructed with variable interpolation',
|
|
1431
|
+
check({ files }) {
|
|
1432
|
+
const findings = [];
|
|
1433
|
+
const p = /(?:execSync|exec|system|popen)\s*\(\s*`[^`]*\$\{/;
|
|
1434
|
+
for (const [path, content] of files) {
|
|
1435
|
+
if (SKIP_PATH.test(path) || !isJS(path)) continue;
|
|
1436
|
+
const lines = content.split('\n');
|
|
1437
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1438
|
+
if (COMMENT_LINE.test(lines[i])) continue;
|
|
1439
|
+
if (p.test(lines[i])) findings.push({ ruleId: 'SEC-INJ-048', category: 'security', severity: 'critical', title: 'Shell command with template literal — command injection risk', description: 'Interpolating variables into shell command strings enables command injection. Use execFile with argument arrays instead.', file: path, line: i + 1, fix: null });
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
return findings;
|
|
1443
|
+
},
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
// SEC-INJ-049: JSON.parse without try/catch on user input
|
|
1447
|
+
rules.push({
|
|
1448
|
+
id: 'SEC-INJ-049', category: 'security', severity: 'medium', confidence: 'likely',
|
|
1449
|
+
title: 'JSON.parse on user input without error handling — DoS risk',
|
|
1450
|
+
check({ files }) {
|
|
1451
|
+
const findings = [];
|
|
1452
|
+
const p = /JSON\.parse\s*\(\s*(?:req\.|body\.|params\.|query\.|input|user)/;
|
|
1453
|
+
for (const [path, content] of files) {
|
|
1454
|
+
if (SKIP_PATH.test(path) || !isJS(path)) continue;
|
|
1455
|
+
const lines = content.split('\n');
|
|
1456
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1457
|
+
if (COMMENT_LINE.test(lines[i])) continue;
|
|
1458
|
+
if (p.test(lines[i])) {
|
|
1459
|
+
const ctx = lines.slice(Math.max(0, i - 3), i).join('\n');
|
|
1460
|
+
if (!/try\s*\{/.test(ctx)) findings.push({ ruleId: 'SEC-INJ-049', category: 'security', severity: 'medium', title: 'JSON.parse on user input without try/catch — malformed input throws and may crash', description: 'JSON.parse throws on invalid input. Wrap in try/catch to prevent uncaught exceptions from crashing the server.', file: path, line: i + 1, fix: null });
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
return findings;
|
|
1465
|
+
},
|
|
1466
|
+
});
|
|
1467
|
+
|
|
1468
|
+
// SEC-INJ-050: Arbitrary file read via path parameter
|
|
1469
|
+
rules.push({
|
|
1470
|
+
id: 'SEC-INJ-050', category: 'security', severity: 'critical', confidence: 'likely',
|
|
1471
|
+
title: 'File read using user-controlled filename — path traversal',
|
|
1472
|
+
check({ files }) {
|
|
1473
|
+
const findings = [];
|
|
1474
|
+
const p = /fs\.(?:readFile|readFileSync|createReadStream)\s*\(\s*(?:req\.|params\.|query\.|body\.|input)/;
|
|
1475
|
+
for (const [path, content] of files) {
|
|
1476
|
+
if (SKIP_PATH.test(path) || !isJS(path)) continue;
|
|
1477
|
+
const lines = content.split('\n');
|
|
1478
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1479
|
+
if (COMMENT_LINE.test(lines[i])) continue;
|
|
1480
|
+
if (p.test(lines[i])) findings.push({ ruleId: 'SEC-INJ-050', category: 'security', severity: 'critical', title: 'fs.readFile with user-supplied path — path traversal to /etc/passwd etc.', description: 'Reading files with user-controlled paths allows directory traversal. Validate paths against an allowlist and use path.resolve() with containment check.', file: path, line: i + 1, fix: null });
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
return findings;
|
|
1484
|
+
},
|
|
1485
|
+
});
|
|
1486
|
+
|
|
1487
|
+
// SEC-INJ-051: Regex DoS via user-controlled regex
|
|
1488
|
+
rules.push({
|
|
1489
|
+
id: 'SEC-INJ-051', category: 'security', severity: 'high', confidence: 'likely',
|
|
1490
|
+
title: 'User input used in RegExp constructor — ReDoS risk',
|
|
1491
|
+
check({ files }) {
|
|
1492
|
+
const findings = [];
|
|
1493
|
+
const p = /new\s+RegExp\s*\(\s*(?:req\.|params\.|query\.|body\.|input)/;
|
|
1494
|
+
for (const [path, content] of files) {
|
|
1495
|
+
if (SKIP_PATH.test(path) || !isJS(path)) continue;
|
|
1496
|
+
const lines = content.split('\n');
|
|
1497
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1498
|
+
if (COMMENT_LINE.test(lines[i])) continue;
|
|
1499
|
+
if (p.test(lines[i])) findings.push({ ruleId: 'SEC-INJ-051', category: 'security', severity: 'high', title: 'RegExp constructed from user input — ReDoS vulnerability', description: 'Crafted input can create pathological regex patterns that hang the server. Escape user input with a regex-escape library before passing to RegExp().', file: path, line: i + 1, fix: null });
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return findings;
|
|
1503
|
+
},
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
// SEC-INJ-052: Subprocess injection via template literal
|
|
1507
|
+
rules.push({
|
|
1508
|
+
id: 'SEC-INJ-052', category: 'security', severity: 'critical', confidence: 'likely',
|
|
1509
|
+
title: 'subprocess injection via template in spawn/fork',
|
|
1510
|
+
check({ files }) {
|
|
1511
|
+
const findings = [];
|
|
1512
|
+
const p = /(?:spawn|fork|execFile)\s*\(\s*`[^`]*\$\{/;
|
|
1513
|
+
for (const [path, content] of files) {
|
|
1514
|
+
if (SKIP_PATH.test(path) || !isJS(path)) continue;
|
|
1515
|
+
const lines = content.split('\n');
|
|
1516
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1517
|
+
if (COMMENT_LINE.test(lines[i])) continue;
|
|
1518
|
+
if (p.test(lines[i])) findings.push({ ruleId: 'SEC-INJ-052', category: 'security', severity: 'critical', title: 'spawn/fork with template literal argument — command injection', description: 'Use separate command and args array: spawn(cmd, [arg1, arg2]) instead of template literals.', file: path, line: i + 1, fix: null });
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
return findings;
|
|
1522
|
+
},
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1525
|
+
// SEC-INJ-053: NoSQL injection via user-controlled filter
|
|
1526
|
+
rules.push({
|
|
1527
|
+
id: 'SEC-INJ-053', category: 'security', severity: 'high', confidence: 'likely',
|
|
1528
|
+
title: 'Potential NoSQL injection via user-controlled query filter',
|
|
1529
|
+
check({ files }) {
|
|
1530
|
+
const findings = [];
|
|
1531
|
+
const p = /(?:find|findOne|findById|updateOne|deleteOne)\s*\(\s*(?:req\.|body\.|params\.|query\.)/;
|
|
1532
|
+
for (const [path, content] of files) {
|
|
1533
|
+
if (SKIP_PATH.test(path) || !isJS(path)) continue;
|
|
1534
|
+
const lines = content.split('\n');
|
|
1535
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1536
|
+
if (COMMENT_LINE.test(lines[i])) continue;
|
|
1537
|
+
if (p.test(lines[i])) findings.push({ ruleId: 'SEC-INJ-053', category: 'security', severity: 'high', title: 'NoSQL query with direct user input — NoSQL injection risk', description: 'Validate and sanitize query parameters before passing to NoSQL find/update operations. Use allowlist filtering.', file: path, line: i + 1, fix: null });
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
return findings;
|
|
1541
|
+
},
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
// SEC-INJ-054: Unsafe use of vm.runInNewContext with user data
|
|
1545
|
+
rules.push({
|
|
1546
|
+
id: 'SEC-INJ-054', category: 'security', severity: 'critical', confidence: 'likely',
|
|
1547
|
+
title: 'vm.runInNewContext/runInContext with user data — sandbox escape risk',
|
|
1548
|
+
check({ files }) {
|
|
1549
|
+
const findings = [];
|
|
1550
|
+
const p = /vm\.(?:runInNewContext|runInContext|runInThisContext)\s*\(\s*(?:req\.|body\.|params\.|query\.|userInput|input)/;
|
|
1551
|
+
for (const [path, content] of files) {
|
|
1552
|
+
if (SKIP_PATH.test(path) || !isJS(path)) continue;
|
|
1553
|
+
const lines = content.split('\n');
|
|
1554
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1555
|
+
if (COMMENT_LINE.test(lines[i])) continue;
|
|
1556
|
+
if (p.test(lines[i])) findings.push({ ruleId: 'SEC-INJ-054', category: 'security', severity: 'critical', title: 'vm.runInContext with user input — sandbox bypass possible', description: 'Node.js vm module does not provide a secure sandbox. Use a separate process or dedicated sandbox library.', file: path, line: i + 1, fix: null });
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
return findings;
|
|
1560
|
+
},
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
// SEC-INJ-055: Unsafe deserialization with node-serialize
|
|
1564
|
+
rules.push({
|
|
1565
|
+
id: 'SEC-INJ-055', category: 'security', severity: 'critical', confidence: 'definite',
|
|
1566
|
+
title: 'node-serialize package used — RCE via deserialization',
|
|
1567
|
+
check({ files, stack }) {
|
|
1568
|
+
const findings = [];
|
|
1569
|
+
if (!stack.dependencies?.['node-serialize']) return findings;
|
|
1570
|
+
const p = /require\(['"]node-serialize['"]\)|from\s+['"]node-serialize['"]/;
|
|
1571
|
+
for (const [path, content] of files) {
|
|
1572
|
+
if (SKIP_PATH.test(path) || !isJS(path)) continue;
|
|
1573
|
+
if (p.test(content)) findings.push({ ruleId: 'SEC-INJ-055', category: 'security', severity: 'critical', title: 'node-serialize allows arbitrary code execution via IIFE in serialized data', description: 'Remove node-serialize. Use JSON.parse/stringify for data serialization instead.', file: path, fix: null });
|
|
1574
|
+
}
|
|
1575
|
+
return findings;
|
|
1576
|
+
},
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1579
|
+
// SEC-INJ-056: Twig/Nunjucks template injection
|
|
1580
|
+
rules.push({
|
|
1581
|
+
id: 'SEC-INJ-056', category: 'security', severity: 'high', confidence: 'likely',
|
|
1582
|
+
title: 'Template engine rendered with user-controlled template string',
|
|
1583
|
+
check({ files }) {
|
|
1584
|
+
const findings = [];
|
|
1585
|
+
const p = /(?:nunjucks|twig|swig|mustache)\.(?:render|renderString)\s*\(\s*(?:req\.|body\.|params\.|query\.)/i;
|
|
1586
|
+
for (const [path, content] of files) {
|
|
1587
|
+
if (SKIP_PATH.test(path) || !isJS(path)) continue;
|
|
1588
|
+
const lines = content.split('\n');
|
|
1589
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1590
|
+
if (COMMENT_LINE.test(lines[i])) continue;
|
|
1591
|
+
if (p.test(lines[i])) findings.push({ ruleId: 'SEC-INJ-056', category: 'security', severity: 'high', title: 'Template engine renderString with user input — SSTI risk', description: 'Never render user-controlled strings as templates. Pre-compile templates from trusted sources only.', file: path, line: i + 1, fix: null });
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
return findings;
|
|
1595
|
+
},
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
// SEC-INJ-057: HTTP Host header injection
|
|
1599
|
+
rules.push({
|
|
1600
|
+
id: 'SEC-INJ-057', category: 'security', severity: 'high', confidence: 'likely',
|
|
1601
|
+
title: 'Trust of HTTP Host header without validation',
|
|
1602
|
+
check({ files }) {
|
|
1603
|
+
const findings = [];
|
|
1604
|
+
const p = /req\.(?:headers\.host|hostname|host)\s*(?:!==|===|==|!=|in|includes|\+|`)/;
|
|
1605
|
+
// HTTPS enforcement patterns that construct redirect URLs with req.hostname are safe
|
|
1606
|
+
const httpsRedirect = /['"]https:\/\/['"]\s*\+\s*req\.hostname/;
|
|
1607
|
+
// Comparisons against req.headers['x-forwarded-proto'] in the same context are HTTPS enforcement
|
|
1608
|
+
const protoCheck = /x-forwarded-proto/i;
|
|
1609
|
+
for (const [path, content] of files) {
|
|
1610
|
+
if (SKIP_PATH.test(path) || !isJS(path)) continue;
|
|
1611
|
+
const lines = content.split('\n');
|
|
1612
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1613
|
+
if (COMMENT_LINE.test(lines[i])) continue;
|
|
1614
|
+
if (p.test(lines[i]) && !httpsRedirect.test(lines[i])) {
|
|
1615
|
+
// Check if the surrounding context (nearby lines) is an HTTPS enforcement block
|
|
1616
|
+
const context = lines.slice(Math.max(0, i - 5), i + 3).join('\n');
|
|
1617
|
+
if (protoCheck.test(context)) continue;
|
|
1618
|
+
findings.push({ ruleId: 'SEC-INJ-057', category: 'security', severity: 'high', title: 'HTTP Host header used in logic — host header injection risk', description: 'Validate the Host header against an allowlist of known hostnames. Do not use it for URL construction.', file: path, line: i + 1, fix: null });
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
return findings;
|
|
1623
|
+
},
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
// SEC-INJ-058: XPath injection via string concatenation
|
|
1627
|
+
rules.push({
|
|
1628
|
+
id: 'SEC-INJ-058', category: 'security', severity: 'high', confidence: 'likely',
|
|
1629
|
+
title: 'XPath query constructed with user input — XPath injection',
|
|
1630
|
+
check({ files }) {
|
|
1631
|
+
const findings = [];
|
|
1632
|
+
const p = /xpath.*(?:req\.|body\.|params\.|query\.)|(?:req\.|body\.|params\.|query\.).*xpath/i;
|
|
1633
|
+
for (const [path, content] of files) {
|
|
1634
|
+
if (SKIP_PATH.test(path) || !isJS(path)) continue;
|
|
1635
|
+
if (p.test(content)) findings.push({ ruleId: 'SEC-INJ-058', category: 'security', severity: 'high', title: 'XPath expression with user input — XPath injection risk', description: 'Use parameterized XPath queries or escape special characters in XPath expressions.', file: path, fix: null });
|
|
1636
|
+
}
|
|
1637
|
+
return findings;
|
|
1638
|
+
},
|
|
1639
|
+
});
|