@sun-asterisk/sunlint 1.3.18 → 1.3.19
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/config/rules/enhanced-rules-registry.json +77 -18
- package/core/cli-program.js +2 -1
- package/core/github-annotate-service.js +89 -0
- package/core/output-service.js +25 -0
- package/core/summary-report-service.js +30 -30
- package/package.json +3 -2
- package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +392 -280
- package/rules/common/C017_constructor_logic/analyzer.js +137 -503
- package/rules/common/C017_constructor_logic/config.json +50 -0
- package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +463 -0
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +463 -21
- package/rules/security/S011_secure_guid_generation/README.md +255 -0
- package/rules/security/S011_secure_guid_generation/analyzer.js +135 -0
- package/rules/security/S011_secure_guid_generation/config.json +56 -0
- package/rules/security/S011_secure_guid_generation/symbol-based-analyzer.js +609 -0
- package/rules/security/S028_file_upload_size_limits/README.md +537 -0
- package/rules/security/S028_file_upload_size_limits/analyzer.js +202 -0
- package/rules/security/S028_file_upload_size_limits/config.json +186 -0
- package/rules/security/S028_file_upload_size_limits/symbol-based-analyzer.js +530 -0
- package/rules/security/S041_session_token_invalidation/README.md +303 -0
- package/rules/security/S041_session_token_invalidation/analyzer.js +242 -0
- package/rules/security/S041_session_token_invalidation/config.json +175 -0
- package/rules/security/S041_session_token_invalidation/regex-based-analyzer.js +411 -0
- package/rules/security/S041_session_token_invalidation/symbol-based-analyzer.js +674 -0
- package/rules/security/S044_re_authentication_required/README.md +136 -0
- package/rules/security/S044_re_authentication_required/analyzer.js +242 -0
- package/rules/security/S044_re_authentication_required/config.json +161 -0
- package/rules/security/S044_re_authentication_required/regex-based-analyzer.js +329 -0
- package/rules/security/S044_re_authentication_required/symbol-based-analyzer.js +537 -0
- package/rules/security/S045_brute_force_protection/README.md +345 -0
- package/rules/security/S045_brute_force_protection/analyzer.js +336 -0
- package/rules/security/S045_brute_force_protection/config.json +139 -0
- package/rules/security/S045_brute_force_protection/symbol-based-analyzer.js +646 -0
- package/rules/common/C017_constructor_logic/semantic-analyzer.js +0 -340
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S028 - Limit upload file size and number of files (Symbol-based Analyzer)
|
|
3
|
+
*
|
|
4
|
+
* Detects file upload configurations without proper size and quantity limits.
|
|
5
|
+
* Prevents DoS attacks and resource exhaustion.
|
|
6
|
+
*
|
|
7
|
+
* Based on:
|
|
8
|
+
* - OWASP A04:2021 - Insecure Design
|
|
9
|
+
* - CWE-400: Uncontrolled Resource Consumption
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { Project, SyntaxKind } = require("ts-morph");
|
|
13
|
+
|
|
14
|
+
class S028SymbolBasedAnalyzer {
|
|
15
|
+
constructor(semanticEngine = null) {
|
|
16
|
+
this.ruleId = "S028";
|
|
17
|
+
this.semanticEngine = semanticEngine;
|
|
18
|
+
this.verbose = process.env.SUNLINT_DEBUG || false;
|
|
19
|
+
|
|
20
|
+
// Recommended limits
|
|
21
|
+
this.maxFileSize = 10 * 1024 * 1024; // 10MB
|
|
22
|
+
this.maxFiles = 10;
|
|
23
|
+
this.highRiskThreshold = 50 * 1024 * 1024; // 50MB
|
|
24
|
+
this.mediumRiskThreshold = 20 * 1024 * 1024; // 20MB
|
|
25
|
+
|
|
26
|
+
// Framework patterns
|
|
27
|
+
this.multerPatterns = ["multer(", "multer({"];
|
|
28
|
+
this.fileInterceptorPatterns = [
|
|
29
|
+
"FileInterceptor",
|
|
30
|
+
"FilesInterceptor",
|
|
31
|
+
"FileFieldsInterceptor",
|
|
32
|
+
];
|
|
33
|
+
this.expressPatterns = ["express.json", "express.urlencoded"];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async initialize(semanticEngine = null) {
|
|
37
|
+
if (semanticEngine) {
|
|
38
|
+
this.semanticEngine = semanticEngine;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Main analyze method
|
|
44
|
+
*/
|
|
45
|
+
async analyze(sourceFile, filePath) {
|
|
46
|
+
const violations = [];
|
|
47
|
+
const reportedLines = new Set(); // Prevent duplicates
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// Create ts-morph project if not already a SourceFile
|
|
51
|
+
let tsSourceFile = sourceFile;
|
|
52
|
+
if (typeof sourceFile === "string") {
|
|
53
|
+
const project = new Project({ useInMemoryFileSystem: true });
|
|
54
|
+
tsSourceFile = project.createSourceFile(filePath, sourceFile);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check different upload patterns
|
|
58
|
+
this.checkMulterConfiguration(
|
|
59
|
+
tsSourceFile,
|
|
60
|
+
filePath,
|
|
61
|
+
violations,
|
|
62
|
+
reportedLines
|
|
63
|
+
);
|
|
64
|
+
this.checkFileInterceptor(
|
|
65
|
+
tsSourceFile,
|
|
66
|
+
filePath,
|
|
67
|
+
violations,
|
|
68
|
+
reportedLines
|
|
69
|
+
);
|
|
70
|
+
this.checkExpressMiddleware(
|
|
71
|
+
tsSourceFile,
|
|
72
|
+
filePath,
|
|
73
|
+
violations,
|
|
74
|
+
reportedLines
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (this.verbose) {
|
|
78
|
+
console.log(
|
|
79
|
+
`🔍 [${this.ruleId}] File: ${filePath} - Found ${violations.length} violations`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (this.verbose) {
|
|
84
|
+
console.error(
|
|
85
|
+
`❌ [${this.ruleId}] Error analyzing ${filePath}:`,
|
|
86
|
+
error.message
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return violations;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check multer() configuration
|
|
96
|
+
* Pattern: multer({ dest, storage, limits, fileFilter })
|
|
97
|
+
*/
|
|
98
|
+
checkMulterConfiguration(sourceFile, filePath, violations, reportedLines) {
|
|
99
|
+
try {
|
|
100
|
+
const callExpressions = sourceFile.getDescendantsOfKind(
|
|
101
|
+
SyntaxKind.CallExpression
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
callExpressions.forEach((callExpr) => {
|
|
105
|
+
const exprText = callExpr.getExpression().getText();
|
|
106
|
+
|
|
107
|
+
// Check if it's multer call
|
|
108
|
+
if (!exprText.includes("multer")) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const args = callExpr.getArguments();
|
|
113
|
+
if (args.length === 0) {
|
|
114
|
+
// multer() without arguments
|
|
115
|
+
const line = callExpr.getStartLineNumber();
|
|
116
|
+
if (reportedLines.has(line)) return;
|
|
117
|
+
|
|
118
|
+
violations.push({
|
|
119
|
+
ruleId: this.ruleId,
|
|
120
|
+
source: filePath,
|
|
121
|
+
filePath: filePath,
|
|
122
|
+
file: filePath,
|
|
123
|
+
line: line,
|
|
124
|
+
column: callExpr.getStartLinePos(),
|
|
125
|
+
message:
|
|
126
|
+
"Multer configuration missing size limits - add limits.fileSize and limits.files to prevent DoS attacks",
|
|
127
|
+
severity: "error",
|
|
128
|
+
category: "security",
|
|
129
|
+
});
|
|
130
|
+
reportedLines.add(line);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check if config object has limits
|
|
135
|
+
const configArg = args[0];
|
|
136
|
+
if (!configArg) return;
|
|
137
|
+
|
|
138
|
+
const configText = configArg.getText();
|
|
139
|
+
|
|
140
|
+
// Check if limits object exists
|
|
141
|
+
if (!configText.includes("limits")) {
|
|
142
|
+
const line = callExpr.getStartLineNumber();
|
|
143
|
+
if (reportedLines.has(line)) return;
|
|
144
|
+
|
|
145
|
+
violations.push({
|
|
146
|
+
ruleId: this.ruleId,
|
|
147
|
+
source: filePath,
|
|
148
|
+
filePath: filePath,
|
|
149
|
+
file: filePath,
|
|
150
|
+
line: line,
|
|
151
|
+
column: callExpr.getStartLinePos(),
|
|
152
|
+
message:
|
|
153
|
+
"Multer configuration missing 'limits' object - add { limits: { fileSize: 10485760, files: 10 } }",
|
|
154
|
+
severity: "error",
|
|
155
|
+
category: "security",
|
|
156
|
+
});
|
|
157
|
+
reportedLines.add(line);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check if limits.fileSize exists
|
|
162
|
+
if (!configText.includes("fileSize")) {
|
|
163
|
+
const line = callExpr.getStartLineNumber();
|
|
164
|
+
if (reportedLines.has(line)) return;
|
|
165
|
+
|
|
166
|
+
violations.push({
|
|
167
|
+
ruleId: this.ruleId,
|
|
168
|
+
source: filePath,
|
|
169
|
+
filePath: filePath,
|
|
170
|
+
file: filePath,
|
|
171
|
+
line: line,
|
|
172
|
+
column: callExpr.getStartLinePos(),
|
|
173
|
+
message:
|
|
174
|
+
"Multer limits missing 'fileSize' - add limits.fileSize to prevent large file uploads (recommend ≤ 10MB)",
|
|
175
|
+
severity: "error",
|
|
176
|
+
category: "security",
|
|
177
|
+
});
|
|
178
|
+
reportedLines.add(line);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
183
|
+
console.log(
|
|
184
|
+
`🔍 [S028] Line ${callExpr.getStartLineNumber()}: Has fileSize, checking threshold...`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Validate fileSize threshold - try to evaluate expression
|
|
189
|
+
let fileSize = null;
|
|
190
|
+
|
|
191
|
+
// Try to evaluate expression first (handles both "100" and "100 * 1024 * 1024")
|
|
192
|
+
const expressionMatch = configText.match(/fileSize\s*:\s*([^,}]+)/);
|
|
193
|
+
if (expressionMatch) {
|
|
194
|
+
const expression = expressionMatch[1]
|
|
195
|
+
.trim()
|
|
196
|
+
.replace(/\/\/.*/g, "")
|
|
197
|
+
.trim();
|
|
198
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
199
|
+
console.log(
|
|
200
|
+
`🔍 [S028] Line ${callExpr.getStartLineNumber()}: Evaluating expression: "${expression}"`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
// Safe evaluation of numeric expressions only
|
|
205
|
+
fileSize = eval(expression);
|
|
206
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
207
|
+
console.log(
|
|
208
|
+
`🔍 [S028] Evaluated fileSize: ${fileSize} bytes = ${(
|
|
209
|
+
fileSize /
|
|
210
|
+
(1024 * 1024)
|
|
211
|
+
).toFixed(0)}MB`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
} catch (e) {
|
|
215
|
+
// If eval fails, skip threshold check
|
|
216
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
217
|
+
console.log(`🔍 [S028] Eval failed: ${e.message}`);
|
|
218
|
+
}
|
|
219
|
+
fileSize = null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (fileSize && !isNaN(fileSize)) {
|
|
224
|
+
if (fileSize > this.highRiskThreshold) {
|
|
225
|
+
const line = callExpr.getStartLineNumber();
|
|
226
|
+
if (reportedLines.has(line)) return;
|
|
227
|
+
|
|
228
|
+
const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(0);
|
|
229
|
+
violations.push({
|
|
230
|
+
ruleId: this.ruleId,
|
|
231
|
+
source: filePath,
|
|
232
|
+
filePath: filePath,
|
|
233
|
+
file: filePath,
|
|
234
|
+
line: line,
|
|
235
|
+
column: callExpr.getStartLinePos(),
|
|
236
|
+
message: `File size limit too high (${fileSizeMB}MB) - recommend ≤ 10MB to prevent DoS attacks`,
|
|
237
|
+
severity: "error",
|
|
238
|
+
category: "security",
|
|
239
|
+
});
|
|
240
|
+
reportedLines.add(line);
|
|
241
|
+
} else if (fileSize > this.mediumRiskThreshold) {
|
|
242
|
+
const line = callExpr.getStartLineNumber();
|
|
243
|
+
if (reportedLines.has(line)) return;
|
|
244
|
+
|
|
245
|
+
const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(0);
|
|
246
|
+
violations.push({
|
|
247
|
+
ruleId: this.ruleId,
|
|
248
|
+
source: filePath,
|
|
249
|
+
filePath: filePath,
|
|
250
|
+
file: filePath,
|
|
251
|
+
line: line,
|
|
252
|
+
column: callExpr.getStartLinePos(),
|
|
253
|
+
message: `File size limit (${fileSizeMB}MB) exceeds recommended threshold - consider reducing to ≤ 10MB`,
|
|
254
|
+
severity: "warning",
|
|
255
|
+
category: "security",
|
|
256
|
+
});
|
|
257
|
+
reportedLines.add(line);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Check if limits.files exists (optional but recommended)
|
|
262
|
+
if (!configText.includes("files")) {
|
|
263
|
+
const line = callExpr.getStartLineNumber();
|
|
264
|
+
// Don't report if already reported fileSize issue
|
|
265
|
+
if (reportedLines.has(line)) return;
|
|
266
|
+
|
|
267
|
+
// This is a warning, not error (files limit is optional)
|
|
268
|
+
violations.push({
|
|
269
|
+
ruleId: this.ruleId,
|
|
270
|
+
source: filePath,
|
|
271
|
+
filePath: filePath,
|
|
272
|
+
file: filePath,
|
|
273
|
+
line: line,
|
|
274
|
+
column: callExpr.getStartLinePos(),
|
|
275
|
+
message:
|
|
276
|
+
"Multer limits missing 'files' count - consider adding limits.files to prevent excessive uploads (recommend ≤ 10)",
|
|
277
|
+
severity: "warning",
|
|
278
|
+
category: "security",
|
|
279
|
+
});
|
|
280
|
+
reportedLines.add(line);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (this.verbose) {
|
|
285
|
+
console.error(
|
|
286
|
+
`❌ [${this.ruleId}] Error checking multer:`,
|
|
287
|
+
error.message
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Check @UseInterceptors(FileInterceptor(...))
|
|
295
|
+
* Pattern: @UseInterceptors(FileInterceptor('file', { limits: {...} }))
|
|
296
|
+
*/
|
|
297
|
+
checkFileInterceptor(sourceFile, filePath, violations, reportedLines) {
|
|
298
|
+
try {
|
|
299
|
+
const decorators = sourceFile.getDescendantsOfKind(SyntaxKind.Decorator);
|
|
300
|
+
|
|
301
|
+
decorators.forEach((decorator) => {
|
|
302
|
+
const decoratorText = decorator.getText();
|
|
303
|
+
|
|
304
|
+
// Check if it's FileInterceptor decorator
|
|
305
|
+
const isFileInterceptor = this.fileInterceptorPatterns.some((pattern) =>
|
|
306
|
+
decoratorText.includes(pattern)
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
if (!isFileInterceptor) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Check if it has limits configuration
|
|
314
|
+
if (!decoratorText.includes("limits")) {
|
|
315
|
+
const line = decorator.getStartLineNumber();
|
|
316
|
+
if (reportedLines.has(line)) return;
|
|
317
|
+
|
|
318
|
+
const interceptorType = decoratorText.includes("FilesInterceptor")
|
|
319
|
+
? "FilesInterceptor"
|
|
320
|
+
: decoratorText.includes("FileFieldsInterceptor")
|
|
321
|
+
? "FileFieldsInterceptor"
|
|
322
|
+
: "FileInterceptor";
|
|
323
|
+
|
|
324
|
+
violations.push({
|
|
325
|
+
ruleId: this.ruleId,
|
|
326
|
+
source: filePath,
|
|
327
|
+
filePath: filePath,
|
|
328
|
+
file: filePath,
|
|
329
|
+
line: line,
|
|
330
|
+
column: decorator.getStartLinePos(),
|
|
331
|
+
message: `${interceptorType} missing 'limits' configuration - add { limits: { fileSize: 10485760 } } to prevent large uploads`,
|
|
332
|
+
severity: "error",
|
|
333
|
+
category: "security",
|
|
334
|
+
});
|
|
335
|
+
reportedLines.add(line);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Check if limits.fileSize exists
|
|
340
|
+
if (!decoratorText.includes("fileSize")) {
|
|
341
|
+
const line = decorator.getStartLineNumber();
|
|
342
|
+
if (reportedLines.has(line)) return;
|
|
343
|
+
|
|
344
|
+
violations.push({
|
|
345
|
+
ruleId: this.ruleId,
|
|
346
|
+
source: filePath,
|
|
347
|
+
filePath: filePath,
|
|
348
|
+
file: filePath,
|
|
349
|
+
line: line,
|
|
350
|
+
column: decorator.getStartLinePos(),
|
|
351
|
+
message:
|
|
352
|
+
"FileInterceptor limits missing 'fileSize' - add limits.fileSize to prevent large file uploads (recommend ≤ 10MB)",
|
|
353
|
+
severity: "error",
|
|
354
|
+
category: "security",
|
|
355
|
+
});
|
|
356
|
+
reportedLines.add(line);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Validate fileSize threshold - try to evaluate expression
|
|
361
|
+
let fileSize = null;
|
|
362
|
+
|
|
363
|
+
// Try to evaluate expression first (handles both "100" and "100 * 1024 * 1024")
|
|
364
|
+
const expressionMatch = decoratorText.match(/fileSize\s*:\s*([^,}]+)/);
|
|
365
|
+
if (expressionMatch) {
|
|
366
|
+
const expression = expressionMatch[1]
|
|
367
|
+
.trim()
|
|
368
|
+
.replace(/\/\/.*/g, "")
|
|
369
|
+
.trim();
|
|
370
|
+
try {
|
|
371
|
+
// Safe evaluation of numeric expressions only
|
|
372
|
+
fileSize = eval(expression);
|
|
373
|
+
} catch (e) {
|
|
374
|
+
// If eval fails, skip threshold check
|
|
375
|
+
fileSize = null;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (fileSize && !isNaN(fileSize)) {
|
|
380
|
+
if (fileSize > this.highRiskThreshold) {
|
|
381
|
+
const line = decorator.getStartLineNumber();
|
|
382
|
+
if (reportedLines.has(line)) return;
|
|
383
|
+
|
|
384
|
+
const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(0);
|
|
385
|
+
violations.push({
|
|
386
|
+
ruleId: this.ruleId,
|
|
387
|
+
source: filePath,
|
|
388
|
+
filePath: filePath,
|
|
389
|
+
file: filePath,
|
|
390
|
+
line: line,
|
|
391
|
+
column: decorator.getStartLinePos(),
|
|
392
|
+
message: `File size limit too high (${fileSizeMB}MB) - recommend ≤ 10MB to prevent DoS attacks`,
|
|
393
|
+
severity: "error",
|
|
394
|
+
category: "security",
|
|
395
|
+
});
|
|
396
|
+
reportedLines.add(line);
|
|
397
|
+
} else if (fileSize > this.mediumRiskThreshold) {
|
|
398
|
+
const line = decorator.getStartLineNumber();
|
|
399
|
+
if (reportedLines.has(line)) return;
|
|
400
|
+
|
|
401
|
+
const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(0);
|
|
402
|
+
violations.push({
|
|
403
|
+
ruleId: this.ruleId,
|
|
404
|
+
source: filePath,
|
|
405
|
+
filePath: filePath,
|
|
406
|
+
file: filePath,
|
|
407
|
+
line: line,
|
|
408
|
+
column: decorator.getStartLinePos(),
|
|
409
|
+
message: `File size limit (${fileSizeMB}MB) exceeds recommended threshold - consider reducing to ≤ 10MB`,
|
|
410
|
+
severity: "warning",
|
|
411
|
+
category: "security",
|
|
412
|
+
});
|
|
413
|
+
reportedLines.add(line);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if (this.verbose) {
|
|
419
|
+
console.error(
|
|
420
|
+
`❌ [${this.ruleId}] Error checking FileInterceptor:`,
|
|
421
|
+
error.message
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Check Express middleware: express.json() and express.urlencoded()
|
|
429
|
+
* Pattern: app.use(express.json({ limit: '10mb' }))
|
|
430
|
+
*/
|
|
431
|
+
checkExpressMiddleware(sourceFile, filePath, violations, reportedLines) {
|
|
432
|
+
try {
|
|
433
|
+
const callExpressions = sourceFile.getDescendantsOfKind(
|
|
434
|
+
SyntaxKind.CallExpression
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
callExpressions.forEach((callExpr) => {
|
|
438
|
+
const exprText = callExpr.getExpression().getText();
|
|
439
|
+
|
|
440
|
+
// Check if it's express.json or express.urlencoded
|
|
441
|
+
const isExpressJson = exprText.includes("express.json");
|
|
442
|
+
const isExpressUrlencoded = exprText.includes("express.urlencoded");
|
|
443
|
+
|
|
444
|
+
if (!isExpressJson && !isExpressUrlencoded) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const args = callExpr.getArguments();
|
|
449
|
+
|
|
450
|
+
// Check if it has limit configuration
|
|
451
|
+
const hasLimitConfig =
|
|
452
|
+
args.length > 0 && args[0].getText().includes("limit");
|
|
453
|
+
|
|
454
|
+
if (!hasLimitConfig) {
|
|
455
|
+
const line = callExpr.getStartLineNumber();
|
|
456
|
+
if (reportedLines.has(line)) return;
|
|
457
|
+
|
|
458
|
+
const methodName = isExpressJson
|
|
459
|
+
? "express.json()"
|
|
460
|
+
: "express.urlencoded()";
|
|
461
|
+
violations.push({
|
|
462
|
+
ruleId: this.ruleId,
|
|
463
|
+
source: filePath,
|
|
464
|
+
filePath: filePath,
|
|
465
|
+
file: filePath,
|
|
466
|
+
line: line,
|
|
467
|
+
column: callExpr.getStartLinePos(),
|
|
468
|
+
message: `${methodName} missing body size limit - add { limit: '10mb' } to prevent large payload attacks`,
|
|
469
|
+
severity: "warning",
|
|
470
|
+
category: "security",
|
|
471
|
+
});
|
|
472
|
+
reportedLines.add(line);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Validate limit threshold
|
|
477
|
+
const configText = args[0].getText();
|
|
478
|
+
const limitMatch = configText.match(
|
|
479
|
+
/limit\s*:\s*['"](\d+)(mb|kb|gb)?['"]/i
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
if (limitMatch) {
|
|
483
|
+
const limitValue = parseInt(limitMatch[1], 10);
|
|
484
|
+
const limitUnit = (limitMatch[2] || "").toLowerCase();
|
|
485
|
+
|
|
486
|
+
let limitBytes = limitValue;
|
|
487
|
+
if (limitUnit === "kb") {
|
|
488
|
+
limitBytes = limitValue * 1024;
|
|
489
|
+
} else if (limitUnit === "mb") {
|
|
490
|
+
limitBytes = limitValue * 1024 * 1024;
|
|
491
|
+
} else if (limitUnit === "gb") {
|
|
492
|
+
limitBytes = limitValue * 1024 * 1024 * 1024;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (limitBytes > this.mediumRiskThreshold) {
|
|
496
|
+
const line = callExpr.getStartLineNumber();
|
|
497
|
+
if (reportedLines.has(line)) return;
|
|
498
|
+
|
|
499
|
+
const limitMB = (limitBytes / (1024 * 1024)).toFixed(0);
|
|
500
|
+
violations.push({
|
|
501
|
+
ruleId: this.ruleId,
|
|
502
|
+
source: filePath,
|
|
503
|
+
filePath: filePath,
|
|
504
|
+
file: filePath,
|
|
505
|
+
line: line,
|
|
506
|
+
column: callExpr.getStartLinePos(),
|
|
507
|
+
message: `Body size limit (${limitMB}MB) exceeds recommended threshold - consider reducing to ≤ 10MB`,
|
|
508
|
+
severity: "warning",
|
|
509
|
+
category: "security",
|
|
510
|
+
});
|
|
511
|
+
reportedLines.add(line);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
} catch (error) {
|
|
516
|
+
if (this.verbose) {
|
|
517
|
+
console.error(
|
|
518
|
+
`❌ [${this.ruleId}] Error checking Express middleware:`,
|
|
519
|
+
error.message
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async cleanup() {
|
|
526
|
+
// Cleanup if needed
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
module.exports = S028SymbolBasedAnalyzer;
|