@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.
Files changed (32) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/config/presets/all.json +49 -48
  3. package/config/presets/beginner.json +7 -18
  4. package/config/presets/ci.json +63 -27
  5. package/config/presets/maintainability.json +6 -4
  6. package/config/presets/performance.json +4 -3
  7. package/config/presets/quality.json +11 -50
  8. package/config/presets/recommended.json +83 -10
  9. package/config/presets/security.json +20 -19
  10. package/config/presets/strict.json +6 -13
  11. package/config/rule-analysis-strategies.js +5 -0
  12. package/config/rules/enhanced-rules-registry.json +87 -7
  13. package/core/config-preset-resolver.js +7 -2
  14. package/package.json +1 -1
  15. package/rules/common/C067_no_hardcoded_config/analyzer.js +95 -0
  16. package/rules/common/C067_no_hardcoded_config/config.json +81 -0
  17. package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +1034 -0
  18. package/rules/common/C070_no_real_time_tests/analyzer.js +320 -0
  19. package/rules/common/C070_no_real_time_tests/config.json +78 -0
  20. package/rules/common/C070_no_real_time_tests/regex-analyzer.js +424 -0
  21. package/rules/security/S024_xpath_xxe_protection/analyzer.js +242 -0
  22. package/rules/security/S024_xpath_xxe_protection/config.json +152 -0
  23. package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +338 -0
  24. package/rules/security/S024_xpath_xxe_protection/symbol-based-analyzer.js +474 -0
  25. package/rules/security/S025_server_side_validation/README.md +179 -0
  26. package/rules/security/S025_server_side_validation/analyzer.js +242 -0
  27. package/rules/security/S025_server_side_validation/config.json +111 -0
  28. package/rules/security/S025_server_side_validation/regex-based-analyzer.js +388 -0
  29. package/rules/security/S025_server_side_validation/symbol-based-analyzer.js +523 -0
  30. package/scripts/README.md +83 -0
  31. package/scripts/analyze-core-rules.js +151 -0
  32. 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/)