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,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth/JWT Vulnerability Detection Rules (SEC-JWT-001 through SEC-JWT-010)
|
|
3
|
+
*
|
|
4
|
+
* Detects insecure JWT configurations, OAuth implementation flaws,
|
|
5
|
+
* and token handling vulnerabilities.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
|
|
9
|
+
const isJS = (f) => JS_EXT.some(ext => f.endsWith(ext));
|
|
10
|
+
|
|
11
|
+
const SKIP_PATH = /[/\\](test|tests|__tests__|__mocks__|mocks|fixtures|__fixtures__|spec|node_modules|vendor|dist|build)[/\\]/i;
|
|
12
|
+
const COMMENT_LINE = /^\s*(\/\/|#|\/\*|\*)/;
|
|
13
|
+
|
|
14
|
+
function scanLines(content, regex, file, rule) {
|
|
15
|
+
const findings = [];
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
for (let i = 0; i < lines.length; i++) {
|
|
18
|
+
const line = lines[i];
|
|
19
|
+
if (COMMENT_LINE.test(line)) continue;
|
|
20
|
+
if (regex.test(line)) {
|
|
21
|
+
findings.push({
|
|
22
|
+
ruleId: rule.id,
|
|
23
|
+
category: rule.category,
|
|
24
|
+
severity: rule.severity,
|
|
25
|
+
title: rule.title,
|
|
26
|
+
description: rule.description,
|
|
27
|
+
confidence: rule.confidence,
|
|
28
|
+
file,
|
|
29
|
+
line: i + 1,
|
|
30
|
+
fix: rule.fix || null,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return findings;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const rules = [
|
|
38
|
+
// SEC-JWT-001: JWT algorithm "none" allowed
|
|
39
|
+
{
|
|
40
|
+
id: 'SEC-JWT-001',
|
|
41
|
+
category: 'security',
|
|
42
|
+
severity: 'critical',
|
|
43
|
+
confidence: 'definite',
|
|
44
|
+
title: 'JWT Algorithm "none" Allowed',
|
|
45
|
+
description:
|
|
46
|
+
'Allowing the "none" algorithm in JWT verification means tokens can be forged without any signature. This completely bypasses authentication.',
|
|
47
|
+
fix: { suggestion: 'Always specify the allowed algorithms explicitly: jwt.verify(token, secret, { algorithms: ["HS256"] })' },
|
|
48
|
+
check({ files }) {
|
|
49
|
+
const findings = [];
|
|
50
|
+
const pattern = /algorithms\s*:\s*\[.*['"]none['"]/;
|
|
51
|
+
for (const [path, content] of files) {
|
|
52
|
+
if (SKIP_PATH.test(path)) continue;
|
|
53
|
+
if (isJS(path)) {
|
|
54
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return findings;
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
// SEC-JWT-002: JWT verification without algorithm specification
|
|
62
|
+
{
|
|
63
|
+
id: 'SEC-JWT-002',
|
|
64
|
+
category: 'security',
|
|
65
|
+
severity: 'high',
|
|
66
|
+
confidence: 'likely',
|
|
67
|
+
title: 'JWT Verification Without Explicit Algorithm',
|
|
68
|
+
description:
|
|
69
|
+
'Not specifying allowed algorithms in jwt.verify() can lead to algorithm confusion attacks where an attacker switches from RS256 to HS256 using the public key as the HMAC secret.',
|
|
70
|
+
fix: { suggestion: 'Always pass { algorithms: ["RS256"] } (or your specific algorithm) to jwt.verify().' },
|
|
71
|
+
check({ files }) {
|
|
72
|
+
const findings = [];
|
|
73
|
+
const verifyPattern = /jwt\.verify\s*\(\s*\w+\s*,\s*\w+\s*\)/;
|
|
74
|
+
for (const [path, content] of files) {
|
|
75
|
+
if (SKIP_PATH.test(path)) continue;
|
|
76
|
+
if (isJS(path)) {
|
|
77
|
+
findings.push(...scanLines(content, verifyPattern, path, this));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return findings;
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// SEC-JWT-003: JWT token in URL query parameter
|
|
85
|
+
{
|
|
86
|
+
id: 'SEC-JWT-003',
|
|
87
|
+
category: 'security',
|
|
88
|
+
severity: 'high',
|
|
89
|
+
confidence: 'likely',
|
|
90
|
+
title: 'JWT Token Passed in URL Query Parameter',
|
|
91
|
+
description:
|
|
92
|
+
'Passing JWT tokens in URL query parameters exposes them in browser history, server logs, referer headers, and proxy logs.',
|
|
93
|
+
fix: { suggestion: 'Send tokens in the Authorization header (Bearer scheme) or in HTTP-only secure cookies.' },
|
|
94
|
+
check({ files }) {
|
|
95
|
+
const findings = [];
|
|
96
|
+
const pattern = /(?:req\.query\.token|req\.query\.jwt|req\.query\.access_token|req\.query\.auth_token|url.*[?&]token=|[?&]jwt=)/;
|
|
97
|
+
for (const [path, content] of files) {
|
|
98
|
+
if (SKIP_PATH.test(path)) continue;
|
|
99
|
+
if (isJS(path)) {
|
|
100
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return findings;
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
// SEC-JWT-004: Weak JWT signing secret
|
|
108
|
+
{
|
|
109
|
+
id: 'SEC-JWT-004',
|
|
110
|
+
category: 'security',
|
|
111
|
+
severity: 'critical',
|
|
112
|
+
confidence: 'definite',
|
|
113
|
+
title: 'Weak JWT Signing Secret',
|
|
114
|
+
description:
|
|
115
|
+
'Using a short or common string as a JWT signing secret allows brute-force attacks to recover the secret and forge tokens.',
|
|
116
|
+
fix: { suggestion: 'Use a cryptographically random secret of at least 256 bits (32 bytes). Better yet, use asymmetric keys (RS256/ES256).' },
|
|
117
|
+
check({ files }) {
|
|
118
|
+
const findings = [];
|
|
119
|
+
const pattern = /jwt\.sign\s*\([^)]*,\s*['"](?:secret|password|key|123|test|jwt|token|auth|mysecret|changeme)['"]/i;
|
|
120
|
+
for (const [path, content] of files) {
|
|
121
|
+
if (SKIP_PATH.test(path)) continue;
|
|
122
|
+
if (isJS(path)) {
|
|
123
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return findings;
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
// SEC-JWT-005: Missing JWT expiration
|
|
131
|
+
{
|
|
132
|
+
id: 'SEC-JWT-005',
|
|
133
|
+
category: 'security',
|
|
134
|
+
severity: 'high',
|
|
135
|
+
confidence: 'likely',
|
|
136
|
+
title: 'JWT Signed Without Expiration Claim',
|
|
137
|
+
description:
|
|
138
|
+
'JWTs without an expiration (exp) claim remain valid forever. If stolen, they grant permanent access.',
|
|
139
|
+
fix: { suggestion: 'Always set an expiresIn option: jwt.sign(payload, secret, { expiresIn: "1h" })' },
|
|
140
|
+
check({ files }) {
|
|
141
|
+
const findings = [];
|
|
142
|
+
const signPattern = /jwt\.sign\s*\(\s*\{[^}]*\}\s*,\s*\w+\s*\)/;
|
|
143
|
+
for (const [path, content] of files) {
|
|
144
|
+
if (SKIP_PATH.test(path)) continue;
|
|
145
|
+
if (!isJS(path)) continue;
|
|
146
|
+
const lines = content.split('\n');
|
|
147
|
+
for (let i = 0; i < lines.length; i++) {
|
|
148
|
+
if (COMMENT_LINE.test(lines[i])) continue;
|
|
149
|
+
if (signPattern.test(lines[i])) {
|
|
150
|
+
// Check if expiresIn or exp is set nearby
|
|
151
|
+
const ctx = lines.slice(Math.max(0, i - 2), Math.min(i + 3, lines.length)).join('\n');
|
|
152
|
+
if (!/(?:expiresIn|exp\s*:|maxAge)/.test(ctx)) {
|
|
153
|
+
findings.push({
|
|
154
|
+
ruleId: this.id,
|
|
155
|
+
category: this.category,
|
|
156
|
+
severity: this.severity,
|
|
157
|
+
title: this.title,
|
|
158
|
+
description: this.description,
|
|
159
|
+
confidence: this.confidence,
|
|
160
|
+
file: path,
|
|
161
|
+
line: i + 1,
|
|
162
|
+
fix: this.fix || null,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return findings;
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
// SEC-JWT-006: JWT stored in localStorage
|
|
173
|
+
{
|
|
174
|
+
id: 'SEC-JWT-006',
|
|
175
|
+
category: 'security',
|
|
176
|
+
severity: 'high',
|
|
177
|
+
confidence: 'likely',
|
|
178
|
+
title: 'JWT Token Stored in localStorage (XSS Accessible)',
|
|
179
|
+
description:
|
|
180
|
+
'Storing JWT tokens in localStorage makes them accessible to any JavaScript on the page, including XSS payloads.',
|
|
181
|
+
fix: { suggestion: 'Store tokens in HTTP-only, Secure, SameSite cookies instead of localStorage.' },
|
|
182
|
+
check({ files }) {
|
|
183
|
+
const findings = [];
|
|
184
|
+
const pattern = /localStorage\.setItem\s*\(\s*['"](?:token|jwt|access_token|auth_token|id_token|authToken|accessToken)['"]/;
|
|
185
|
+
for (const [path, content] of files) {
|
|
186
|
+
if (SKIP_PATH.test(path)) continue;
|
|
187
|
+
if (isJS(path)) {
|
|
188
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return findings;
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
// SEC-JWT-007: Missing audience/issuer validation
|
|
196
|
+
{
|
|
197
|
+
id: 'SEC-JWT-007',
|
|
198
|
+
category: 'security',
|
|
199
|
+
severity: 'medium',
|
|
200
|
+
confidence: 'likely',
|
|
201
|
+
title: 'JWT Verification Without Audience or Issuer Validation',
|
|
202
|
+
description:
|
|
203
|
+
'Not validating the audience (aud) and issuer (iss) claims allows tokens issued for other services to be reused, enabling cross-service token confusion.',
|
|
204
|
+
fix: { suggestion: 'Add audience and issuer validation: jwt.verify(token, secret, { audience: "my-app", issuer: "auth-server" })' },
|
|
205
|
+
check({ files }) {
|
|
206
|
+
const findings = [];
|
|
207
|
+
const verifyPattern = /jwt\.verify\s*\(/;
|
|
208
|
+
for (const [path, content] of files) {
|
|
209
|
+
if (SKIP_PATH.test(path)) continue;
|
|
210
|
+
if (!isJS(path)) continue;
|
|
211
|
+
if (verifyPattern.test(content)) {
|
|
212
|
+
if (!/(?:audience|issuer|aud|iss)/.test(content)) {
|
|
213
|
+
findings.push({
|
|
214
|
+
ruleId: this.id,
|
|
215
|
+
category: this.category,
|
|
216
|
+
severity: this.severity,
|
|
217
|
+
title: this.title,
|
|
218
|
+
description: this.description,
|
|
219
|
+
confidence: this.confidence,
|
|
220
|
+
file: path,
|
|
221
|
+
fix: this.fix || null,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return findings;
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
// SEC-JWT-008: OAuth state parameter not validated
|
|
231
|
+
{
|
|
232
|
+
id: 'SEC-JWT-008',
|
|
233
|
+
category: 'security',
|
|
234
|
+
severity: 'high',
|
|
235
|
+
confidence: 'likely',
|
|
236
|
+
title: 'OAuth State Parameter Not Validated (CSRF Risk)',
|
|
237
|
+
description:
|
|
238
|
+
'Not validating the OAuth state parameter allows CSRF attacks where an attacker can force a victim to authenticate with the attacker\'s account.',
|
|
239
|
+
fix: { suggestion: 'Generate a cryptographic random state, store it in the session, and verify it matches on callback.' },
|
|
240
|
+
check({ files }) {
|
|
241
|
+
const findings = [];
|
|
242
|
+
const callbackPattern = /(?:\/callback|\/oauth\/callback|\/auth\/callback)/;
|
|
243
|
+
const tokenExchange = /(?:getToken|requestToken|exchangeCode|code.*token|grant_type.*authorization_code)/;
|
|
244
|
+
for (const [path, content] of files) {
|
|
245
|
+
if (SKIP_PATH.test(path)) continue;
|
|
246
|
+
if (!isJS(path)) continue;
|
|
247
|
+
if (callbackPattern.test(content) && tokenExchange.test(content)) {
|
|
248
|
+
if (!/(?:state|csrf|nonce)/.test(content)) {
|
|
249
|
+
findings.push({
|
|
250
|
+
ruleId: this.id,
|
|
251
|
+
category: this.category,
|
|
252
|
+
severity: this.severity,
|
|
253
|
+
title: this.title,
|
|
254
|
+
description: this.description,
|
|
255
|
+
confidence: this.confidence,
|
|
256
|
+
file: path,
|
|
257
|
+
fix: this.fix || null,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return findings;
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
// SEC-JWT-009: Refresh token without rotation
|
|
267
|
+
{
|
|
268
|
+
id: 'SEC-JWT-009',
|
|
269
|
+
category: 'security',
|
|
270
|
+
severity: 'medium',
|
|
271
|
+
confidence: 'likely',
|
|
272
|
+
title: 'Refresh Token Reuse Without Rotation',
|
|
273
|
+
description:
|
|
274
|
+
'Reusing the same refresh token without issuing a new one on each refresh allows stolen refresh tokens to be used indefinitely.',
|
|
275
|
+
fix: { suggestion: 'Implement refresh token rotation: issue a new refresh token on each use and invalidate the old one.' },
|
|
276
|
+
check({ files }) {
|
|
277
|
+
const findings = [];
|
|
278
|
+
const refreshPattern = /(?:refreshToken|refresh_token)/;
|
|
279
|
+
for (const [path, content] of files) {
|
|
280
|
+
if (SKIP_PATH.test(path)) continue;
|
|
281
|
+
if (!isJS(path)) continue;
|
|
282
|
+
if (refreshPattern.test(content) && /jwt\.sign/.test(content)) {
|
|
283
|
+
// Check if there is token rotation logic
|
|
284
|
+
if (!/(?:rotate|revoke|invalidate|delete.*refresh|remove.*refresh|new.*refreshToken|replace.*refresh)/.test(content)) {
|
|
285
|
+
findings.push({
|
|
286
|
+
ruleId: this.id,
|
|
287
|
+
category: this.category,
|
|
288
|
+
severity: this.severity,
|
|
289
|
+
title: this.title,
|
|
290
|
+
description: this.description,
|
|
291
|
+
confidence: this.confidence,
|
|
292
|
+
file: path,
|
|
293
|
+
fix: this.fix || null,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return findings;
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
// SEC-JWT-010: JWT payload used without verification
|
|
303
|
+
{
|
|
304
|
+
id: 'SEC-JWT-010',
|
|
305
|
+
category: 'security',
|
|
306
|
+
severity: 'critical',
|
|
307
|
+
confidence: 'likely',
|
|
308
|
+
title: 'JWT Payload Decoded Without Verification',
|
|
309
|
+
description:
|
|
310
|
+
'Using jwt.decode() instead of jwt.verify() reads the JWT payload without checking the signature, allowing forged tokens to pass.',
|
|
311
|
+
fix: { suggestion: 'Always use jwt.verify() to validate the signature before trusting the payload.' },
|
|
312
|
+
check({ files }) {
|
|
313
|
+
const findings = [];
|
|
314
|
+
const pattern = /jwt\.decode\s*\(/;
|
|
315
|
+
for (const [path, content] of files) {
|
|
316
|
+
if (SKIP_PATH.test(path)) continue;
|
|
317
|
+
if (isJS(path)) {
|
|
318
|
+
// Only flag if jwt.verify is not also used (decode-only usage)
|
|
319
|
+
if (!/jwt\.verify\s*\(/.test(content)) {
|
|
320
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return findings;
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
export default rules;
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path Traversal Detection Rules (SEC-PT-001 through SEC-PT-010)
|
|
3
|
+
*
|
|
4
|
+
* Detects patterns where user-controlled input is used in file system
|
|
5
|
+
* operations without proper validation, enabling directory traversal attacks.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
|
|
9
|
+
const isJS = (f) => JS_EXT.some(ext => f.endsWith(ext));
|
|
10
|
+
|
|
11
|
+
const SKIP_PATH = /[/\\](test|tests|__tests__|__mocks__|mocks|fixtures|__fixtures__|spec|node_modules|vendor|dist|build)[/\\]/i;
|
|
12
|
+
const COMMENT_LINE = /^\s*(\/\/|#|\/\*|\*)/;
|
|
13
|
+
|
|
14
|
+
function scanLines(content, regex, file, rule) {
|
|
15
|
+
const findings = [];
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
for (let i = 0; i < lines.length; i++) {
|
|
18
|
+
const line = lines[i];
|
|
19
|
+
if (COMMENT_LINE.test(line)) continue;
|
|
20
|
+
if (regex.test(line)) {
|
|
21
|
+
findings.push({
|
|
22
|
+
ruleId: rule.id,
|
|
23
|
+
category: rule.category,
|
|
24
|
+
severity: rule.severity,
|
|
25
|
+
title: rule.title,
|
|
26
|
+
description: rule.description,
|
|
27
|
+
confidence: rule.confidence,
|
|
28
|
+
file,
|
|
29
|
+
line: i + 1,
|
|
30
|
+
fix: rule.fix || null,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return findings;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const rules = [
|
|
38
|
+
// SEC-PT-001: fs.readFile with user-controlled path
|
|
39
|
+
{
|
|
40
|
+
id: 'SEC-PT-001',
|
|
41
|
+
category: 'security',
|
|
42
|
+
severity: 'critical',
|
|
43
|
+
confidence: 'likely',
|
|
44
|
+
title: 'fs.readFile with User-Controlled Path (Path Traversal)',
|
|
45
|
+
description:
|
|
46
|
+
'Using fs.readFile/readFileSync with user-controlled file paths allows attackers to read arbitrary files (e.g., /etc/passwd, .env) via ../ sequences.',
|
|
47
|
+
fix: { suggestion: 'Use path.resolve() and verify the resolved path starts with the expected base directory. Never pass user input directly to fs operations.' },
|
|
48
|
+
check({ files }) {
|
|
49
|
+
const findings = [];
|
|
50
|
+
const pattern = /fs\.readFile(?:Sync)?\s*\(\s*(?:req\.body|req\.query|req\.params|filePath|fileName|userPath|inputPath|file)\b/;
|
|
51
|
+
for (const [path, content] of files) {
|
|
52
|
+
if (SKIP_PATH.test(path)) continue;
|
|
53
|
+
if (isJS(path)) {
|
|
54
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return findings;
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
// SEC-PT-002: fs.writeFile with user-controlled path
|
|
62
|
+
{
|
|
63
|
+
id: 'SEC-PT-002',
|
|
64
|
+
category: 'security',
|
|
65
|
+
severity: 'critical',
|
|
66
|
+
confidence: 'likely',
|
|
67
|
+
title: 'fs.writeFile with User-Controlled Path (Path Traversal)',
|
|
68
|
+
description:
|
|
69
|
+
'Using fs.writeFile/writeFileSync with user-controlled paths allows attackers to overwrite arbitrary files, potentially replacing config files or injecting code.',
|
|
70
|
+
fix: { suggestion: 'Resolve the path and verify it is within the allowed directory. Use a whitelist of allowed file extensions.' },
|
|
71
|
+
check({ files }) {
|
|
72
|
+
const findings = [];
|
|
73
|
+
const pattern = /fs\.writeFile(?:Sync)?\s*\(\s*(?:req\.body|req\.query|req\.params|filePath|fileName|userPath|inputPath|file)\b/;
|
|
74
|
+
for (const [path, content] of files) {
|
|
75
|
+
if (SKIP_PATH.test(path)) continue;
|
|
76
|
+
if (isJS(path)) {
|
|
77
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return findings;
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// SEC-PT-003: path.join with unsanitized user input
|
|
85
|
+
{
|
|
86
|
+
id: 'SEC-PT-003',
|
|
87
|
+
category: 'security',
|
|
88
|
+
severity: 'high',
|
|
89
|
+
confidence: 'likely',
|
|
90
|
+
title: 'path.join with Unsanitized User Input',
|
|
91
|
+
description:
|
|
92
|
+
'path.join does NOT prevent directory traversal — path.join("/uploads", "../../../etc/passwd") resolves to "/etc/passwd". Always validate the resolved path.',
|
|
93
|
+
fix: { suggestion: 'After path.join, use path.resolve and verify the result starts with the intended base directory.' },
|
|
94
|
+
check({ files }) {
|
|
95
|
+
const findings = [];
|
|
96
|
+
const pattern = /path\.join\s*\([^)]*(?:req\.body|req\.query|req\.params|fileName|userPath|inputPath|input)\b/;
|
|
97
|
+
for (const [path, content] of files) {
|
|
98
|
+
if (SKIP_PATH.test(path)) continue;
|
|
99
|
+
if (isJS(path)) {
|
|
100
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return findings;
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
// SEC-PT-004: Express static file serving with dynamic path
|
|
108
|
+
{
|
|
109
|
+
id: 'SEC-PT-004',
|
|
110
|
+
category: 'security',
|
|
111
|
+
severity: 'high',
|
|
112
|
+
confidence: 'likely',
|
|
113
|
+
title: 'Dynamic File Serving Endpoint (Path Traversal Risk)',
|
|
114
|
+
description:
|
|
115
|
+
'Express route that serves files based on URL parameters without path validation enables directory traversal to read any file on the server.',
|
|
116
|
+
fix: { suggestion: 'Use express.static() for serving files. If dynamic paths are needed, resolve and validate against the base directory.' },
|
|
117
|
+
check({ files }) {
|
|
118
|
+
const findings = [];
|
|
119
|
+
const pattern = /res\.sendFile\s*\(\s*(?:req\.params|req\.query|req\.body|filePath|fileName|path\.join\s*\([^)]*req\.)/;
|
|
120
|
+
for (const [path, content] of files) {
|
|
121
|
+
if (SKIP_PATH.test(path)) continue;
|
|
122
|
+
if (isJS(path)) {
|
|
123
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return findings;
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
// SEC-PT-005: Directory traversal pattern in string
|
|
131
|
+
{
|
|
132
|
+
id: 'SEC-PT-005',
|
|
133
|
+
category: 'security',
|
|
134
|
+
severity: 'high',
|
|
135
|
+
confidence: 'likely',
|
|
136
|
+
title: 'Insufficient Directory Traversal Filtering',
|
|
137
|
+
description:
|
|
138
|
+
'Simple string replacement of "../" is insufficient as it can be bypassed with "....//", URL encoding (%2e%2e%2f), or double encoding.',
|
|
139
|
+
fix: { suggestion: 'Use path.resolve() to normalize the path, then verify it starts with the allowed base directory. Do not rely on string filtering.' },
|
|
140
|
+
check({ files }) {
|
|
141
|
+
const findings = [];
|
|
142
|
+
const pattern = /\.replace\s*\(\s*['"]\.\.[\\/]['"]/;
|
|
143
|
+
for (const [path, content] of files) {
|
|
144
|
+
if (SKIP_PATH.test(path)) continue;
|
|
145
|
+
if (isJS(path)) {
|
|
146
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return findings;
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// SEC-PT-006: createReadStream with user input
|
|
154
|
+
{
|
|
155
|
+
id: 'SEC-PT-006',
|
|
156
|
+
category: 'security',
|
|
157
|
+
severity: 'critical',
|
|
158
|
+
confidence: 'likely',
|
|
159
|
+
title: 'fs.createReadStream with User-Controlled Path',
|
|
160
|
+
description:
|
|
161
|
+
'Creating a read stream from a user-controlled path enables reading arbitrary files, potentially exfiltrating sensitive data.',
|
|
162
|
+
fix: { suggestion: 'Validate and resolve the path against an allowed base directory before creating the stream.' },
|
|
163
|
+
check({ files }) {
|
|
164
|
+
const findings = [];
|
|
165
|
+
const pattern = /fs\.createReadStream\s*\(\s*(?:req\.body|req\.query|req\.params|filePath|fileName|userPath|inputPath|file)\b/;
|
|
166
|
+
for (const [path, content] of files) {
|
|
167
|
+
if (SKIP_PATH.test(path)) continue;
|
|
168
|
+
if (isJS(path)) {
|
|
169
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return findings;
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
// SEC-PT-007: Zip extraction without path validation (Zip Slip)
|
|
177
|
+
{
|
|
178
|
+
id: 'SEC-PT-007',
|
|
179
|
+
category: 'security',
|
|
180
|
+
severity: 'critical',
|
|
181
|
+
confidence: 'likely',
|
|
182
|
+
title: 'Zip Extraction Without Path Validation (Zip Slip)',
|
|
183
|
+
description:
|
|
184
|
+
'Extracting zip/tar archives without validating extracted file paths allows Zip Slip attacks (CVE-2018-1002200) where malicious archives overwrite files outside the target directory.',
|
|
185
|
+
fix: { suggestion: 'Validate that each extracted file path, after resolution, starts with the intended extraction directory.' },
|
|
186
|
+
check({ files }) {
|
|
187
|
+
const findings = [];
|
|
188
|
+
const pattern = /(?:unzip|extract|decompress|tar\.extract|AdmZip|yauzl|archiver|node-tar)\s*[\(.]/;
|
|
189
|
+
for (const [path, content] of files) {
|
|
190
|
+
if (SKIP_PATH.test(path)) continue;
|
|
191
|
+
if (!isJS(path)) continue;
|
|
192
|
+
if (pattern.test(content)) {
|
|
193
|
+
// Check if there is path validation
|
|
194
|
+
if (!/(?:startsWith|resolve.*includes|normalize.*startsWith|zipSlip|sanitize)/.test(content)) {
|
|
195
|
+
findings.push({
|
|
196
|
+
ruleId: this.id,
|
|
197
|
+
category: this.category,
|
|
198
|
+
severity: this.severity,
|
|
199
|
+
title: this.title,
|
|
200
|
+
description: this.description,
|
|
201
|
+
confidence: this.confidence,
|
|
202
|
+
file: path,
|
|
203
|
+
fix: this.fix || null,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return findings;
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
// SEC-PT-008: fs.unlink/rm with user-controlled path
|
|
213
|
+
{
|
|
214
|
+
id: 'SEC-PT-008',
|
|
215
|
+
category: 'security',
|
|
216
|
+
severity: 'critical',
|
|
217
|
+
confidence: 'likely',
|
|
218
|
+
title: 'fs.unlink/rm with User-Controlled Path (Arbitrary File Deletion)',
|
|
219
|
+
description:
|
|
220
|
+
'Using fs.unlink or fs.rm with user-controlled paths allows attackers to delete arbitrary files on the server.',
|
|
221
|
+
fix: { suggestion: 'Resolve and validate the path against an allowed directory before deletion. Implement proper access controls.' },
|
|
222
|
+
check({ files }) {
|
|
223
|
+
const findings = [];
|
|
224
|
+
const pattern = /fs\.(?:unlink|unlinkSync|rm|rmSync)\s*\(\s*(?:req\.body|req\.query|req\.params|filePath|fileName|userPath|inputPath|file)\b/;
|
|
225
|
+
for (const [path, content] of files) {
|
|
226
|
+
if (SKIP_PATH.test(path)) continue;
|
|
227
|
+
if (isJS(path)) {
|
|
228
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return findings;
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
// SEC-PT-009: Symlink following in file operations
|
|
236
|
+
{
|
|
237
|
+
id: 'SEC-PT-009',
|
|
238
|
+
category: 'security',
|
|
239
|
+
severity: 'high',
|
|
240
|
+
confidence: 'likely',
|
|
241
|
+
title: 'File Operation Without Symlink Check',
|
|
242
|
+
description:
|
|
243
|
+
'File operations that follow symlinks can be exploited by creating symlinks pointing to sensitive files outside the intended directory.',
|
|
244
|
+
fix: { suggestion: 'Use fs.lstat() to check for symlinks before file operations, or use O_NOFOLLOW flag.' },
|
|
245
|
+
check({ files }) {
|
|
246
|
+
const findings = [];
|
|
247
|
+
const uploadPattern = /(?:multer|upload|formidable|busboy)/i;
|
|
248
|
+
const fsPattern = /fs\.(?:readFile|writeFile|createReadStream|createWriteStream|access|stat)\s*\(/;
|
|
249
|
+
for (const [path, content] of files) {
|
|
250
|
+
if (SKIP_PATH.test(path)) continue;
|
|
251
|
+
if (!isJS(path)) continue;
|
|
252
|
+
if (uploadPattern.test(content) && fsPattern.test(content)) {
|
|
253
|
+
if (!/(?:lstat|isSymbolicLink|O_NOFOLLOW|realpath)/.test(content)) {
|
|
254
|
+
findings.push({
|
|
255
|
+
ruleId: this.id,
|
|
256
|
+
category: this.category,
|
|
257
|
+
severity: this.severity,
|
|
258
|
+
title: this.title,
|
|
259
|
+
description: this.description,
|
|
260
|
+
confidence: this.confidence,
|
|
261
|
+
file: path,
|
|
262
|
+
fix: this.fix || null,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return findings;
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
// SEC-PT-010: Template file inclusion with user input
|
|
272
|
+
{
|
|
273
|
+
id: 'SEC-PT-010',
|
|
274
|
+
category: 'security',
|
|
275
|
+
severity: 'critical',
|
|
276
|
+
confidence: 'likely',
|
|
277
|
+
title: 'Template/View File Inclusion with User Input (LFI)',
|
|
278
|
+
description:
|
|
279
|
+
'Rendering templates based on user-controlled view names allows Local File Inclusion (LFI) to read arbitrary files or execute server-side templates.',
|
|
280
|
+
fix: { suggestion: 'Use a whitelist of allowed template names. Never pass user input directly to res.render() or template engines.' },
|
|
281
|
+
check({ files }) {
|
|
282
|
+
const findings = [];
|
|
283
|
+
const pattern = /res\.render\s*\(\s*(?:req\.body|req\.query|req\.params|templateName|viewName|page|input)\b/;
|
|
284
|
+
for (const [path, content] of files) {
|
|
285
|
+
if (SKIP_PATH.test(path)) continue;
|
|
286
|
+
if (isJS(path)) {
|
|
287
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return findings;
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
export default rules;
|