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,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Insecure Deserialization Detection Rules (SEC-DES-001 through SEC-DES-010)
|
|
3
|
+
*
|
|
4
|
+
* Detects patterns where untrusted data is deserialized in ways that
|
|
5
|
+
* can lead to remote code execution, prototype pollution, or DoS.
|
|
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|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
|
+
// SEC-DES-001: eval-based deserialization
|
|
40
|
+
{
|
|
41
|
+
id: 'SEC-DES-001',
|
|
42
|
+
category: 'security',
|
|
43
|
+
severity: 'critical',
|
|
44
|
+
confidence: 'definite',
|
|
45
|
+
title: 'eval() Used for Deserialization',
|
|
46
|
+
description:
|
|
47
|
+
'Using eval() to parse data (JSON, config, etc.) allows arbitrary code execution. An attacker can inject executable code into the serialized payload.',
|
|
48
|
+
fix: { suggestion: 'Use JSON.parse() for JSON data. Never use eval() for deserialization.' },
|
|
49
|
+
check({ files }) {
|
|
50
|
+
const findings = [];
|
|
51
|
+
const pattern = /eval\s*\(\s*(?:req\.body|req\.query|data|payload|input|content|message|response|text)\b/;
|
|
52
|
+
for (const [path, content] of files) {
|
|
53
|
+
if (SKIP_PATH.test(path)) continue;
|
|
54
|
+
if (isJS(path)) {
|
|
55
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return findings;
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// SEC-DES-002: Function constructor for deserialization
|
|
63
|
+
{
|
|
64
|
+
id: 'SEC-DES-002',
|
|
65
|
+
category: 'security',
|
|
66
|
+
severity: 'critical',
|
|
67
|
+
confidence: 'definite',
|
|
68
|
+
title: 'Function Constructor Used for Deserialization',
|
|
69
|
+
description:
|
|
70
|
+
'new Function() with untrusted input is equivalent to eval() and allows arbitrary code execution.',
|
|
71
|
+
fix: { suggestion: 'Use JSON.parse() or a safe parsing library. Never construct functions from user data.' },
|
|
72
|
+
check({ files }) {
|
|
73
|
+
const findings = [];
|
|
74
|
+
const pattern = /new\s+Function\s*\(\s*(?:req\.body|req\.query|data|payload|input|content|message|text)\b/;
|
|
75
|
+
for (const [path, content] of files) {
|
|
76
|
+
if (SKIP_PATH.test(path)) continue;
|
|
77
|
+
if (isJS(path)) {
|
|
78
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return findings;
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// SEC-DES-003: YAML unsafe load
|
|
86
|
+
{
|
|
87
|
+
id: 'SEC-DES-003',
|
|
88
|
+
category: 'security',
|
|
89
|
+
severity: 'critical',
|
|
90
|
+
confidence: 'definite',
|
|
91
|
+
title: 'Unsafe YAML Load (Code Execution Risk)',
|
|
92
|
+
description:
|
|
93
|
+
'yaml.load() without SafeLoader in Python or js-yaml.load() without safe schema can execute arbitrary code via YAML tags like !!python/object.',
|
|
94
|
+
fix: { suggestion: 'Use yaml.safe_load() in Python or yaml.load(data, { schema: yaml.SAFE_SCHEMA }) in js-yaml.' },
|
|
95
|
+
check({ files }) {
|
|
96
|
+
const findings = [];
|
|
97
|
+
const jsPattern = /yaml\.load\s*\([^)]*(?!.*(?:safe|SAFE_SCHEMA|JSON_SCHEMA|FAILSAFE_SCHEMA))/;
|
|
98
|
+
const pyPattern = /yaml\.load\s*\((?![^)]*(?:SafeLoader|safe_load|CSafeLoader))/;
|
|
99
|
+
for (const [path, content] of files) {
|
|
100
|
+
if (SKIP_PATH.test(path)) continue;
|
|
101
|
+
if (isJS(path)) {
|
|
102
|
+
findings.push(...scanLines(content, jsPattern, path, this));
|
|
103
|
+
} else if (isPy(path)) {
|
|
104
|
+
findings.push(...scanLines(content, pyPattern, path, this));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return findings;
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
// SEC-DES-004: node-serialize / serialize-javascript with untrusted data
|
|
112
|
+
{
|
|
113
|
+
id: 'SEC-DES-004',
|
|
114
|
+
category: 'security',
|
|
115
|
+
severity: 'critical',
|
|
116
|
+
confidence: 'definite',
|
|
117
|
+
title: 'node-serialize Unserialize with Untrusted Data (RCE)',
|
|
118
|
+
description:
|
|
119
|
+
'node-serialize.unserialize() can execute arbitrary code via IIFE patterns in serialized data (CVE-2017-5941).',
|
|
120
|
+
fix: { suggestion: 'Remove node-serialize. Use JSON.parse() for structured data exchange.' },
|
|
121
|
+
check({ files }) {
|
|
122
|
+
const findings = [];
|
|
123
|
+
const pattern = /(?:serialize\.unserialize|unserialize)\s*\(\s*(?:req\.body|req\.query|data|payload|input|cookie)/;
|
|
124
|
+
for (const [path, content] of files) {
|
|
125
|
+
if (SKIP_PATH.test(path)) continue;
|
|
126
|
+
if (isJS(path)) {
|
|
127
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return findings;
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
// SEC-DES-005: JSON.parse of large/untrusted input without size limit
|
|
135
|
+
{
|
|
136
|
+
id: 'SEC-DES-005',
|
|
137
|
+
category: 'security',
|
|
138
|
+
severity: 'medium',
|
|
139
|
+
confidence: 'likely',
|
|
140
|
+
title: 'JSON.parse on Untrusted Input Without Size Limit',
|
|
141
|
+
description:
|
|
142
|
+
'Parsing large JSON payloads from user input without size limits can cause DoS via memory exhaustion or CPU-intensive parsing.',
|
|
143
|
+
fix: { suggestion: 'Limit request body size (e.g., express.json({ limit: "1mb" })) and validate input structure with a schema.' },
|
|
144
|
+
check({ files }) {
|
|
145
|
+
const findings = [];
|
|
146
|
+
const pattern = /JSON\.parse\s*\(\s*(?:req\.body|req\.query\.\w+|rawBody|body|payload)\b/;
|
|
147
|
+
for (const [path, content] of files) {
|
|
148
|
+
if (SKIP_PATH.test(path)) continue;
|
|
149
|
+
if (isJS(path)) {
|
|
150
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return findings;
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
// SEC-DES-006: Python pickle with untrusted data
|
|
158
|
+
{
|
|
159
|
+
id: 'SEC-DES-006',
|
|
160
|
+
category: 'security',
|
|
161
|
+
severity: 'critical',
|
|
162
|
+
confidence: 'definite',
|
|
163
|
+
title: 'Python pickle.loads with Untrusted Data (RCE)',
|
|
164
|
+
description:
|
|
165
|
+
'pickle.loads() can execute arbitrary Python code during deserialization. Never unpickle data from untrusted sources.',
|
|
166
|
+
fix: { suggestion: 'Use JSON, MessagePack, or Protocol Buffers instead of pickle for untrusted data.' },
|
|
167
|
+
check({ files }) {
|
|
168
|
+
const findings = [];
|
|
169
|
+
const pattern = /pickle\.(?:loads?|Unpickler)\s*\(/;
|
|
170
|
+
for (const [path, content] of files) {
|
|
171
|
+
if (SKIP_PATH.test(path)) continue;
|
|
172
|
+
if (isPy(path)) {
|
|
173
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return findings;
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
// SEC-DES-007: vm/vm2 module with untrusted code
|
|
181
|
+
{
|
|
182
|
+
id: 'SEC-DES-007',
|
|
183
|
+
category: 'security',
|
|
184
|
+
severity: 'critical',
|
|
185
|
+
confidence: 'definite',
|
|
186
|
+
title: 'Node.js vm Module with Untrusted Code (Sandbox Escape)',
|
|
187
|
+
description:
|
|
188
|
+
'The Node.js vm module does not provide a secure sandbox. Untrusted code can escape via prototype chain access. vm2 also has known escapes (CVE-2023-37466).',
|
|
189
|
+
fix: { suggestion: 'Use isolated-vm or run untrusted code in a separate process/container with seccomp/AppArmor.' },
|
|
190
|
+
check({ files }) {
|
|
191
|
+
const findings = [];
|
|
192
|
+
const pattern = /(?:vm\.runInNewContext|vm\.runInContext|vm\.runInThisContext|vm\.createContext|vm\.Script|new\s+VM\s*\()\s*\(/;
|
|
193
|
+
for (const [path, content] of files) {
|
|
194
|
+
if (SKIP_PATH.test(path)) continue;
|
|
195
|
+
if (isJS(path)) {
|
|
196
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return findings;
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
// SEC-DES-008: XML external entity processing (XXE)
|
|
204
|
+
{
|
|
205
|
+
id: 'SEC-DES-008',
|
|
206
|
+
category: 'security',
|
|
207
|
+
severity: 'critical',
|
|
208
|
+
confidence: 'likely',
|
|
209
|
+
title: 'XML Parser Without XXE Protection',
|
|
210
|
+
description:
|
|
211
|
+
'XML parsers that process external entities can be exploited to read local files, perform SSRF, or cause DoS. Disable external entity processing.',
|
|
212
|
+
fix: { suggestion: 'Disable external entities: use libxmljs with noent: false, or xml2js (safe by default). For fast-xml-parser, set processEntities: false.' },
|
|
213
|
+
check({ files }) {
|
|
214
|
+
const findings = [];
|
|
215
|
+
const pattern = /(?:libxmljs\.parseXml|DOMParser|xml2js\.parseString|parseXml|XMLParser)\s*\(/;
|
|
216
|
+
for (const [path, content] of files) {
|
|
217
|
+
if (SKIP_PATH.test(path)) continue;
|
|
218
|
+
if (!isJS(path)) continue;
|
|
219
|
+
const lines = content.split('\n');
|
|
220
|
+
for (let i = 0; i < lines.length; i++) {
|
|
221
|
+
if (COMMENT_LINE.test(lines[i])) continue;
|
|
222
|
+
if (pattern.test(lines[i])) {
|
|
223
|
+
// Check if noent is explicitly set or entities are disabled
|
|
224
|
+
if (/noent\s*:\s*true|processEntities\s*:\s*true|resolveExternals/.test(content)) {
|
|
225
|
+
findings.push({
|
|
226
|
+
ruleId: this.id,
|
|
227
|
+
category: this.category,
|
|
228
|
+
severity: this.severity,
|
|
229
|
+
title: this.title,
|
|
230
|
+
description: this.description,
|
|
231
|
+
confidence: this.confidence,
|
|
232
|
+
file: path,
|
|
233
|
+
line: i + 1,
|
|
234
|
+
fix: this.fix || null,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return findings;
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
// SEC-DES-009: MessagePack / BSON deserialization of untrusted data
|
|
245
|
+
{
|
|
246
|
+
id: 'SEC-DES-009',
|
|
247
|
+
category: 'security',
|
|
248
|
+
severity: 'medium',
|
|
249
|
+
confidence: 'likely',
|
|
250
|
+
title: 'Binary Format Deserialization of Untrusted Input',
|
|
251
|
+
description:
|
|
252
|
+
'Deserializing binary formats (MessagePack, BSON, protobuf) from untrusted input without schema validation can lead to type confusion or memory issues.',
|
|
253
|
+
fix: { suggestion: 'Validate deserialized data against a strict schema using Zod, Joi, or Protocol Buffers with defined .proto files.' },
|
|
254
|
+
check({ files }) {
|
|
255
|
+
const findings = [];
|
|
256
|
+
const pattern = /(?:msgpack\.decode|msgpack\.unpack|BSON\.deserialize|bson\.deserialize)\s*\(\s*(?:req\.body|buffer|data|payload|input)\b/;
|
|
257
|
+
for (const [path, content] of files) {
|
|
258
|
+
if (SKIP_PATH.test(path)) continue;
|
|
259
|
+
if (isJS(path)) {
|
|
260
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return findings;
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
// SEC-DES-010: Cookie deserialization without signature verification
|
|
268
|
+
{
|
|
269
|
+
id: 'SEC-DES-010',
|
|
270
|
+
category: 'security',
|
|
271
|
+
severity: 'high',
|
|
272
|
+
confidence: 'likely',
|
|
273
|
+
title: 'Cookie Value Deserialized Without Signature Verification',
|
|
274
|
+
description:
|
|
275
|
+
'Deserializing cookie values (JSON.parse of cookie, unserialize of cookie) without verifying a cryptographic signature (HMAC) allows cookie tampering and injection.',
|
|
276
|
+
fix: { suggestion: 'Use signed cookies (cookie-signature or express signed cookies) and verify the HMAC before parsing.' },
|
|
277
|
+
check({ files }) {
|
|
278
|
+
const findings = [];
|
|
279
|
+
const pattern = /JSON\.parse\s*\(\s*(?:req\.cookies|cookie|cookies)\b/;
|
|
280
|
+
for (const [path, content] of files) {
|
|
281
|
+
if (SKIP_PATH.test(path)) continue;
|
|
282
|
+
if (isJS(path)) {
|
|
283
|
+
findings.push(...scanLines(content, pattern, path, this));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return findings;
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
export default rules;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
|
|
2
|
+
const isJS = (f) => JS_EXT.some(ext => f.endsWith(ext));
|
|
3
|
+
const isTest = (f) => f.includes('test') || f.includes('spec') || f.includes('mock');
|
|
4
|
+
|
|
5
|
+
const rules = [
|
|
6
|
+
// SEC-UPL-001
|
|
7
|
+
{
|
|
8
|
+
id: 'SEC-UPL-001', category: 'security', severity: 'high', confidence: 'likely',
|
|
9
|
+
title: 'No file type validation on upload',
|
|
10
|
+
check({ files }) {
|
|
11
|
+
const findings = [];
|
|
12
|
+
for (const [fp, content] of files) {
|
|
13
|
+
if (!isJS(fp) || isTest(fp)) continue;
|
|
14
|
+
if (content.includes('multer') || content.includes('formidable') || content.includes('busboy')) {
|
|
15
|
+
if (!content.includes('fileFilter') && !content.includes('mimetype') && !content.includes('mimeType') &&
|
|
16
|
+
!content.includes('allowedTypes') && !content.includes('accept')) {
|
|
17
|
+
findings.push({
|
|
18
|
+
ruleId: 'SEC-UPL-001', category: 'security', severity: 'high',
|
|
19
|
+
title: 'File uploads without MIME type or extension validation',
|
|
20
|
+
description: 'Validate file types to prevent malicious file uploads.',
|
|
21
|
+
file: fp, fix: null,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return findings;
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
// SEC-UPL-002
|
|
31
|
+
{
|
|
32
|
+
id: 'SEC-UPL-002', category: 'security', severity: 'high', confidence: 'likely',
|
|
33
|
+
title: 'No file size limit on upload',
|
|
34
|
+
check({ files }) {
|
|
35
|
+
const findings = [];
|
|
36
|
+
for (const [fp, content] of files) {
|
|
37
|
+
if (!isJS(fp) || isTest(fp)) continue;
|
|
38
|
+
if (content.includes('multer') || content.includes('formidable') || content.includes('busboy')) {
|
|
39
|
+
if (!content.includes('limits') && !content.includes('maxFileSize') && !content.includes('fileSize') && !content.includes('maxSize')) {
|
|
40
|
+
findings.push({
|
|
41
|
+
ruleId: 'SEC-UPL-002', category: 'security', severity: 'high',
|
|
42
|
+
title: 'File uploads without size limit — DoS via large file uploads',
|
|
43
|
+
file: fp, fix: null,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return findings;
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// SEC-UPL-003
|
|
53
|
+
{
|
|
54
|
+
id: 'SEC-UPL-003', category: 'security', severity: 'high', confidence: 'likely',
|
|
55
|
+
title: 'Files stored in web-accessible directory',
|
|
56
|
+
check({ files }) {
|
|
57
|
+
const findings = [];
|
|
58
|
+
for (const [fp, content] of files) {
|
|
59
|
+
if (!isJS(fp) || isTest(fp)) continue;
|
|
60
|
+
if (content.match(/destination\s*:\s*['"](?:\.\/)?(?:public|static|uploads)(?:\/|['"])/)) {
|
|
61
|
+
findings.push({
|
|
62
|
+
ruleId: 'SEC-UPL-003', category: 'security', severity: 'high',
|
|
63
|
+
title: 'Uploaded files stored in publicly accessible directory',
|
|
64
|
+
description: 'Store uploads outside webroot and serve through a controller with auth.',
|
|
65
|
+
file: fp, fix: null,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return findings;
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
// SEC-UPL-004
|
|
74
|
+
{
|
|
75
|
+
id: 'SEC-UPL-004', category: 'security', severity: 'medium', confidence: 'likely',
|
|
76
|
+
title: 'Original filename used for storage',
|
|
77
|
+
check({ files }) {
|
|
78
|
+
const findings = [];
|
|
79
|
+
for (const [fp, content] of files) {
|
|
80
|
+
if (!isJS(fp) || isTest(fp)) continue;
|
|
81
|
+
if (content.match(/(?:file|req\.file)\.originalname/) || content.match(/(?:file|req\.file)\.name/)) {
|
|
82
|
+
if (!content.includes('sanitize') && !content.includes('uuid') && !content.includes('crypto.random')) {
|
|
83
|
+
findings.push({
|
|
84
|
+
ruleId: 'SEC-UPL-004', category: 'security', severity: 'medium',
|
|
85
|
+
title: 'Using user-provided filename for storage — path traversal risk',
|
|
86
|
+
description: 'Generate a random filename (UUID) instead of using the original.',
|
|
87
|
+
file: fp, fix: null,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return findings;
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
// SEC-UPL-005
|
|
97
|
+
{
|
|
98
|
+
id: 'SEC-UPL-005', category: 'security', severity: 'critical', confidence: 'definite',
|
|
99
|
+
title: 'Executable file upload allowed',
|
|
100
|
+
check({ files }) {
|
|
101
|
+
const findings = [];
|
|
102
|
+
for (const [fp, content] of files) {
|
|
103
|
+
if (!isJS(fp) || isTest(fp)) continue;
|
|
104
|
+
if (content.includes('multer') || content.includes('upload')) {
|
|
105
|
+
// If there's a fileFilter, check if it blocks executables
|
|
106
|
+
if (content.includes('fileFilter')) {
|
|
107
|
+
if (!content.match(/(?:exe|sh|bat|cmd|php|jsp|asp|py|rb|pl)\b/i)) {
|
|
108
|
+
// File filter exists but doesn't seem to block executables
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Check for accept-all patterns
|
|
112
|
+
if (content.match(/fileFilter.*return\s+cb\s*\(\s*null\s*,\s*true\s*\)/)) {
|
|
113
|
+
findings.push({
|
|
114
|
+
ruleId: 'SEC-UPL-005', category: 'security', severity: 'critical',
|
|
115
|
+
title: 'File upload accepts all file types including executables',
|
|
116
|
+
file: fp, fix: null,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return findings;
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
// SEC-UPL-006
|
|
126
|
+
{
|
|
127
|
+
id: 'SEC-UPL-006', category: 'security', severity: 'medium', confidence: 'likely',
|
|
128
|
+
title: 'No antivirus scanning on uploads',
|
|
129
|
+
check({ files, stack }) {
|
|
130
|
+
const findings = [];
|
|
131
|
+
const hasUpload = [...files.values()].some(c =>
|
|
132
|
+
c.includes('multer') || c.includes('formidable') || c.includes('busboy')
|
|
133
|
+
);
|
|
134
|
+
if (!hasUpload) return findings;
|
|
135
|
+
|
|
136
|
+
const hasAV = Object.keys(stack.dependencies || {}).some(d =>
|
|
137
|
+
['clamav', 'clamscan', 'node-clam', 'virus-scan'].includes(d)
|
|
138
|
+
);
|
|
139
|
+
const hasAVCode = [...files.values()].some(c =>
|
|
140
|
+
c.includes('clamav') || c.includes('clamscan') || c.includes('virusScan')
|
|
141
|
+
);
|
|
142
|
+
// If an explicit MIME type allowlist check is present in the upload handler, skip AV warning
|
|
143
|
+
// Detect: content has MIME type strings + an .includes(mimetype) check (may span lines)
|
|
144
|
+
const hasMimeAllowlist = [...files.values()].some(c =>
|
|
145
|
+
(c.includes('multer') || c.includes('formidable') || c.includes('busboy')) &&
|
|
146
|
+
/['"]\w+\/[\w.+-]+['"]/.test(c) &&
|
|
147
|
+
/\.includes\s*\(\s*(?:file|req\.file|f)\.mimetype|mimetype\s*===\s*['"]/.test(c)
|
|
148
|
+
);
|
|
149
|
+
if (!hasAV && !hasAVCode && !hasMimeAllowlist) {
|
|
150
|
+
findings.push({
|
|
151
|
+
ruleId: 'SEC-UPL-006', category: 'security', severity: 'medium',
|
|
152
|
+
title: 'Uploaded files not scanned for malware — consider ClamAV integration',
|
|
153
|
+
fix: null,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return findings;
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
// SEC-UPL-007
|
|
161
|
+
{
|
|
162
|
+
id: 'SEC-UPL-007', category: 'security', severity: 'medium', confidence: 'likely',
|
|
163
|
+
title: 'Image upload without re-encoding',
|
|
164
|
+
check({ files, stack }) {
|
|
165
|
+
const findings = [];
|
|
166
|
+
const hasImageUpload = [...files.values()].some(c =>
|
|
167
|
+
c.match(/(?:image|img|photo|avatar|thumbnail).*(?:upload|multer)/i)
|
|
168
|
+
);
|
|
169
|
+
if (!hasImageUpload) return findings;
|
|
170
|
+
|
|
171
|
+
const hasImageProcessing = Object.keys(stack.dependencies || {}).some(d =>
|
|
172
|
+
['sharp', 'jimp', 'gm', 'imagemagick', 'canvas'].includes(d)
|
|
173
|
+
);
|
|
174
|
+
if (!hasImageProcessing) {
|
|
175
|
+
findings.push({
|
|
176
|
+
ruleId: 'SEC-UPL-007', category: 'security', severity: 'medium',
|
|
177
|
+
title: 'Image uploads served without re-encoding — may contain embedded scripts',
|
|
178
|
+
description: 'Use sharp or similar to re-encode images before serving.',
|
|
179
|
+
fix: null,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return findings;
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
export default rules;
|