@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.
- package/CHANGELOG.md +115 -1
- package/CONTRIBUTING.md +249 -605
- package/README.md +3 -4
- package/config/ci-cd.json +54 -0
- package/config/development.json +56 -0
- package/config/large-project.json +143 -0
- package/config/presets/all.json +0 -1
- package/config/release.json +70 -0
- package/config/rule-analysis-strategies.js +38 -3
- package/config/rules/enhanced-rules-registry.json +474 -1179
- package/config/rules/rules-registry-generated.json +3 -3
- package/core/cli-action-handler.js +24 -30
- package/core/cli-program.js +11 -3
- package/core/config-merger.js +29 -2
- package/core/enhanced-rules-registry.js +3 -2
- package/core/semantic-engine.js +129 -19
- package/core/semantic-rule-base.js +4 -2
- package/core/unified-rule-registry.js +1 -1
- package/docs/COMMAND-EXAMPLES.md +134 -0
- package/docs/LARGE-PROJECT-GUIDE.md +324 -0
- package/engines/heuristic-engine.js +135 -16
- package/integrations/eslint/plugin/index.js +0 -2
- package/integrations/eslint/plugin/rules/common/c003-no-vague-abbreviations.js +59 -1
- package/integrations/eslint/plugin/rules/common/c006-function-name-verb-noun.js +26 -1
- package/integrations/eslint/plugin/rules/common/c030-use-custom-error-classes.js +54 -19
- package/origin-rules/common-en.md +19 -15
- package/package.json +1 -1
- package/rules/common/C002_no_duplicate_code/analyzer.js +334 -36
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +220 -35
- package/rules/common/C006_function_naming/analyzer.js +29 -3
- package/rules/common/C010_limit_block_nesting/analyzer.js +181 -337
- package/rules/common/C010_limit_block_nesting/config.json +64 -0
- package/rules/common/C010_limit_block_nesting/regex-based-analyzer.js +379 -0
- package/rules/common/C010_limit_block_nesting/symbol-based-analyzer.js +231 -0
- package/rules/common/C013_no_dead_code/analyzer.js +75 -177
- package/rules/common/C013_no_dead_code/config.json +61 -0
- package/rules/common/C013_no_dead_code/regex-based-analyzer.js +345 -0
- package/rules/common/C013_no_dead_code/symbol-based-analyzer.js +640 -0
- package/rules/common/C014_dependency_injection/analyzer.js +48 -313
- package/rules/common/C014_dependency_injection/config.json +26 -0
- package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +751 -0
- package/rules/common/C017_constructor_logic/analyzer.js +254 -17
- package/rules/common/C017_constructor_logic/semantic-analyzer.js +340 -0
- package/rules/common/C018_no_throw_generic_error/analyzer.js +232 -0
- package/rules/common/C018_no_throw_generic_error/config.json +50 -0
- package/rules/common/C018_no_throw_generic_error/regex-based-analyzer.js +387 -0
- package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +314 -0
- package/rules/common/C019_log_level_usage/analyzer.js +110 -317
- package/rules/common/C019_log_level_usage/pattern-analyzer.js +88 -0
- package/rules/common/C019_log_level_usage/system-log-analyzer.js +1267 -0
- package/rules/common/C023_no_duplicate_variable/analyzer.js +180 -0
- package/rules/common/C023_no_duplicate_variable/config.json +50 -0
- package/rules/common/C023_no_duplicate_variable/symbol-based-analyzer.js +158 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/analyzer.js +180 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/config.json +50 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +181 -0
- package/rules/common/C030_use_custom_error_classes/analyzer.js +200 -0
- package/rules/common/C033_separate_service_repository/README.md +78 -0
- package/rules/common/C033_separate_service_repository/analyzer.js +160 -0
- package/rules/common/C033_separate_service_repository/config.json +50 -0
- package/rules/common/C033_separate_service_repository/regex-based-analyzer.js +585 -0
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +368 -0
- package/rules/common/C035_error_logging_context/STRATEGY.md +99 -0
- package/rules/common/C035_error_logging_context/analyzer.js +232 -0
- package/rules/common/C035_error_logging_context/config.json +54 -0
- package/rules/common/C035_error_logging_context/regex-based-analyzer.js +299 -0
- package/rules/common/C035_error_logging_context/symbol-based-analyzer.js +454 -0
- package/rules/common/C040_centralized_validation/analyzer.js +165 -0
- package/rules/common/C040_centralized_validation/config.json +46 -0
- package/rules/common/C040_centralized_validation/regex-based-analyzer.js +243 -0
- package/rules/common/C040_centralized_validation/symbol-based-analyzer.js +416 -0
- package/rules/common/{C076_single_test_behavior → C072_single_test_behavior}/analyzer.js +6 -6
- package/rules/common/C076_explicit_function_types/README.md +30 -0
- package/rules/common/C076_explicit_function_types/analyzer.js +172 -0
- package/rules/common/C076_explicit_function_types/config.json +15 -0
- package/rules/common/C076_explicit_function_types/semantic-analyzer.js +341 -0
- package/rules/index.js +6 -1
- package/rules/parser/rule-parser.js +13 -2
- package/rules/security/S005_no_origin_auth/README.md +226 -0
- package/rules/security/S005_no_origin_auth/analyzer.js +184 -0
- package/rules/security/S005_no_origin_auth/ast-analyzer.js +406 -0
- package/rules/security/S005_no_origin_auth/config.json +85 -0
- package/rules/security/S006_no_plaintext_recovery_codes/README.md +139 -0
- package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +306 -0
- package/rules/security/S006_no_plaintext_recovery_codes/config.json +48 -0
- package/rules/security/S007_no_plaintext_otp/README.md +198 -0
- package/rules/security/S007_no_plaintext_otp/analyzer.js +406 -0
- package/rules/security/S007_no_plaintext_otp/config.json +79 -0
- package/rules/security/S007_no_plaintext_otp/semantic-analyzer.js +609 -0
- package/rules/security/S007_no_plaintext_otp/semantic-config.json +195 -0
- package/rules/security/S007_no_plaintext_otp/semantic-wrapper.js +280 -0
- package/rules/security/S009_no_insecure_encryption/README.md +158 -0
- package/rules/security/S009_no_insecure_encryption/analyzer.js +319 -0
- package/rules/security/S009_no_insecure_encryption/config.json +55 -0
- package/rules/security/S010_no_insecure_encryption/README.md +224 -0
- package/rules/security/S010_no_insecure_encryption/analyzer.js +493 -0
- package/rules/security/S010_no_insecure_encryption/config.json +48 -0
- package/rules/security/S016_no_sensitive_querystring/STRATEGY.md +149 -0
- package/rules/security/S016_no_sensitive_querystring/analyzer.js +276 -0
- package/rules/security/S016_no_sensitive_querystring/config.json +127 -0
- package/rules/security/S016_no_sensitive_querystring/regex-based-analyzer.js +258 -0
- package/rules/security/S016_no_sensitive_querystring/symbol-based-analyzer.js +495 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +180 -366
- package/rules/security/S027_no_hardcoded_secrets/categories.json +153 -0
- package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +250 -0
- package/rules/security/S048_no_current_password_in_reset/README.md +222 -0
- package/rules/security/S048_no_current_password_in_reset/analyzer.js +366 -0
- package/rules/security/S048_no_current_password_in_reset/config.json +48 -0
- package/rules/security/S055_content_type_validation/README.md +176 -0
- package/rules/security/S055_content_type_validation/analyzer.js +312 -0
- package/rules/security/S055_content_type_validation/config.json +48 -0
- package/rules/utils/rule-helpers.js +140 -1
- package/scripts/consolidate-config.js +116 -0
- package/scripts/prepare-release.sh +1 -1
- package/config/rules/rules-registry.json +0 -765
- package/docs/ESLINT-INTEGRATION-STRATEGY.md +0 -392
- package/docs/FUTURE_PACKAGES.md +0 -83
- package/docs/HEURISTIC_VS_AI.md +0 -113
- package/docs/PRODUCTION_DEPLOYMENT_ANALYSIS.md +0 -112
- package/docs/PRODUCTION_SIZE_IMPACT.md +0 -183
- package/docs/RELEASE_GUIDE.md +0 -230
- package/docs/STANDARDIZED-CATEGORY-FILTERING.md +0 -156
- package/integrations/eslint/plugin/rules/common/c076-single-behavior-per-test.js +0 -254
- 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
|