@sun-asterisk/sunlint 1.3.0 → 1.3.2

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 (124) hide show
  1. package/CHANGELOG.md +115 -1
  2. package/CONTRIBUTING.md +249 -605
  3. package/README.md +3 -4
  4. package/config/ci-cd.json +54 -0
  5. package/config/development.json +56 -0
  6. package/config/large-project.json +143 -0
  7. package/config/presets/all.json +0 -1
  8. package/config/release.json +70 -0
  9. package/config/rule-analysis-strategies.js +38 -3
  10. package/config/rules/enhanced-rules-registry.json +474 -1179
  11. package/config/rules/rules-registry-generated.json +3 -3
  12. package/core/cli-action-handler.js +24 -30
  13. package/core/cli-program.js +11 -3
  14. package/core/config-merger.js +29 -2
  15. package/core/enhanced-rules-registry.js +3 -2
  16. package/core/semantic-engine.js +129 -19
  17. package/core/semantic-rule-base.js +4 -2
  18. package/core/unified-rule-registry.js +1 -1
  19. package/docs/COMMAND-EXAMPLES.md +134 -0
  20. package/docs/LARGE-PROJECT-GUIDE.md +324 -0
  21. package/engines/heuristic-engine.js +135 -16
  22. package/integrations/eslint/plugin/index.js +0 -2
  23. package/integrations/eslint/plugin/rules/common/c003-no-vague-abbreviations.js +59 -1
  24. package/integrations/eslint/plugin/rules/common/c006-function-name-verb-noun.js +26 -1
  25. package/integrations/eslint/plugin/rules/common/c030-use-custom-error-classes.js +54 -19
  26. package/origin-rules/common-en.md +19 -15
  27. package/package.json +1 -1
  28. package/rules/common/C002_no_duplicate_code/analyzer.js +334 -36
  29. package/rules/common/C003_no_vague_abbreviations/analyzer.js +220 -35
  30. package/rules/common/C006_function_naming/analyzer.js +29 -3
  31. package/rules/common/C010_limit_block_nesting/analyzer.js +181 -337
  32. package/rules/common/C010_limit_block_nesting/config.json +64 -0
  33. package/rules/common/C010_limit_block_nesting/regex-based-analyzer.js +379 -0
  34. package/rules/common/C010_limit_block_nesting/symbol-based-analyzer.js +231 -0
  35. package/rules/common/C013_no_dead_code/analyzer.js +75 -177
  36. package/rules/common/C013_no_dead_code/config.json +61 -0
  37. package/rules/common/C013_no_dead_code/regex-based-analyzer.js +345 -0
  38. package/rules/common/C013_no_dead_code/symbol-based-analyzer.js +640 -0
  39. package/rules/common/C014_dependency_injection/analyzer.js +48 -313
  40. package/rules/common/C014_dependency_injection/config.json +26 -0
  41. package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +751 -0
  42. package/rules/common/C017_constructor_logic/analyzer.js +254 -17
  43. package/rules/common/C017_constructor_logic/semantic-analyzer.js +340 -0
  44. package/rules/common/C018_no_throw_generic_error/analyzer.js +232 -0
  45. package/rules/common/C018_no_throw_generic_error/config.json +50 -0
  46. package/rules/common/C018_no_throw_generic_error/regex-based-analyzer.js +387 -0
  47. package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +314 -0
  48. package/rules/common/C019_log_level_usage/analyzer.js +110 -317
  49. package/rules/common/C019_log_level_usage/pattern-analyzer.js +88 -0
  50. package/rules/common/C019_log_level_usage/system-log-analyzer.js +1267 -0
  51. package/rules/common/C023_no_duplicate_variable/analyzer.js +180 -0
  52. package/rules/common/C023_no_duplicate_variable/config.json +50 -0
  53. package/rules/common/C023_no_duplicate_variable/symbol-based-analyzer.js +158 -0
  54. package/rules/common/C024_no_scatter_hardcoded_constants/analyzer.js +180 -0
  55. package/rules/common/C024_no_scatter_hardcoded_constants/config.json +50 -0
  56. package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +181 -0
  57. package/rules/common/C030_use_custom_error_classes/analyzer.js +200 -0
  58. package/rules/common/C033_separate_service_repository/README.md +78 -0
  59. package/rules/common/C033_separate_service_repository/analyzer.js +160 -0
  60. package/rules/common/C033_separate_service_repository/config.json +50 -0
  61. package/rules/common/C033_separate_service_repository/regex-based-analyzer.js +585 -0
  62. package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +368 -0
  63. package/rules/common/C035_error_logging_context/STRATEGY.md +99 -0
  64. package/rules/common/C035_error_logging_context/analyzer.js +232 -0
  65. package/rules/common/C035_error_logging_context/config.json +54 -0
  66. package/rules/common/C035_error_logging_context/regex-based-analyzer.js +299 -0
  67. package/rules/common/C035_error_logging_context/symbol-based-analyzer.js +454 -0
  68. package/rules/common/C040_centralized_validation/analyzer.js +165 -0
  69. package/rules/common/C040_centralized_validation/config.json +46 -0
  70. package/rules/common/C040_centralized_validation/regex-based-analyzer.js +243 -0
  71. package/rules/common/C040_centralized_validation/symbol-based-analyzer.js +416 -0
  72. package/rules/common/{C076_single_test_behavior → C072_single_test_behavior}/analyzer.js +6 -6
  73. package/rules/common/C076_explicit_function_types/README.md +30 -0
  74. package/rules/common/C076_explicit_function_types/analyzer.js +172 -0
  75. package/rules/common/C076_explicit_function_types/config.json +15 -0
  76. package/rules/common/C076_explicit_function_types/semantic-analyzer.js +341 -0
  77. package/rules/index.js +6 -1
  78. package/rules/parser/rule-parser.js +13 -2
  79. package/rules/security/S005_no_origin_auth/README.md +226 -0
  80. package/rules/security/S005_no_origin_auth/analyzer.js +184 -0
  81. package/rules/security/S005_no_origin_auth/ast-analyzer.js +406 -0
  82. package/rules/security/S005_no_origin_auth/config.json +85 -0
  83. package/rules/security/S006_no_plaintext_recovery_codes/README.md +139 -0
  84. package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +306 -0
  85. package/rules/security/S006_no_plaintext_recovery_codes/config.json +48 -0
  86. package/rules/security/S007_no_plaintext_otp/README.md +198 -0
  87. package/rules/security/S007_no_plaintext_otp/analyzer.js +406 -0
  88. package/rules/security/S007_no_plaintext_otp/config.json +79 -0
  89. package/rules/security/S007_no_plaintext_otp/semantic-analyzer.js +609 -0
  90. package/rules/security/S007_no_plaintext_otp/semantic-config.json +195 -0
  91. package/rules/security/S007_no_plaintext_otp/semantic-wrapper.js +280 -0
  92. package/rules/security/S009_no_insecure_encryption/README.md +158 -0
  93. package/rules/security/S009_no_insecure_encryption/analyzer.js +319 -0
  94. package/rules/security/S009_no_insecure_encryption/config.json +55 -0
  95. package/rules/security/S010_no_insecure_encryption/README.md +224 -0
  96. package/rules/security/S010_no_insecure_encryption/analyzer.js +493 -0
  97. package/rules/security/S010_no_insecure_encryption/config.json +48 -0
  98. package/rules/security/S016_no_sensitive_querystring/STRATEGY.md +149 -0
  99. package/rules/security/S016_no_sensitive_querystring/analyzer.js +276 -0
  100. package/rules/security/S016_no_sensitive_querystring/config.json +127 -0
  101. package/rules/security/S016_no_sensitive_querystring/regex-based-analyzer.js +258 -0
  102. package/rules/security/S016_no_sensitive_querystring/symbol-based-analyzer.js +495 -0
  103. package/rules/security/S027_no_hardcoded_secrets/analyzer.js +180 -366
  104. package/rules/security/S027_no_hardcoded_secrets/categories.json +153 -0
  105. package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +250 -0
  106. package/rules/security/S048_no_current_password_in_reset/README.md +222 -0
  107. package/rules/security/S048_no_current_password_in_reset/analyzer.js +366 -0
  108. package/rules/security/S048_no_current_password_in_reset/config.json +48 -0
  109. package/rules/security/S055_content_type_validation/README.md +176 -0
  110. package/rules/security/S055_content_type_validation/analyzer.js +312 -0
  111. package/rules/security/S055_content_type_validation/config.json +48 -0
  112. package/rules/utils/rule-helpers.js +140 -1
  113. package/scripts/consolidate-config.js +116 -0
  114. package/scripts/prepare-release.sh +1 -1
  115. package/config/rules/rules-registry.json +0 -765
  116. package/docs/ESLINT-INTEGRATION-STRATEGY.md +0 -392
  117. package/docs/FUTURE_PACKAGES.md +0 -83
  118. package/docs/HEURISTIC_VS_AI.md +0 -113
  119. package/docs/PRODUCTION_DEPLOYMENT_ANALYSIS.md +0 -112
  120. package/docs/PRODUCTION_SIZE_IMPACT.md +0 -183
  121. package/docs/RELEASE_GUIDE.md +0 -230
  122. package/docs/STANDARDIZED-CATEGORY-FILTERING.md +0 -156
  123. package/integrations/eslint/plugin/rules/common/c076-single-behavior-per-test.js +0 -254
  124. package/rules/common/C006_function_naming/smart-analyzer.js +0 -503
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Heuristic analyzer for S048 - No Current Password in Reset Process
3
+ * Purpose: Detect requiring current password during password reset process
4
+ * Based on OWASP A04:2021 - Insecure Design
5
+ */
6
+
7
+ class S048Analyzer {
8
+ constructor() {
9
+ this.ruleId = 'S048';
10
+ this.ruleName = 'No Current Password in Reset Process';
11
+ this.description = 'Do not require current password during password reset process';
12
+
13
+ // Keywords that indicate password reset functionality
14
+ this.resetKeywords = [
15
+ 'reset', 'forgot', 'recover', 'change', 'update', 'modify',
16
+ 'resetpassword', 'forgotpassword', 'changepassword', 'updatepassword'
17
+ ];
18
+
19
+ // Keywords that indicate current password requirement
20
+ this.currentPasswordKeywords = [
21
+ 'currentpassword', 'current_password', 'oldpassword', 'old_password',
22
+ 'existingpassword', 'existing_password', 'presentpassword', 'present_password',
23
+ 'previouspassword', 'previous_password', 'originalpassword', 'original_password'
24
+ ];
25
+
26
+ // API endpoint patterns for password reset
27
+ this.resetEndpointPatterns = [
28
+ /\/reset[-_]?password/i,
29
+ /\/forgot[-_]?password/i,
30
+ /\/change[-_]?password/i,
31
+ /\/update[-_]?password/i,
32
+ /\/password[-_]?reset/i,
33
+ /\/password[-_]?change/i,
34
+ /\/password[-_]?update/i,
35
+ /\/user\/password/i,
36
+ /\/auth\/reset/i,
37
+ /\/auth\/forgot/i
38
+ ];
39
+
40
+ // Function/method patterns related to password reset
41
+ this.resetFunctionPatterns = [
42
+ /resetpassword/i,
43
+ /forgotpassword/i,
44
+ /changepassword/i,
45
+ /updatepassword/i,
46
+ /passwordreset/i,
47
+ /passwordchange/i,
48
+ /passwordupdate/i,
49
+ /handlepasswordreset/i,
50
+ /handleforgotpassword/i,
51
+ /processpasswordreset/i
52
+ ];
53
+
54
+ // Patterns for requiring current password in reset context
55
+ this.violationPatterns = [
56
+ // Validation/requirement patterns
57
+ /(?:required?|validate|check|verify).*(?:current|old|existing|present|previous|original).*password/i,
58
+ /(?:current|old|existing|present|previous|original).*password.*(?:required?|validate|check|verify)/i,
59
+
60
+ // Form field patterns
61
+ /(?:input|field|param|body|request).*(?:current|old|existing|present|previous|original).*password/i,
62
+ /(?:current|old|existing|present|previous|original).*password.*(?:input|field|param|body|request)/i,
63
+
64
+ // Comparison patterns
65
+ /(?:compare|match|equal|verify).*(?:current|old|existing|present|previous|original).*password/i,
66
+ /(?:current|old|existing|present|previous|original).*password.*(?:compare|match|equal|verify)/i,
67
+
68
+ // Database lookup patterns
69
+ /(?:select|find|get|fetch|query).*(?:current|old|existing|present|previous|original).*password/i,
70
+ /(?:current|old|existing|present|previous|original).*password.*(?:select|find|get|fetch|query)/i,
71
+
72
+ // Error message patterns
73
+ /(?:current|old|existing|present|previous|original).*password.*(?:incorrect|wrong|invalid|mismatch)/i,
74
+ /(?:incorrect|wrong|invalid|mismatch).*(?:current|old|existing|present|previous|original).*password/i,
75
+
76
+ // Schema/model field patterns
77
+ /currentPassword|current_password|oldPassword|old_password|existingPassword|existing_password/,
78
+
79
+ // Template/HTML patterns
80
+ /"[^"]*(?:current|old|existing|present|previous|original)[^"]*password[^"]*"/i,
81
+ /'[^']*(?:current|old|existing|present|previous|original)[^']*password[^']*'/i,
82
+ /`[^`]*(?:current|old|existing|present|previous|original)[^`]*password[^`]*`/i
83
+ ];
84
+
85
+ // Safe patterns that should be excluded
86
+ this.safePatterns = [
87
+ // Comments and documentation
88
+ /\/\/|\/\*|\*\/|@param|@return|@example|@deprecated/,
89
+
90
+ // Import/export statements
91
+ /import|export|require|module\.exports/i,
92
+
93
+ // Type definitions
94
+ /interface|type|enum|class.*\{/i,
95
+
96
+ // Configuration files
97
+ /config|setting|option|constant|env/i,
98
+
99
+ // Test files patterns
100
+ /test|spec|mock|fixture|stub/i,
101
+
102
+ // Logging patterns (acceptable for debugging)
103
+ /log|debug|trace|console|logger/i,
104
+
105
+ // Historical/audit patterns (not current validation)
106
+ /history|audit|backup|archive|previous.*login/i,
107
+
108
+ // Password change (not reset) - legitimate to require current password
109
+ /changepassword.*current/i,
110
+ /updatepassword.*current/i,
111
+
112
+ // Safe messages about security
113
+ /for security|security purposes|secure|protection|best practice/i,
114
+
115
+ // Documentation patterns
116
+ /should not|avoid|don't|never|security risk|vulnerability/i
117
+ ];
118
+
119
+ // Context keywords that indicate password reset (not change)
120
+ this.resetContextKeywords = [
121
+ 'reset', 'forgot', 'forgotten', 'recover', 'recovery', 'token', 'link', 'email',
122
+ 'verification', 'verify', 'code', 'otp', 'temporary'
123
+ ];
124
+
125
+ // Keywords that indicate password change (legitimate to require current password)
126
+ this.changeContextKeywords = [
127
+ 'profile', 'settings', 'account', 'preferences', 'dashboard', 'authenticated',
128
+ 'logged', 'session'
129
+ ];
130
+ }
131
+
132
+ async analyze(files, language, options = {}) {
133
+ const violations = [];
134
+
135
+ for (const filePath of files) {
136
+ // Skip test files, build directories, and node_modules
137
+ if (this.shouldSkipFile(filePath)) {
138
+ continue;
139
+ }
140
+
141
+ try {
142
+ const content = require('fs').readFileSync(filePath, 'utf8');
143
+ const fileViolations = this.analyzeFile(content, filePath, options);
144
+ violations.push(...fileViolations);
145
+ } catch (error) {
146
+ if (options.verbose) {
147
+ console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
148
+ }
149
+ }
150
+ }
151
+
152
+ return violations;
153
+ }
154
+
155
+ shouldSkipFile(filePath) {
156
+ const skipPatterns = [
157
+ 'test/', 'tests/', '__tests__/', '.test.', '.spec.',
158
+ 'node_modules/', 'build/', 'dist/', '.next/', 'coverage/',
159
+ 'vendor/', 'mocks/', '.mock.'
160
+ ];
161
+
162
+ return skipPatterns.some(pattern => filePath.includes(pattern));
163
+ }
164
+
165
+ analyzeFile(content, filePath, options = {}) {
166
+ const violations = [];
167
+ const lines = content.split('\n');
168
+
169
+ lines.forEach((line, index) => {
170
+ const lineNumber = index + 1;
171
+ const trimmedLine = line.trim();
172
+
173
+ // Skip comments, imports, and empty lines
174
+ if (this.shouldSkipLine(trimmedLine)) {
175
+ return;
176
+ }
177
+
178
+ // Check for password reset context
179
+ if (this.isPasswordResetContext(content, line, lineNumber)) {
180
+ // Check for current password requirement violation
181
+ const violation = this.checkForCurrentPasswordRequirement(line, lineNumber, filePath, content);
182
+ if (violation) {
183
+ violations.push(violation);
184
+ }
185
+ }
186
+ });
187
+
188
+ return violations;
189
+ }
190
+
191
+ shouldSkipLine(line) {
192
+ // Skip comments, imports, and other non-code lines
193
+ return (
194
+ line.length === 0 ||
195
+ line.startsWith('//') ||
196
+ line.startsWith('/*') ||
197
+ line.startsWith('*') ||
198
+ line.startsWith('import ') ||
199
+ line.startsWith('export ') ||
200
+ line.startsWith('require(') ||
201
+ line.includes('module.exports')
202
+ );
203
+ }
204
+
205
+ isPasswordResetContext(content, line, lineNumber) {
206
+ const lowerContent = content.toLowerCase();
207
+ const lowerLine = line.toLowerCase();
208
+
209
+ // Check if this is in a password reset context
210
+ const hasResetContext = (
211
+ // Check current line for reset keywords
212
+ this.resetKeywords.some(keyword => lowerLine.includes(keyword)) ||
213
+
214
+ // Check for reset endpoint patterns
215
+ this.resetEndpointPatterns.some(pattern => pattern.test(line)) ||
216
+
217
+ // Check for reset function patterns
218
+ this.resetFunctionPatterns.some(pattern => pattern.test(line)) ||
219
+
220
+ // Check surrounding context (within 10 lines)
221
+ this.hasResetContextNearby(content, lineNumber)
222
+ );
223
+
224
+ // Exclude if it's clearly a password change context (not reset)
225
+ const hasChangeContext = this.changeContextKeywords.some(keyword =>
226
+ lowerContent.includes(keyword) || lowerLine.includes(keyword)
227
+ );
228
+
229
+ return hasResetContext && !hasChangeContext;
230
+ }
231
+
232
+ hasResetContextNearby(content, lineNumber) {
233
+ const lines = content.split('\n');
234
+ const start = Math.max(0, lineNumber - 10);
235
+ const end = Math.min(lines.length, lineNumber + 10);
236
+
237
+ for (let i = start; i < end; i++) {
238
+ const nearbyLine = lines[i].toLowerCase();
239
+
240
+ // Check for reset context keywords
241
+ if (this.resetContextKeywords.some(keyword => nearbyLine.includes(keyword))) {
242
+ return true;
243
+ }
244
+
245
+ // Check for reset endpoints
246
+ if (this.resetEndpointPatterns.some(pattern => pattern.test(lines[i]))) {
247
+ return true;
248
+ }
249
+
250
+ // Check for reset function names
251
+ if (this.resetFunctionPatterns.some(pattern => pattern.test(lines[i]))) {
252
+ return true;
253
+ }
254
+ }
255
+
256
+ return false;
257
+ }
258
+
259
+ checkForCurrentPasswordRequirement(line, lineNumber, filePath, content) {
260
+ // First check if line contains safe patterns (early exit)
261
+ if (this.containsSafePattern(line)) {
262
+ return null;
263
+ }
264
+
265
+ // Check for direct violation patterns
266
+ for (const pattern of this.violationPatterns) {
267
+ if (pattern.test(line)) {
268
+ // Additional context validation to reduce false positives
269
+ if (this.isValidViolationContext(line, content, lineNumber)) {
270
+ return {
271
+ ruleId: this.ruleId,
272
+ severity: 'error',
273
+ message: 'Password reset process should not require current password. Use secure token-based reset instead.',
274
+ line: lineNumber,
275
+ column: this.findPatternColumn(line, pattern),
276
+ filePath: filePath,
277
+ type: 'current_password_in_reset',
278
+ details: 'Requiring current password during reset defeats the purpose of password reset and creates security issues. Use email/SMS verification with secure tokens instead.'
279
+ };
280
+ }
281
+ }
282
+ }
283
+
284
+ // Check for variable/field names that suggest current password requirement
285
+ const currentPasswordField = this.checkCurrentPasswordField(line, lineNumber, filePath);
286
+ if (currentPasswordField) {
287
+ return currentPasswordField;
288
+ }
289
+
290
+ return null;
291
+ }
292
+
293
+ containsSafePattern(line) {
294
+ return this.safePatterns.some(pattern => pattern.test(line));
295
+ }
296
+
297
+ isValidViolationContext(line, content, lineNumber) {
298
+ const lowerLine = line.toLowerCase();
299
+
300
+ // Check if this is actually about password reset (not change)
301
+ const hasResetIndicators = this.resetContextKeywords.some(keyword =>
302
+ content.toLowerCase().includes(keyword)
303
+ );
304
+
305
+ // Check if it's in a validation/requirement context
306
+ const hasRequirementContext = [
307
+ 'required', 'validate', 'check', 'verify', 'input', 'field', 'param',
308
+ 'body', 'request', 'schema', 'model', 'form'
309
+ ].some(keyword => lowerLine.includes(keyword));
310
+
311
+ // Check if it's actually requiring/validating current password
312
+ const hasCurrentPasswordRequirement = this.currentPasswordKeywords.some(keyword =>
313
+ lowerLine.includes(keyword)
314
+ );
315
+
316
+ return hasResetIndicators && hasRequirementContext && hasCurrentPasswordRequirement;
317
+ }
318
+
319
+ checkCurrentPasswordField(line, lineNumber, filePath) {
320
+ // Look for variable declarations, object properties, or field definitions
321
+ // that suggest current password fields in reset context
322
+
323
+ const fieldPatterns = [
324
+ // Variable declarations
325
+ /(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=.*(?:current|old|existing).*password/i,
326
+
327
+ // Object properties
328
+ /['"']?(currentPassword|current_password|oldPassword|old_password|existingPassword|existing_password)['"']?\s*:/,
329
+
330
+ // Form field names
331
+ /name\s*=\s*['"](current|old|existing)[-_]?password['"]/i,
332
+
333
+ // Schema/model fields
334
+ /(?:currentPassword|current_password|oldPassword|old_password|existingPassword|existing_password)\s*:\s*(?:String|type|required)/i,
335
+
336
+ // Validation rules
337
+ /(?:currentPassword|current_password|oldPassword|old_password|existingPassword|existing_password).*(?:required|validate)/i
338
+ ];
339
+
340
+ for (const pattern of fieldPatterns) {
341
+ const match = line.match(pattern);
342
+ if (match) {
343
+ return {
344
+ ruleId: this.ruleId,
345
+ severity: 'warning',
346
+ message: `Field '${match[1] || match[0]}' suggests requiring current password in reset process. This should be avoided.`,
347
+ line: lineNumber,
348
+ column: line.indexOf(match[0]) + 1,
349
+ filePath: filePath,
350
+ type: 'current_password_field',
351
+ fieldName: match[1] || match[0],
352
+ details: 'Password reset should use token-based verification, not current password validation.'
353
+ };
354
+ }
355
+ }
356
+
357
+ return null;
358
+ }
359
+
360
+ findPatternColumn(line, pattern) {
361
+ const match = pattern.exec(line);
362
+ return match ? match.index + 1 : 1;
363
+ }
364
+ }
365
+
366
+ module.exports = S048Analyzer;
@@ -0,0 +1,48 @@
1
+ {
2
+ "ruleId": "S048",
3
+ "name": "No Current Password in Reset Process",
4
+ "description": "Do not require current password during password reset process",
5
+ "category": "security",
6
+ "severity": "error",
7
+ "languages": ["All languages"],
8
+ "tags": ["security", "owasp", "insecure-design", "authentication", "password-reset"],
9
+ "enabled": true,
10
+ "fixable": false,
11
+ "engine": "heuristic",
12
+ "metadata": {
13
+ "owaspCategory": "A04:2021 - Insecure Design",
14
+ "cweId": "CWE-640",
15
+ "description": "Requiring the current password during password reset defeats the purpose of the reset process and creates security vulnerabilities. Users who have forgotten their password cannot complete the reset, and this practice can lead to account lockouts and security issues.",
16
+ "impact": "Medium - Account lockout, user frustration, security bypass attempts",
17
+ "likelihood": "High",
18
+ "remediation": "Use secure token-based password reset with email/SMS verification. Never require current password during reset process."
19
+ },
20
+ "patterns": {
21
+ "vulnerable": [
22
+ "Requiring current password in forgot password form",
23
+ "Validating old password during reset process",
24
+ "API endpoints that check current password for reset",
25
+ "Reset forms with current password fields"
26
+ ],
27
+ "secure": [
28
+ "Token-based password reset via email",
29
+ "SMS verification for password reset",
30
+ "Time-limited secure reset links",
31
+ "Multi-factor authentication for reset verification"
32
+ ]
33
+ },
34
+ "examples": {
35
+ "violations": [
36
+ "if (!validateCurrentPassword(currentPassword)) { return error; }",
37
+ "const resetData = { currentPassword, newPassword };",
38
+ "currentPassword: { type: String, required: true }",
39
+ "req.body.currentPassword === user.password"
40
+ ],
41
+ "fixes": [
42
+ "if (!validateResetToken(token)) { return error; }",
43
+ "const resetData = { token, newPassword };",
44
+ "resetToken: { type: String, required: true }",
45
+ "validateResetToken(req.body.token)"
46
+ ]
47
+ }
48
+ }
@@ -0,0 +1,176 @@
1
+ # S055 - Content-Type Validation in REST Services
2
+
3
+ ## Mô tả
4
+
5
+ Rule này kiểm tra xem các dịch vụ REST có xác thực Content-Type header của request đầu vào hay không. Việc thiếu xác thực Content-Type có thể dẫn đến các lỗ hổng bảo mật khi kẻ tấn công gửi dữ liệu độc hại với định dạng không mong muốn.
6
+
7
+ ## Mục tiêu
8
+
9
+ - Đảm bảo các REST endpoint xác thực Content-Type trước khi xử lý request body
10
+ - Ngăn chặn các cuộc tấn công thông qua dữ liệu có định dạng không mong muốn
11
+ - Tuân thủ OWASP ASVS 13.2.5 về xác thực đầu vào
12
+
13
+ ## Chi tiết Rule
14
+
15
+ ### Phát hiện lỗi khi:
16
+
17
+ 1. **Express.js handlers** sử dụng `req.body` mà không kiểm tra Content-Type:
18
+ ```javascript
19
+ app.post('/api/users', (req, res) => {
20
+ const user = req.body; // ❌ Không kiểm tra Content-Type
21
+ // ...
22
+ });
23
+ ```
24
+
25
+ 2. **NestJS controllers** sử dụng `@Body()` mà không có validation:
26
+ ```typescript
27
+ @Post()
28
+ create(@Body() data: any) { // ❌ Không kiểm tra Content-Type
29
+ return this.service.create(data);
30
+ }
31
+ ```
32
+
33
+ 3. **Generic handlers** xử lý request body mà không xác thực:
34
+ ```javascript
35
+ function handlePost(req, res) {
36
+ processData(req.body); // ❌ Không kiểm tra Content-Type
37
+ }
38
+ ```
39
+
40
+ ### Cách khắc phục:
41
+
42
+ 1. **Sử dụng req.is() method**:
43
+ ```javascript
44
+ app.post('/api/users', (req, res) => {
45
+ if (!req.is('application/json')) {
46
+ return res.status(415).send('Unsupported Media Type');
47
+ }
48
+ const user = req.body; // ✅ An toàn
49
+ });
50
+ ```
51
+
52
+ 2. **Kiểm tra header trực tiếp**:
53
+ ```javascript
54
+ app.put('/api/data', (req, res) => {
55
+ if (req.headers['content-type'] !== 'application/json') {
56
+ return res.status(415).json({ error: 'Invalid Content-Type' });
57
+ }
58
+ processData(req.body); // ✅ An toàn
59
+ });
60
+ ```
61
+
62
+ 3. **Sử dụng middleware**:
63
+ ```javascript
64
+ app.use(express.json({ type: 'application/json' }));
65
+ // hoặc
66
+ app.use((req, res, next) => {
67
+ if (req.method !== 'GET' && !req.is('application/json')) {
68
+ return res.status(415).send('Unsupported Media Type');
69
+ }
70
+ next();
71
+ });
72
+ ```
73
+
74
+ 4. **NestJS với decorators**:
75
+ ```typescript
76
+ @Post()
77
+ @Header('Content-Type', 'application/json')
78
+ create(@Body() data: CreateDto) {
79
+ return this.service.create(data);
80
+ }
81
+ ```
82
+
83
+ ## Các trường hợp được bỏ qua
84
+
85
+ Rule sẽ **KHÔNG** báo lỗi trong các trường hợp:
86
+
87
+ 1. **Có global middleware xử lý Content-Type**:
88
+ ```javascript
89
+ app.use(express.json()); // Đã xử lý Content-Type validation
90
+ ```
91
+
92
+ 2. **Test files**: Files có chứa `test`, `spec`, `__tests__`
93
+
94
+ 3. **Configuration files**: Files trong thư mục `config`, `configs`
95
+
96
+ 4. **Comments và documentation**
97
+
98
+ 5. **Import/export statements**
99
+
100
+ ## Mức độ nghiêm trọng
101
+
102
+ - **Severity**: Error
103
+ - **Impact**: Medium - Data injection, parsing errors, security bypass
104
+ - **Likelihood**: Medium
105
+
106
+ ## Tham khảo
107
+
108
+ - **OWASP ASVS 13.2.5**: Input Validation
109
+ - **CWE-20**: Improper Input Validation
110
+ - **Express.js Documentation**: [req.is()](https://expressjs.com/en/4x/api.html#req.is)
111
+ - **NestJS Documentation**: [Validation](https://docs.nestjs.com/techniques/validation)
112
+
113
+ ## Ví dụ chi tiết
114
+
115
+ ### Express.js
116
+
117
+ **Lỗi:**
118
+ ```javascript
119
+ const express = require('express');
120
+ const app = express();
121
+
122
+ app.post('/api/users', (req, res) => {
123
+ const userData = req.body; // ❌ Thiếu Content-Type validation
124
+ // Xử lý userData...
125
+ });
126
+ ```
127
+
128
+ **Sửa:**
129
+ ```javascript
130
+ const express = require('express');
131
+ const app = express();
132
+
133
+ app.post('/api/users', (req, res) => {
134
+ if (!req.is('application/json')) {
135
+ return res.status(415).json({ error: 'Content-Type must be application/json' });
136
+ }
137
+ const userData = req.body; // ✅ An toàn
138
+ // Xử lý userData...
139
+ });
140
+ ```
141
+
142
+ ### NestJS
143
+
144
+ **Lỗi:**
145
+ ```typescript
146
+ @Controller('users')
147
+ export class UsersController {
148
+ @Post()
149
+ create(@Body() createUserDto: any) { // ❌ Thiếu Content-Type validation
150
+ return this.usersService.create(createUserDto);
151
+ }
152
+ }
153
+ ```
154
+
155
+ **Sửa:**
156
+ ```typescript
157
+ @Controller('users')
158
+ export class UsersController {
159
+ @Post()
160
+ @Header('Content-Type', 'application/json')
161
+ create(@Body() createUserDto: CreateUserDto) { // ✅ An toàn với DTO validation
162
+ return this.usersService.create(createUserDto);
163
+ }
164
+ }
165
+ ```
166
+
167
+ ## Cấu hình
168
+
169
+ Rule này hỗ trợ các ngôn ngữ:
170
+ - TypeScript
171
+ - JavaScript
172
+
173
+ Và tương thích với các framework:
174
+ - Express.js
175
+ - NestJS
176
+ - Generic Node.js applications