@sun-asterisk/sunlint 1.3.1 → 1.3.3

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 (120) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/CONTRIBUTING.md +210 -1691
  3. package/README.md +5 -3
  4. package/config/rule-analysis-strategies.js +17 -1
  5. package/config/rules/enhanced-rules-registry.json +506 -1161
  6. package/config/rules/rules-registry-generated.json +1 -1
  7. package/core/analysis-orchestrator.js +167 -42
  8. package/core/auto-performance-manager.js +243 -0
  9. package/core/cli-action-handler.js +9 -1
  10. package/core/cli-program.js +19 -5
  11. package/core/constants/defaults.js +56 -0
  12. package/core/enhanced-rules-registry.js +2 -1
  13. package/core/performance-optimizer.js +271 -0
  14. package/core/semantic-engine.js +15 -3
  15. package/core/semantic-rule-base.js +4 -2
  16. package/docs/FILE_LIMITS_COMPLETION_REPORT.md +151 -0
  17. package/docs/FILE_LIMITS_EXPLANATION.md +190 -0
  18. package/docs/PERFORMANCE.md +311 -0
  19. package/docs/PERFORMANCE_MIGRATION_GUIDE.md +368 -0
  20. package/docs/PERFORMANCE_OPTIMIZATION_PLAN.md +255 -0
  21. package/docs/QUICK_FILE_LIMITS.md +64 -0
  22. package/docs/SIMPLIFIED_USAGE_GUIDE.md +208 -0
  23. package/engines/heuristic-engine.js +247 -9
  24. package/integrations/eslint/plugin/rules/common/c003-no-vague-abbreviations.js +59 -1
  25. package/integrations/eslint/plugin/rules/common/c006-function-name-verb-noun.js +26 -1
  26. package/integrations/eslint/plugin/rules/common/c030-use-custom-error-classes.js +54 -19
  27. package/origin-rules/common-en.md +11 -7
  28. package/package.json +2 -1
  29. package/rules/common/C002_no_duplicate_code/analyzer.js +334 -36
  30. package/rules/common/C003_no_vague_abbreviations/analyzer.js +220 -35
  31. package/rules/common/C006_function_naming/analyzer.js +29 -3
  32. package/rules/common/C010_limit_block_nesting/analyzer.js +181 -337
  33. package/rules/common/C010_limit_block_nesting/config.json +64 -0
  34. package/rules/common/C010_limit_block_nesting/regex-based-analyzer.js +379 -0
  35. package/rules/common/C010_limit_block_nesting/symbol-based-analyzer.js +231 -0
  36. package/rules/common/C013_no_dead_code/analyzer.js +75 -177
  37. package/rules/common/C013_no_dead_code/config.json +61 -0
  38. package/rules/common/C013_no_dead_code/regex-based-analyzer.js +345 -0
  39. package/rules/common/C013_no_dead_code/symbol-based-analyzer.js +640 -0
  40. package/rules/common/C014_dependency_injection/analyzer.js +48 -313
  41. package/rules/common/C014_dependency_injection/config.json +26 -0
  42. package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +751 -0
  43. package/rules/common/C018_no_throw_generic_error/analyzer.js +232 -0
  44. package/rules/common/C018_no_throw_generic_error/config.json +50 -0
  45. package/rules/common/C018_no_throw_generic_error/regex-based-analyzer.js +387 -0
  46. package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +314 -0
  47. package/rules/common/C019_log_level_usage/analyzer.js +110 -317
  48. package/rules/common/C019_log_level_usage/pattern-analyzer.js +88 -0
  49. package/rules/common/C019_log_level_usage/system-log-analyzer.js +1267 -0
  50. package/rules/common/C023_no_duplicate_variable/analyzer.js +180 -0
  51. package/rules/common/C023_no_duplicate_variable/config.json +50 -0
  52. package/rules/common/C023_no_duplicate_variable/symbol-based-analyzer.js +158 -0
  53. package/rules/common/C024_no_scatter_hardcoded_constants/analyzer.js +180 -0
  54. package/rules/common/C024_no_scatter_hardcoded_constants/config.json +50 -0
  55. package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +181 -0
  56. package/rules/common/C030_use_custom_error_classes/analyzer.js +200 -0
  57. package/rules/common/C035_error_logging_context/analyzer.js +3 -1
  58. package/rules/common/C048_no_bypass_architectural_layers/analyzer.js +180 -0
  59. package/rules/common/C048_no_bypass_architectural_layers/config.json +50 -0
  60. package/rules/common/C048_no_bypass_architectural_layers/symbol-based-analyzer.js +235 -0
  61. package/rules/common/C052_parsing_or_data_transformation/analyzer.js +180 -0
  62. package/rules/common/C052_parsing_or_data_transformation/config.json +50 -0
  63. package/rules/common/C052_parsing_or_data_transformation/symbol-based-analyzer.js +132 -0
  64. package/rules/index.js +7 -1
  65. package/rules/security/S009_no_insecure_encryption/README.md +158 -0
  66. package/rules/security/S009_no_insecure_encryption/analyzer.js +319 -0
  67. package/rules/security/S009_no_insecure_encryption/config.json +55 -0
  68. package/rules/security/S010_no_insecure_encryption/README.md +224 -0
  69. package/rules/security/S010_no_insecure_encryption/analyzer.js +493 -0
  70. package/rules/security/S010_no_insecure_encryption/config.json +48 -0
  71. package/rules/security/S016_no_sensitive_querystring/STRATEGY.md +149 -0
  72. package/rules/security/S016_no_sensitive_querystring/analyzer.js +276 -0
  73. package/rules/security/S016_no_sensitive_querystring/config.json +127 -0
  74. package/rules/security/S016_no_sensitive_querystring/regex-based-analyzer.js +258 -0
  75. package/rules/security/S016_no_sensitive_querystring/symbol-based-analyzer.js +495 -0
  76. package/rules/security/S017_use_parameterized_queries/README.md +128 -0
  77. package/rules/security/S017_use_parameterized_queries/analyzer.js +286 -0
  78. package/rules/security/S017_use_parameterized_queries/config.json +109 -0
  79. package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +541 -0
  80. package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +777 -0
  81. package/rules/security/S031_secure_session_cookies/README.md +127 -0
  82. package/rules/security/S031_secure_session_cookies/analyzer.js +245 -0
  83. package/rules/security/S031_secure_session_cookies/config.json +86 -0
  84. package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +196 -0
  85. package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +1084 -0
  86. package/rules/security/S032_httponly_session_cookies/FRAMEWORK_SUPPORT.md +209 -0
  87. package/rules/security/S032_httponly_session_cookies/README.md +184 -0
  88. package/rules/security/S032_httponly_session_cookies/analyzer.js +282 -0
  89. package/rules/security/S032_httponly_session_cookies/config.json +96 -0
  90. package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +715 -0
  91. package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +1348 -0
  92. package/rules/security/S033_samesite_session_cookies/README.md +227 -0
  93. package/rules/security/S033_samesite_session_cookies/analyzer.js +242 -0
  94. package/rules/security/S033_samesite_session_cookies/config.json +87 -0
  95. package/rules/security/S033_samesite_session_cookies/regex-based-analyzer.js +703 -0
  96. package/rules/security/S033_samesite_session_cookies/symbol-based-analyzer.js +732 -0
  97. package/rules/security/S034_host_prefix_session_cookies/README.md +204 -0
  98. package/rules/security/S034_host_prefix_session_cookies/analyzer.js +290 -0
  99. package/rules/security/S034_host_prefix_session_cookies/config.json +62 -0
  100. package/rules/security/S034_host_prefix_session_cookies/regex-based-analyzer.js +478 -0
  101. package/rules/security/S034_host_prefix_session_cookies/symbol-based-analyzer.js +277 -0
  102. package/rules/security/S035_path_session_cookies/README.md +257 -0
  103. package/rules/security/S035_path_session_cookies/analyzer.js +316 -0
  104. package/rules/security/S035_path_session_cookies/config.json +99 -0
  105. package/rules/security/S035_path_session_cookies/regex-based-analyzer.js +724 -0
  106. package/rules/security/S035_path_session_cookies/symbol-based-analyzer.js +373 -0
  107. package/rules/security/S048_no_current_password_in_reset/README.md +222 -0
  108. package/rules/security/S048_no_current_password_in_reset/analyzer.js +366 -0
  109. package/rules/security/S048_no_current_password_in_reset/config.json +48 -0
  110. package/rules/security/S055_content_type_validation/README.md +176 -0
  111. package/rules/security/S055_content_type_validation/analyzer.js +312 -0
  112. package/rules/security/S055_content_type_validation/config.json +48 -0
  113. package/rules/utils/rule-helpers.js +140 -1
  114. package/scripts/batch-processing-demo.js +334 -0
  115. package/scripts/consolidate-config.js +116 -0
  116. package/scripts/performance-test.js +541 -0
  117. package/scripts/quick-performance-test.js +108 -0
  118. package/config/rules/S027-categories.json +0 -122
  119. package/config/rules/rules-registry.json +0 -777
  120. package/rules/common/C006_function_naming/smart-analyzer.js +0 -503
