@sun-asterisk/sunlint 1.3.4 → 1.3.6
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/CHANGELOG.md +62 -0
- package/config/presets/all.json +49 -48
- package/config/presets/beginner.json +7 -18
- package/config/presets/ci.json +63 -27
- package/config/presets/maintainability.json +6 -4
- package/config/presets/performance.json +4 -3
- package/config/presets/quality.json +11 -50
- package/config/presets/recommended.json +83 -10
- package/config/presets/security.json +20 -19
- package/config/presets/strict.json +6 -13
- package/config/rule-analysis-strategies.js +5 -0
- package/config/rules/enhanced-rules-registry.json +87 -7
- package/core/config-preset-resolver.js +7 -2
- package/package.json +1 -1
- package/rules/common/C067_no_hardcoded_config/analyzer.js +95 -0
- package/rules/common/C067_no_hardcoded_config/config.json +81 -0
- package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +1034 -0
- package/rules/common/C070_no_real_time_tests/analyzer.js +320 -0
- package/rules/common/C070_no_real_time_tests/config.json +78 -0
- package/rules/common/C070_no_real_time_tests/regex-analyzer.js +424 -0
- package/rules/security/S024_xpath_xxe_protection/analyzer.js +242 -0
- package/rules/security/S024_xpath_xxe_protection/config.json +152 -0
- package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +338 -0
- package/rules/security/S024_xpath_xxe_protection/symbol-based-analyzer.js +474 -0
- package/rules/security/S025_server_side_validation/README.md +179 -0
- package/rules/security/S025_server_side_validation/analyzer.js +242 -0
- package/rules/security/S025_server_side_validation/config.json +111 -0
- package/rules/security/S025_server_side_validation/regex-based-analyzer.js +388 -0
- package/rules/security/S025_server_side_validation/symbol-based-analyzer.js +523 -0
- package/scripts/README.md +83 -0
- package/scripts/analyze-core-rules.js +151 -0
- package/scripts/generate-presets.js +202 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S024 Symbol-Based Analyzer - Protect against XPath Injection and XML External Entity (XXE)
|
|
3
|
+
* Uses TypeScript compiler API for semantic analysis
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const ts = require("typescript");
|
|
7
|
+
|
|
8
|
+
class S024SymbolBasedAnalyzer {
|
|
9
|
+
constructor(semanticEngine = null) {
|
|
10
|
+
this.semanticEngine = semanticEngine;
|
|
11
|
+
this.ruleId = "S024";
|
|
12
|
+
this.category = "security";
|
|
13
|
+
|
|
14
|
+
// XPath-related method names that can be vulnerable
|
|
15
|
+
this.xpathMethods = [
|
|
16
|
+
"evaluate",
|
|
17
|
+
"select",
|
|
18
|
+
"selectText",
|
|
19
|
+
"selectValue",
|
|
20
|
+
"selectNodes",
|
|
21
|
+
"selectSingleNode"
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// XML parsing methods that can have XXE vulnerabilities
|
|
25
|
+
this.xmlParsingMethods = [
|
|
26
|
+
"parseString",
|
|
27
|
+
"parseXml",
|
|
28
|
+
"parseFromString",
|
|
29
|
+
"parse",
|
|
30
|
+
"parseXmlString",
|
|
31
|
+
"transform"
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// XML parser constructors that need XXE protection
|
|
35
|
+
this.xmlParserConstructors = [
|
|
36
|
+
"DOMParser",
|
|
37
|
+
"XSLTProcessor",
|
|
38
|
+
"SAXParser"
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// User input sources that could lead to injection
|
|
42
|
+
this.userInputSources = [
|
|
43
|
+
"req",
|
|
44
|
+
"request",
|
|
45
|
+
"params",
|
|
46
|
+
"query",
|
|
47
|
+
"body",
|
|
48
|
+
"headers",
|
|
49
|
+
"cookies"
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
// Secure XPath/XML patterns
|
|
53
|
+
this.securePatterns = [
|
|
54
|
+
"parameterized",
|
|
55
|
+
"escaped",
|
|
56
|
+
"sanitized",
|
|
57
|
+
"validate"
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Initialize analyzer with semantic engine
|
|
63
|
+
*/
|
|
64
|
+
async initialize(semanticEngine) {
|
|
65
|
+
this.semanticEngine = semanticEngine;
|
|
66
|
+
if (this.verbose) {
|
|
67
|
+
console.log(`🔍 [${this.ruleId}] Symbol: Semantic engine initialized`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async analyze(filePath) {
|
|
72
|
+
if (this.verbose) {
|
|
73
|
+
console.log(
|
|
74
|
+
`🔍 [${this.ruleId}] Symbol: Starting analysis for ${filePath}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!this.semanticEngine) {
|
|
79
|
+
if (this.verbose) {
|
|
80
|
+
console.log(
|
|
81
|
+
`🔍 [${this.ruleId}] Symbol: No semantic engine available, skipping`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const sourceFile = this.semanticEngine.getSourceFile(filePath);
|
|
89
|
+
if (!sourceFile) {
|
|
90
|
+
if (this.verbose) {
|
|
91
|
+
console.log(
|
|
92
|
+
`🔍 [${this.ruleId}] Symbol: No source file found, trying ts-morph fallback`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return await this.analyzeTsMorph(filePath);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (this.verbose) {
|
|
99
|
+
console.log(`🔧 [${this.ruleId}] Source file found, analyzing...`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return await this.analyzeSourceFile(sourceFile, filePath);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (this.verbose) {
|
|
105
|
+
console.log(
|
|
106
|
+
`🔍 [${this.ruleId}] Symbol: Error in analysis:`,
|
|
107
|
+
error.message
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async analyzeTsMorph(filePath) {
|
|
115
|
+
try {
|
|
116
|
+
if (this.verbose) {
|
|
117
|
+
console.log(`🔍 [${this.ruleId}] Symbol: Starting ts-morph analysis`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const { Project } = require("ts-morph");
|
|
121
|
+
const project = new Project();
|
|
122
|
+
const sourceFile = project.addSourceFileAtPath(filePath);
|
|
123
|
+
|
|
124
|
+
return await this.analyzeSourceFile(sourceFile, filePath);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (this.verbose) {
|
|
127
|
+
console.log(
|
|
128
|
+
`🔍 [${this.ruleId}] Symbol: ts-morph analysis failed:`,
|
|
129
|
+
error.message
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async analyzeSourceFile(sourceFile, filePath) {
|
|
137
|
+
const violations = [];
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
if (this.verbose) {
|
|
141
|
+
console.log(`🔍 [${this.ruleId}] Symbol: Starting symbol-based analysis`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const callExpressions = sourceFile.getDescendantsOfKind
|
|
145
|
+
? sourceFile.getDescendantsOfKind(
|
|
146
|
+
require("typescript").SyntaxKind.CallExpression
|
|
147
|
+
)
|
|
148
|
+
: [];
|
|
149
|
+
|
|
150
|
+
if (this.verbose) {
|
|
151
|
+
console.log(
|
|
152
|
+
`🔍 [${this.ruleId}] Symbol: Found ${callExpressions.length} call expressions`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const callNode of callExpressions) {
|
|
157
|
+
try {
|
|
158
|
+
// Analyze XPath injection vulnerabilities
|
|
159
|
+
const xpathViolation = this.analyzeXPathCall(callNode, sourceFile);
|
|
160
|
+
if (xpathViolation) {
|
|
161
|
+
violations.push(xpathViolation);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Analyze XXE vulnerabilities
|
|
165
|
+
const xxeViolation = this.analyzeXXEVulnerability(callNode, sourceFile);
|
|
166
|
+
if (xxeViolation) {
|
|
167
|
+
violations.push(xxeViolation);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (this.verbose) {
|
|
172
|
+
console.log(
|
|
173
|
+
`🔍 [${this.ruleId}] Symbol: Error analyzing call expression:`,
|
|
174
|
+
error.message
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Also check for new expressions (constructors)
|
|
181
|
+
const newExpressions = sourceFile.getDescendantsOfKind
|
|
182
|
+
? sourceFile.getDescendantsOfKind(
|
|
183
|
+
require("typescript").SyntaxKind.NewExpression
|
|
184
|
+
)
|
|
185
|
+
: [];
|
|
186
|
+
|
|
187
|
+
for (const newNode of newExpressions) {
|
|
188
|
+
try {
|
|
189
|
+
const xxeViolation = this.analyzeXMLParserConstructor(newNode, sourceFile);
|
|
190
|
+
if (xxeViolation) {
|
|
191
|
+
violations.push(xxeViolation);
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
if (this.verbose) {
|
|
195
|
+
console.log(
|
|
196
|
+
`🔍 [${this.ruleId}] Symbol: Error analyzing new expression:`,
|
|
197
|
+
error.message
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (this.verbose) {
|
|
204
|
+
console.log(
|
|
205
|
+
`🔍 [${this.ruleId}] Symbol: Analysis completed. Found ${violations.length} violations`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return violations;
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (this.verbose) {
|
|
212
|
+
console.log(
|
|
213
|
+
`🔍 [${this.ruleId}] Symbol: Error in source file analysis:`,
|
|
214
|
+
error.message
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
analyzeXPathCall(callNode, sourceFile) {
|
|
222
|
+
try {
|
|
223
|
+
const expression = callNode.getExpression();
|
|
224
|
+
const methodName = this.getMethodName(expression);
|
|
225
|
+
|
|
226
|
+
if (!this.xpathMethods.includes(methodName)) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (this.verbose) {
|
|
231
|
+
console.log(
|
|
232
|
+
`🔍 [${this.ruleId}] Symbol: XPath method call detected: ${methodName}`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check if user input is used in XPath arguments
|
|
237
|
+
const args = callNode.getArguments();
|
|
238
|
+
if (args.length === 0) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const firstArg = args[0];
|
|
243
|
+
if (this.containsUserInput(firstArg)) {
|
|
244
|
+
return this.createViolation(
|
|
245
|
+
sourceFile,
|
|
246
|
+
callNode,
|
|
247
|
+
`XPath Injection vulnerability: User input used directly in ${methodName}() without proper sanitization`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Check for string concatenation with user input
|
|
252
|
+
if (this.containsStringConcatenationWithUserInput(firstArg)) {
|
|
253
|
+
return this.createViolation(
|
|
254
|
+
sourceFile,
|
|
255
|
+
callNode,
|
|
256
|
+
`XPath Injection vulnerability: XPath query constructed using string concatenation with user input`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return null;
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (this.verbose) {
|
|
263
|
+
console.log(
|
|
264
|
+
`🔍 [${this.ruleId}] Symbol: Error analyzing XPath call:`,
|
|
265
|
+
error.message
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
analyzeXXEVulnerability(callNode, sourceFile) {
|
|
273
|
+
try {
|
|
274
|
+
const expression = callNode.getExpression();
|
|
275
|
+
const methodName = this.getMethodName(expression);
|
|
276
|
+
|
|
277
|
+
if (!this.xmlParsingMethods.includes(methodName)) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (this.verbose) {
|
|
282
|
+
console.log(
|
|
283
|
+
`🔍 [${this.ruleId}] Symbol: XML parsing method detected: ${methodName}`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Check if XXE protection is implemented
|
|
288
|
+
const hasProtection = this.hasXXEProtectionInContext(callNode, sourceFile);
|
|
289
|
+
if (!hasProtection) {
|
|
290
|
+
return this.createViolation(
|
|
291
|
+
sourceFile,
|
|
292
|
+
callNode,
|
|
293
|
+
`XXE vulnerability: ${methodName}() used without disabling external entity processing`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return null;
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (this.verbose) {
|
|
300
|
+
console.log(
|
|
301
|
+
`🔍 [${this.ruleId}] Symbol: Error analyzing XXE vulnerability:`,
|
|
302
|
+
error.message
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
analyzeXMLParserConstructor(newNode, sourceFile) {
|
|
310
|
+
try {
|
|
311
|
+
const expression = newNode.getExpression();
|
|
312
|
+
const constructorName = expression.getText();
|
|
313
|
+
|
|
314
|
+
const isXMLParser = this.xmlParserConstructors.some(parser =>
|
|
315
|
+
constructorName.includes(parser)
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
if (!isXMLParser) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (this.verbose) {
|
|
323
|
+
console.log(
|
|
324
|
+
`🔍 [${this.ruleId}] Symbol: XML parser constructor detected: ${constructorName}`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Check if XXE protection is configured
|
|
329
|
+
const hasProtection = this.hasXXEProtectionInContext(newNode, sourceFile);
|
|
330
|
+
if (!hasProtection) {
|
|
331
|
+
return this.createViolation(
|
|
332
|
+
sourceFile,
|
|
333
|
+
newNode,
|
|
334
|
+
`XXE vulnerability: ${constructorName} instantiated without XXE protection`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return null;
|
|
339
|
+
} catch (error) {
|
|
340
|
+
if (this.verbose) {
|
|
341
|
+
console.log(
|
|
342
|
+
`🔍 [${this.ruleId}] Symbol: Error analyzing XML parser constructor:`,
|
|
343
|
+
error.message
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
containsUserInput(node) {
|
|
351
|
+
try {
|
|
352
|
+
const nodeText = node.getText();
|
|
353
|
+
return this.userInputSources.some(source =>
|
|
354
|
+
nodeText.includes(`${source}.`) || nodeText.includes(`${source}[`)
|
|
355
|
+
);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
containsStringConcatenationWithUserInput(node) {
|
|
362
|
+
try {
|
|
363
|
+
const nodeText = node.getText();
|
|
364
|
+
|
|
365
|
+
// Check for binary expressions with +
|
|
366
|
+
if (nodeText.includes('+')) {
|
|
367
|
+
return this.userInputSources.some(source =>
|
|
368
|
+
nodeText.includes(`${source}.`) || nodeText.includes(`${source}[`)
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Check for template literals with user input
|
|
373
|
+
if (nodeText.includes('`') && nodeText.includes('${')) {
|
|
374
|
+
return this.userInputSources.some(source =>
|
|
375
|
+
nodeText.includes(`\${${source}.`) || nodeText.includes(`\${${source}[`)
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return false;
|
|
380
|
+
} catch (error) {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
hasXXEProtectionInContext(node, sourceFile) {
|
|
386
|
+
try {
|
|
387
|
+
// Get the parent scope (function/method) to check for XXE protection
|
|
388
|
+
let parent = node.getParent();
|
|
389
|
+
while (parent && !this.isFunctionLike(parent)) {
|
|
390
|
+
parent = parent.getParent();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (!parent) {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const functionText = parent.getText();
|
|
398
|
+
|
|
399
|
+
// Check for XXE protection patterns
|
|
400
|
+
const protectionPatterns = [
|
|
401
|
+
/resolveExternalEntities\s*:\s*false/,
|
|
402
|
+
/setFeature.*disallow-doctype-decl.*true/,
|
|
403
|
+
/setFeature.*external-general-entities.*false/,
|
|
404
|
+
/setFeature.*external-parameter-entities.*false/,
|
|
405
|
+
/explicitChildren\s*:\s*false/,
|
|
406
|
+
/ignoreAttrs\s*:\s*true/,
|
|
407
|
+
/parseDoctype\s*:\s*false/
|
|
408
|
+
];
|
|
409
|
+
|
|
410
|
+
return protectionPatterns.some(pattern => pattern.test(functionText));
|
|
411
|
+
} catch (error) {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
isFunctionLike(node) {
|
|
417
|
+
try {
|
|
418
|
+
const SyntaxKind = require("typescript").SyntaxKind;
|
|
419
|
+
const kind = node.getKind();
|
|
420
|
+
|
|
421
|
+
return kind === SyntaxKind.FunctionDeclaration ||
|
|
422
|
+
kind === SyntaxKind.FunctionExpression ||
|
|
423
|
+
kind === SyntaxKind.ArrowFunction ||
|
|
424
|
+
kind === SyntaxKind.MethodDeclaration;
|
|
425
|
+
} catch (error) {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
getMethodName(expression) {
|
|
431
|
+
try {
|
|
432
|
+
const ts = require("typescript");
|
|
433
|
+
|
|
434
|
+
if (expression.getKind() === ts.SyntaxKind.PropertyAccessExpression) {
|
|
435
|
+
return expression.getNameNode().getText();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (expression.getKind() === ts.SyntaxKind.Identifier) {
|
|
439
|
+
return expression.getText();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return "";
|
|
443
|
+
} catch (error) {
|
|
444
|
+
return "";
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
createViolation(sourceFile, node, message) {
|
|
449
|
+
try {
|
|
450
|
+
const start = node.getStart();
|
|
451
|
+
const lineAndChar = sourceFile.getLineAndColumnAtPos(start);
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
rule: this.ruleId,
|
|
455
|
+
source: sourceFile.getFilePath(),
|
|
456
|
+
category: this.category,
|
|
457
|
+
line: lineAndChar.line,
|
|
458
|
+
column: lineAndChar.column,
|
|
459
|
+
message: message,
|
|
460
|
+
severity: "error",
|
|
461
|
+
};
|
|
462
|
+
} catch (error) {
|
|
463
|
+
if (this.verbose) {
|
|
464
|
+
console.log(
|
|
465
|
+
`🔍 [${this.ruleId}] Symbol: Error creating violation:`,
|
|
466
|
+
error.message
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
module.exports = S024SymbolBasedAnalyzer;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# S025 - Always validate client-side data on the server
|
|
2
|
+
|
|
3
|
+
## Mô tả
|
|
4
|
+
|
|
5
|
+
Rule S025 đảm bảo rằng tất cả dữ liệu từ client đều được validate trên server. Client-side validation không đủ để bảo mật vì có thể bị bypass bởi kẻ tấn công. Server-side validation là bắt buộc để đảm bảo tính toàn vẹn dữ liệu và bảo mật.
|
|
6
|
+
|
|
7
|
+
## OWASP Mapping
|
|
8
|
+
|
|
9
|
+
- **Category**: A03:2021 – Injection
|
|
10
|
+
- **Subcategories**:
|
|
11
|
+
- A04:2021 – Insecure Design
|
|
12
|
+
- A07:2021 – Identification and Authentication Failures
|
|
13
|
+
|
|
14
|
+
## Các Pattern được phát hiện
|
|
15
|
+
|
|
16
|
+
### 1. NestJS Violations
|
|
17
|
+
|
|
18
|
+
#### ❌ Sử dụng @Body() với 'any' type
|
|
19
|
+
```typescript
|
|
20
|
+
@Post('/checkout')
|
|
21
|
+
checkout(@Body() body: any) {
|
|
22
|
+
// Client có thể inject bất kỳ field nào như isAdmin, discount
|
|
23
|
+
return this.orderService.checkout(body);
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
#### ❌ Trust sensitive fields từ client
|
|
28
|
+
```typescript
|
|
29
|
+
@Post('/orders')
|
|
30
|
+
create(@Body() { userId, price, discount, isAdmin }: any) {
|
|
31
|
+
// userId, price, discount, isAdmin KHÔNG nên từ client
|
|
32
|
+
return this.orderService.create(userId, price, discount, isAdmin);
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
#### ✅ Cách fix đúng
|
|
37
|
+
```typescript
|
|
38
|
+
// Cấu hình ValidationPipe global
|
|
39
|
+
app.useGlobalPipes(new ValidationPipe({
|
|
40
|
+
whitelist: true,
|
|
41
|
+
forbidNonWhitelisted: true,
|
|
42
|
+
transform: true
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
// Sử dụng DTO với validation
|
|
46
|
+
export class CheckoutDto {
|
|
47
|
+
@IsUUID() productId: string;
|
|
48
|
+
@IsInt() @Min(1) @Max(100) quantity: number;
|
|
49
|
+
@IsOptional() @IsIn(['SPRING10','VIP20']) coupon?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Post('/checkout')
|
|
53
|
+
@UseGuards(JwtAuthGuard)
|
|
54
|
+
checkout(@Body() dto: CheckoutDto, @Req() req) {
|
|
55
|
+
const userId = req.user.sub; // Từ JWT, không phải client
|
|
56
|
+
const discount = this.pricingService.resolveCoupon(dto.coupon, userId);
|
|
57
|
+
return this.orderService.checkout({ userId, ...dto, discount });
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 2. Express.js Violations
|
|
62
|
+
|
|
63
|
+
#### ❌ Direct sử dụng req.body không có validation
|
|
64
|
+
```typescript
|
|
65
|
+
app.post('/checkout', (req, res) => {
|
|
66
|
+
const { userId, price, discount } = req.body;
|
|
67
|
+
// Không có server-side validation
|
|
68
|
+
const order = await orderService.create(userId, price, discount);
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### ✅ Cách fix đúng
|
|
73
|
+
```typescript
|
|
74
|
+
app.post('/checkout', [
|
|
75
|
+
body('productId').isUUID(),
|
|
76
|
+
body('quantity').isInt({ min: 1, max: 100 }),
|
|
77
|
+
body('coupon').optional().isIn(['SPRING10', 'VIP20']),
|
|
78
|
+
validateRequest
|
|
79
|
+
], async (req, res) => {
|
|
80
|
+
const userId = req.user.id; // Từ auth middleware
|
|
81
|
+
const { productId, quantity, coupon } = req.body; // Đã được validate
|
|
82
|
+
// ...
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 3. SQL Injection
|
|
87
|
+
|
|
88
|
+
#### ❌ String concatenation/template literals
|
|
89
|
+
```typescript
|
|
90
|
+
async findUser(userId: string) {
|
|
91
|
+
const query = `SELECT * FROM users WHERE id = ${userId}`;
|
|
92
|
+
return await this.connection.query(query);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### ✅ Parameterized queries
|
|
97
|
+
```typescript
|
|
98
|
+
async findUser(userId: string) {
|
|
99
|
+
const query = 'SELECT * FROM users WHERE id = ?';
|
|
100
|
+
return await this.connection.query(query, [userId]);
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 4. File Upload
|
|
105
|
+
|
|
106
|
+
#### ❌ Không có validation server-side
|
|
107
|
+
```typescript
|
|
108
|
+
@Post('/avatar')
|
|
109
|
+
@UseInterceptors(FileInterceptor('file'))
|
|
110
|
+
uploadAvatar(@UploadedFile() file) {
|
|
111
|
+
// Không validate type, size, content
|
|
112
|
+
return this.fileService.save(file);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### ✅ Validation đầy đủ
|
|
117
|
+
```typescript
|
|
118
|
+
@Post('/avatar')
|
|
119
|
+
@UseInterceptors(FileInterceptor('file', {
|
|
120
|
+
limits: { fileSize: 5 * 1024 * 1024 },
|
|
121
|
+
fileFilter: (req, file, cb) => {
|
|
122
|
+
const allowedMimes = ['image/jpeg', 'image/png'];
|
|
123
|
+
if (!allowedMimes.includes(file.mimetype)) {
|
|
124
|
+
return cb(new BadRequestException('Invalid file type'), false);
|
|
125
|
+
}
|
|
126
|
+
cb(null, true);
|
|
127
|
+
}
|
|
128
|
+
}))
|
|
129
|
+
async uploadAvatar(@UploadedFile() file) {
|
|
130
|
+
// Additional server-side validation
|
|
131
|
+
const isValid = await this.fileService.validateImageContent(file.path);
|
|
132
|
+
if (!isValid) {
|
|
133
|
+
throw new BadRequestException('Invalid image file');
|
|
134
|
+
}
|
|
135
|
+
return this.fileService.processAvatar(file);
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Sensitive Fields
|
|
140
|
+
|
|
141
|
+
Rule phát hiện các field nhạy cảm không nên trust từ client:
|
|
142
|
+
|
|
143
|
+
- `userId`, `user_id`, `id`
|
|
144
|
+
- `role`, `roles`, `permissions`
|
|
145
|
+
- `price`, `amount`, `total`, `cost`
|
|
146
|
+
- `isAdmin`, `is_admin`, `admin`
|
|
147
|
+
- `discount`, `balance`, `credits`
|
|
148
|
+
- `isActive`, `is_active`, `enabled`
|
|
149
|
+
- `status`, `state`
|
|
150
|
+
|
|
151
|
+
## Test Command
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
node cli.js --input=./examples/rule-test-fixtures/rules/S025_server_side_validation --rule=S025 --engine=heuristic
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Framework Support
|
|
158
|
+
|
|
159
|
+
- ✅ **NestJS**: ValidationPipe, DTO validation, class-validator
|
|
160
|
+
- ✅ **Express.js**: express-validator, joi, yup validation middleware
|
|
161
|
+
- ✅ **TypeORM**: Parameterized queries, QueryBuilder
|
|
162
|
+
- ✅ **File Upload**: Multer validation, file type checking
|
|
163
|
+
|
|
164
|
+
## Checklist
|
|
165
|
+
|
|
166
|
+
- [ ] Sử dụng ValidationPipe global với `whitelist`, `forbidNonWhitelisted`, `transform`
|
|
167
|
+
- [ ] Tất cả route có DTO + class-validator, không sử dụng `any`/`Record<string, any>`
|
|
168
|
+
- [ ] Không nhận field nhạy cảm từ client (`userId`, `role`, `price`, `isAdmin`)
|
|
169
|
+
- [ ] Tính toán giá/discount ở server từ dữ liệu chuẩn, không tin client
|
|
170
|
+
- [ ] Query DB luôn tham số hóa; tham số động (sort, column) phải whitelist
|
|
171
|
+
- [ ] Upload file: kiểm tra type/size server-side; không render trực tiếp SVG
|
|
172
|
+
- [ ] Endpoint thanh toán có idempotency key/nonce/timestamp chống replay
|
|
173
|
+
- [ ] Exception filter: log nội bộ chi tiết, trả message tối giản cho client
|
|
174
|
+
|
|
175
|
+
## Tham khảo
|
|
176
|
+
|
|
177
|
+
- [OWASP Input Validation](https://owasp.org/www-project-proactive-controls/v3/en/c5-validate-inputs)
|
|
178
|
+
- [NestJS Validation](https://docs.nestjs.com/techniques/validation)
|
|
179
|
+
- [Express Validator](https://express-validator.github.io/docs/)
|