@@ -0,0 +1,373 @@
1
+ /**
2
+ * S035 Symbol-Based Analyzer - Set Path attribute for Session Cookies
3
+ * Uses TypeScript compiler API for semantic analysis
4
+ */
5
+
6
+ const { SyntaxKind } = require("typescript");
7
+
8
+ class S035SymbolBasedAnalyzer {
9
+ constructor(semanticEngine = null) {
10
+ this.semanticEngine = semanticEngine;
11
+ this.ruleId = "S035";
12
+ this.ruleName = "Set Path attribute for Session Cookies";
13
+ this.category = "security";
14
+ this.violations = [];
15
+
16
+ // Session cookie indicators
17
+ this.sessionIndicators = [
18
+ "session",
19
+ "sessionid",
20
+ "sessid",
21
+ "jsessionid",
22
+ "phpsessid",
23
+ "asp.net_sessionid",
24
+ "connect.sid",
25
+ "auth",
26
+ "token",
27
+ "jwt",
28
+ "csrf",
29
+ "refresh",
30
+ "user",
31
+ "login",
32
+ "authentication",
33
+ "session_id",
34
+ "sid",
35
+ "auth_token",
36
+ "userid",
37
+ "user_id",
38
+ ];
39
+
40
+ // Cookie methods that need security checking
41
+ this.cookieMethods = [
42
+ "setCookie",
43
+ "cookie",
44
+ "set",
45
+ "append",
46
+ "session",
47
+ "setHeader",
48
+ "writeHead",
49
+ ];
50
+
51
+ // Acceptable Path values - should be specific paths, not just "/"
52
+ this.acceptableValues = [
53
+ "/app",
54
+ "/admin",
55
+ "/api",
56
+ "/auth",
57
+ "/user",
58
+ "/secure",
59
+ "/dashboard",
60
+ "/login",
61
+ ];
62
+
63
+ // Root path "/" is acceptable but not recommended for security
64
+ this.rootPath = "/";
65
+ }
66
+
67
+ /**
68
+ * Initialize analyzer with semantic engine
69
+ */
70
+ async initialize(semanticEngine) {
71
+ this.semanticEngine = semanticEngine;
72
+ if (process.env.SUNLINT_DEBUG) {
73
+ console.log(`🔧 [S035] Symbol analyzer initialized`);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Main analysis method for source file
79
+ */
80
+ async analyze(sourceFile, filePath) {
81
+ if (process.env.SUNLINT_DEBUG) {
82
+ console.log(`🔍 [S035] Symbol analysis starting for: ${filePath}`);
83
+ }
84
+
85
+ this.violations = [];
86
+ this.currentFile = sourceFile;
87
+ this.currentFilePath = filePath;
88
+
89
+ this.visitNode(sourceFile);
90
+
91
+ if (process.env.SUNLINT_DEBUG) {
92
+ console.log(
93
+ `🔍 [S035] Symbol analysis completed: ${this.violations.length} violations`
94
+ );
95
+ }
96
+
97
+ return this.violations;
98
+ }
99
+
100
+ visitNode(node) {
101
+ // Check for res.cookie() calls
102
+ if (this.isCallExpression(node)) {
103
+ this.checkCookieCall(node);
104
+ this.checkSetHeaderCall(node);
105
+ }
106
+
107
+ // Check for session middleware configuration
108
+ if (this.isSessionMiddleware(node)) {
109
+ this.checkSessionMiddleware(node);
110
+ }
111
+
112
+ // Recursively visit child nodes
113
+ node.forEachChild((child) => this.visitNode(child));
114
+ }
115
+
116
+ isCallExpression(node) {
117
+ return node.getKind() === SyntaxKind.CallExpression;
118
+ }
119
+
120
+ isSessionMiddleware(node) {
121
+ if (node.getKind() !== SyntaxKind.CallExpression) {
122
+ return false;
123
+ }
124
+
125
+ const expression = node.getExpression();
126
+ const text = expression.getText();
127
+
128
+ return text === "session" || text.includes("session(");
129
+ }
130
+
131
+ checkCookieCall(node) {
132
+ const expression = node.getExpression();
133
+
134
+ // Check if it's res.cookie() call
135
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
136
+ const propertyAccess = expression;
137
+ const property = propertyAccess.getName();
138
+
139
+ if (property === "cookie") {
140
+ const args = node.getArguments();
141
+ if (args.length >= 1) {
142
+ const cookieName = this.extractStringValue(args[0]);
143
+
144
+ if (cookieName && this.isSessionCookie(cookieName)) {
145
+ this.checkCookieOptions(node, args, cookieName);
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ checkSetHeaderCall(node) {
153
+ const expression = node.getExpression();
154
+
155
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
156
+ const propertyAccess = expression;
157
+ const property = propertyAccess.getName();
158
+
159
+ if (property === "setHeader") {
160
+ const args = node.getArguments();
161
+ if (args.length >= 2) {
162
+ const headerName = this.extractStringValue(args[0]);
163
+
164
+ if (headerName && headerName.toLowerCase() === "set-cookie") {
165
+ const headerValue = this.extractStringValue(args[1]);
166
+ if (headerValue) {
167
+ this.checkSetCookieHeader(node, headerValue);
168
+ }
169
+ }
170
+ }
171
+ }
172
+ }
173
+ }
174
+
175
+ checkSessionMiddleware(node) {
176
+ const args = node.getArguments();
177
+ if (
178
+ args.length >= 1 &&
179
+ args[0].getKind() === SyntaxKind.ObjectLiteralExpression
180
+ ) {
181
+ const config = args[0];
182
+ const nameProperty = this.findProperty(config, "name");
183
+ const cookieProperty = this.findProperty(config, "cookie");
184
+
185
+ if (nameProperty) {
186
+ const nameValue = this.extractStringValue(
187
+ nameProperty.getInitializer()
188
+ );
189
+ if (nameValue && this.isSessionCookie(nameValue)) {
190
+ // Check if cookie configuration has path
191
+ if (
192
+ cookieProperty &&
193
+ cookieProperty.getInitializer().getKind() ===
194
+ SyntaxKind.ObjectLiteralExpression
195
+ ) {
196
+ const cookieConfig = cookieProperty.getInitializer();
197
+ this.checkCookieConfigForPath(node, cookieConfig, nameValue);
198
+ } else {
199
+ this.addViolation(
200
+ node,
201
+ `Session middleware cookie "${nameValue}" should specify Path attribute to limit access scope`
202
+ );
203
+ }
204
+ }
205
+ }
206
+ }
207
+ }
208
+
209
+ checkCookieOptions(node, args, cookieName) {
210
+ if (
211
+ args.length >= 3 &&
212
+ args[2].getKind() === SyntaxKind.ObjectLiteralExpression
213
+ ) {
214
+ const options = args[2];
215
+ this.checkCookieConfigForPath(node, options, cookieName);
216
+ } else {
217
+ this.addViolation(
218
+ node,
219
+ `Session cookie "${cookieName}" should specify Path attribute to limit access scope`
220
+ );
221
+ }
222
+ }
223
+
224
+ checkCookieConfigForPath(node, config, cookieName) {
225
+ const pathProperty = this.findProperty(config, "path");
226
+
227
+ if (!pathProperty) {
228
+ this.addViolation(
229
+ node,
230
+ `Session cookie "${cookieName}" should specify Path attribute to limit access scope`
231
+ );
232
+ return;
233
+ }
234
+
235
+ const pathValue = this.extractStringValue(pathProperty.getInitializer());
236
+ if (!pathValue) {
237
+ this.addViolation(
238
+ node,
239
+ `Session cookie "${cookieName}" should have a valid Path attribute value`
240
+ );
241
+ return;
242
+ }
243
+
244
+ // Check if path is too broad (root path "/")
245
+ if (pathValue === this.rootPath) {
246
+ this.addViolation(
247
+ node,
248
+ `Session cookie "${cookieName}" uses root path "/", consider using a more specific path to limit access scope`
249
+ );
250
+ }
251
+ }
252
+
253
+ checkSetCookieHeader(node, headerValue) {
254
+ // Extract cookie name from Set-Cookie header
255
+ const cookieMatch = headerValue.match(/^([^=]+)=/);
256
+ if (!cookieMatch) return;
257
+
258
+ const cookieName = cookieMatch[1].trim();
259
+ if (!this.isSessionCookie(cookieName)) return;
260
+
261
+ // Check if Path attribute is present
262
+ const pathMatch = headerValue.match(/Path=([^;\\s]*)/i);
263
+ if (!pathMatch) {
264
+ this.addViolation(
265
+ node,
266
+ `Session cookie "${cookieName}" in Set-Cookie header should specify Path attribute`
267
+ );
268
+ return;
269
+ }
270
+
271
+ const pathValue = pathMatch[1];
272
+ if (pathValue === this.rootPath) {
273
+ this.addViolation(
274
+ node,
275
+ `Session cookie "${cookieName}" uses root path "/", consider using a more specific path`
276
+ );
277
+ }
278
+ }
279
+
280
+ isSessionCookie(cookieName) {
281
+ const lowerName = cookieName.toLowerCase();
282
+ return this.sessionIndicators.some((indicator) =>
283
+ lowerName.includes(indicator.toLowerCase())
284
+ );
285
+ }
286
+
287
+ extractStringValue(node) {
288
+ if (!node) return null;
289
+
290
+ const kind = node.getKind();
291
+
292
+ if (kind === SyntaxKind.StringLiteral) {
293
+ return node.getLiteralValue();
294
+ }
295
+
296
+ if (kind === SyntaxKind.NoSubstitutionTemplateLiteral) {
297
+ return node.getLiteralValue();
298
+ }
299
+
300
+ return null;
301
+ }
302
+
303
+ findProperty(objectLiteral, propertyName) {
304
+ const properties = objectLiteral.getProperties();
305
+
306
+ for (const property of properties) {
307
+ if (property.getKind() === SyntaxKind.PropertyAssignment) {
308
+ const name = property.getName();
309
+ if (name === propertyName) {
310
+ return property;
311
+ }
312
+ }
313
+ }
314
+
315
+ return null;
316
+ }
317
+
318
+ addViolation(node, message) {
319
+ const start = node.getStart();
320
+ const sourceFile = node.getSourceFile();
321
+ const lineAndColumn = sourceFile.getLineAndColumnAtPos(start);
322
+
323
+ // Debug output to understand position issues
324
+ if (process.env.SUNLINT_DEBUG) {
325
+ console.log(
326
+ `🔍 [S035] Violation at node kind: ${node.getKindName()}, text: "${node
327
+ .getText()
328
+ .substring(0, 50)}..."`
329
+ );
330
+ console.log(
331
+ `🔍 [S035] Position: line ${lineAndColumn.line + 1}, column ${
332
+ lineAndColumn.column + 1
333
+ }`
334
+ );
335
+ }
336
+
337
+ // Fix line number calculation - ts-morph may have offset issues
338
+ // Use actual line calculation based on source file text
339
+ const sourceText = sourceFile.getFullText();
340
+ const actualLine = this.calculateActualLine(sourceText, start);
341
+
342
+ this.violations.push({
343
+ ruleId: this.ruleId,
344
+ ruleName: this.ruleName,
345
+ severity: "warning",
346
+ message: message,
347
+ line: actualLine,
348
+ column: lineAndColumn.column + 1,
349
+ source: "symbol-based",
350
+ });
351
+
352
+ if (process.env.SUNLINT_DEBUG) {
353
+ console.log(`🔍 [S035] Added violation: ${message}`);
354
+ }
355
+ }
356
+
357
+ calculateActualLine(sourceText, position) {
358
+ // Count newlines up to the position to get accurate line number
359
+ let lineCount = 1;
360
+ for (let i = 0; i < position; i++) {
361
+ if (sourceText[i] === "\n") {
362
+ lineCount++;
363
+ }
364
+ }
365
+ return lineCount;
366
+ }
367
+
368
+ cleanup() {
369
+ this.violations = [];
370
+ }
371
+ }
372
+
373
+ module.exports = S035SymbolBasedAnalyzer;
@@ -0,0 +1,222 @@
1
+ # S048 - No Current Password in Reset Process
2
+
3
+ ## Mô tả
4
+
5
+ Rule này kiểm tra xem các quy trình đặt lại mật khẩu có yêu cầu mật khẩu hiện tại hay không. Việc yêu cầu mật khẩu hiện tại trong quy trình reset mật khẩu vi phạm nguyên tắc bảo mật và làm mất đi mục đích của tính năng "quên mật khẩu".
6
+
7
+ ## Mục tiêu
8
+
9
+ - Ngăn chặn việc yêu cầu mật khẩu hiện tại trong quy trình reset mật khẩu
10
+ - Đảm bảo quy trình reset mật khẩu được thiết kế an toàn và hợp lý
11
+ - Tuân thủ OWASP A04:2021 - Insecure Design và CWE-640
12
+
13
+ ## Chi tiết Rule
14
+
15
+ ### Phát hiện lỗi khi:
16
+
17
+ 1. **API endpoints yêu cầu current password trong reset**:
18
+ ```javascript
19
+ app.post('/reset-password', (req, res) => {
20
+ const { currentPassword, newPassword } = req.body; // ❌ Yêu cầu mật khẩu hiện tại
21
+ if (!validateCurrentPassword(currentPassword)) {
22
+ return res.status(400).json({ error: 'Current password incorrect' });
23
+ }
24
+ });
25
+ ```
26
+
27
+ 2. **Form validation yêu cầu current password**:
28
+ ```typescript
29
+ const resetPasswordSchema = {
30
+ currentPassword: { type: String, required: true }, // ❌ Bắt buộc mật khẩu hiện tại
31
+ newPassword: { type: String, required: true }
32
+ };
33
+ ```
34
+
35
+ 3. **Service methods kiểm tra current password trong reset**:
36
+ ```javascript
37
+ async resetPassword(userId, currentPassword, newPassword) {
38
+ const user = await User.findById(userId);
39
+ if (!user.validatePassword(currentPassword)) { // ❌ Validate mật khẩu hiện tại
40
+ throw new Error('Current password is incorrect');
41
+ }
42
+ }
43
+ ```
44
+
45
+ 4. **React components với current password field**:
46
+ ```typescript
47
+ function ResetPasswordForm() {
48
+ return (
49
+ <form>
50
+ <input name="currentPassword" required /> {/* ❌ Trường mật khẩu hiện tại */}
51
+ <input name="newPassword" required />
52
+ </form>
53
+ );
54
+ }
55
+ ```
56
+
57
+ ### Cách khắc phục:
58
+
59
+ 1. **Sử dụng token-based reset**:
60
+ ```javascript
61
+ app.post('/reset-password', (req, res) => {
62
+ const { token, newPassword } = req.body; // ✅ Sử dụng token thay vì current password
63
+ if (!validateResetToken(token)) {
64
+ return res.status(400).json({ error: 'Invalid reset token' });
65
+ }
66
+ });
67
+ ```
68
+
69
+ 2. **Schema với reset token**:
70
+ ```typescript
71
+ const resetPasswordSchema = {
72
+ resetToken: { type: String, required: true }, // ✅ Token reset an toàn
73
+ newPassword: { type: String, required: true }
74
+ };
75
+ ```
76
+
77
+ 3. **Service method an toàn**:
78
+ ```javascript
79
+ async resetPasswordWithToken(resetToken, newPassword) {
80
+ const tokenData = await validateResetToken(resetToken); // ✅ Validate token
81
+ if (!tokenData.valid) {
82
+ throw new Error('Invalid or expired reset token');
83
+ }
84
+ }
85
+ ```
86
+
87
+ 4. **Form với email verification**:
88
+ ```typescript
89
+ function ForgotPasswordForm() {
90
+ return (
91
+ <form>
92
+ <input name="email" type="email" required /> {/* ✅ Chỉ cần email */}
93
+ <button>Send Reset Link</button>
94
+ </form>
95
+ );
96
+ }
97
+ ```
98
+
99
+ ## Tại sao đây là vấn đề bảo mật?
100
+
101
+ ### 1. **Mâu thuẫn logic**
102
+ - Nếu người dùng nhớ mật khẩu hiện tại, họ không cần reset
103
+ - Yêu cầu mật khẩu hiện tại làm vô hiệu hóa tính năng "quên mật khẩu"
104
+
105
+ ### 2. **Tạo điểm yếu bảo mật**
106
+ - Kẻ tấn công có thể lợi dụng để brute force mật khẩu
107
+ - Tăng surface attack cho account takeover
108
+
109
+ ### 3. **Trải nghiệm người dùng kém**
110
+ - Người dùng quên mật khẩu không thể hoàn thành quy trình reset
111
+ - Dẫn đến khóa tài khoản và frustration
112
+
113
+ ## Các trường hợp ngoại lệ
114
+
115
+ ### Trường hợp hợp lệ (không phải lỗi):
116
+
117
+ 1. **Password Change (không phải Reset)**:
118
+ ```javascript
119
+ // ✅ Thay đổi mật khẩu khi đã đăng nhập - hợp lệ
120
+ app.post('/change-password', authenticateUser, (req, res) => {
121
+ const { currentPassword, newPassword } = req.body;
122
+ // Hợp lệ vì đây là thay đổi, không phải reset
123
+ });
124
+ ```
125
+
126
+ 2. **Profile settings**:
127
+ ```javascript
128
+ // ✅ Cập nhật mật khẩu trong settings - hợp lệ
129
+ function ProfileSettings() {
130
+ return (
131
+ <div>
132
+ <h2>Change Password</h2> {/* Đây là change, không phải reset */}
133
+ <input name="currentPassword" />
134
+ <input name="newPassword" />
135
+ </div>
136
+ );
137
+ }
138
+ ```
139
+
140
+ ## Phương pháp detect
141
+
142
+ Rule này sử dụng **heuristic analysis** với các pattern:
143
+
144
+ 1. **Context Detection**: Phát hiện ngữ cảnh password reset
145
+ - Keywords: `reset`, `forgot`, `recover`, `forgotpassword`
146
+ - Endpoints: `/reset-password`, `/forgot-password`
147
+ - Functions: `resetPassword()`, `forgotPassword()`
148
+
149
+ 2. **Violation Detection**: Tìm yêu cầu current password
150
+ - Field names: `currentPassword`, `oldPassword`, `existingPassword`
151
+ - Validation patterns: `validateCurrentPassword()`, `checkOldPassword()`
152
+ - Schema fields: `currentPassword: { required: true }`
153
+
154
+ 3. **Context Filtering**: Loại bỏ false positives
155
+ - Bỏ qua password change contexts
156
+ - Bỏ qua test files và documentation
157
+ - Bỏ qua comments và type definitions
158
+
159
+ ## Tham khảo
160
+
161
+ - [OWASP A04:2021 - Insecure Design](https://owasp.org/Top10/A04_2021-Insecure_Design/)
162
+ - [CWE-640: Weak Password Recovery Mechanism for Forgotten Password](https://cwe.mitre.org/data/definitions/640.html)
163
+ - [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#forgot-password)
164
+ - [NIST SP 800-63B - Digital Identity Guidelines](https://pages.nist.gov/800-63-3/sp800-63b.html#sec5)
165
+
166
+ ## Ví dụ
167
+
168
+ ### Violation Examples
169
+
170
+ ```javascript
171
+ // ❌ Express.js với current password requirement
172
+ app.post('/reset-password', (req, res) => {
173
+ if (!req.body.currentPassword) {
174
+ return res.status(400).json({ error: 'Current password required' });
175
+ }
176
+ });
177
+
178
+ // ❌ NestJS với validation current password
179
+ @Post('reset-password')
180
+ async resetPassword(@Body() data: { currentPassword: string, newPassword: string }) {
181
+ await this.authService.validateCurrentPassword(data.currentPassword);
182
+ }
183
+
184
+ // ❌ Mongoose schema yêu cầu current password
185
+ const resetSchema = new Schema({
186
+ currentPassword: { type: String, required: true }, // Vi phạm
187
+ newPassword: { type: String, required: true }
188
+ });
189
+
190
+ // ❌ React form với current password field
191
+ <input
192
+ name="currentPassword"
193
+ placeholder="Enter current password"
194
+ required
195
+ />
196
+ ```
197
+
198
+ ### Secure Examples
199
+
200
+ ```javascript
201
+ // ✅ Token-based reset
202
+ app.post('/reset-password', (req, res) => {
203
+ const { token, newPassword } = req.body;
204
+ if (!validateResetToken(token)) {
205
+ return res.status(400).json({ error: 'Invalid reset token' });
206
+ }
207
+ });
208
+
209
+ // ✅ Email-based forgot password
210
+ app.post('/forgot-password', (req, res) => {
211
+ const { email } = req.body;
212
+ sendResetEmail(email);
213
+ res.json({ message: 'Reset link sent to email' });
214
+ });
215
+
216
+ // ✅ Secure reset schema
217
+ const resetSchema = new Schema({
218
+ resetToken: { type: String, required: true },
219
+ newPassword: { type: String, required: true },
220
+ tokenExpiry: { type: Date, required: true }
221
+ });
222
+ ```