@sun-asterisk/sunlint 1.2.2 → 1.3.1

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 +107 -1
  2. package/CONTRIBUTING.md +1654 -66
  3. package/README.md +19 -6
  4. package/config/ci-cd.json +54 -0
  5. package/config/development.json +56 -0
  6. package/config/engines/engines-enhanced.json +86 -0
  7. package/config/engines/semantic-config.json +114 -0
  8. package/config/eslint-rule-mapping.json +50 -38
  9. package/config/large-project.json +143 -0
  10. package/config/presets/all.json +0 -1
  11. package/config/release.json +70 -0
  12. package/config/rule-analysis-strategies.js +23 -4
  13. package/config/rules/S027-categories.json +122 -0
  14. package/config/rules/enhanced-rules-registry.json +2564 -0
  15. package/config/rules/rules-registry-generated.json +785 -837
  16. package/config/rules/rules-registry.json +13 -1
  17. package/core/adapters/sunlint-rule-adapter.js +25 -30
  18. package/core/analysis-orchestrator.js +42 -2
  19. package/core/categories.js +52 -0
  20. package/core/category-constants.js +39 -0
  21. package/core/cli-action-handler.js +53 -32
  22. package/core/cli-program.js +11 -3
  23. package/core/config-manager.js +111 -0
  24. package/core/config-merger.js +88 -0
  25. package/core/constants/categories.js +168 -0
  26. package/core/constants/defaults.js +165 -0
  27. package/core/constants/engines.js +185 -0
  28. package/core/constants/index.js +30 -0
  29. package/core/constants/rules.js +215 -0
  30. package/core/enhanced-rules-registry.js +3 -3
  31. package/core/file-targeting-service.js +128 -7
  32. package/core/interfaces/rule-plugin.interface.js +207 -0
  33. package/core/plugin-manager.js +448 -0
  34. package/core/rule-selection-service.js +42 -15
  35. package/core/semantic-engine.js +658 -0
  36. package/core/semantic-rule-base.js +433 -0
  37. package/core/unified-rule-registry.js +484 -0
  38. package/docs/COMMAND-EXAMPLES.md +134 -0
  39. package/docs/CONSTANTS-ARCHITECTURE.md +288 -0
  40. package/docs/LARGE-PROJECT-GUIDE.md +324 -0
  41. package/engines/core/base-engine.js +249 -0
  42. package/engines/engine-factory.js +275 -0
  43. package/engines/eslint-engine.js +171 -19
  44. package/engines/heuristic-engine.js +569 -78
  45. package/integrations/eslint/plugin/index.js +26 -28
  46. package/origin-rules/common-en.md +8 -8
  47. package/package.json +10 -6
  48. package/rules/common/C003_no_vague_abbreviations/analyzer.js +1 -1
  49. package/rules/common/C017_constructor_logic/analyzer.js +254 -17
  50. package/rules/common/C017_constructor_logic/semantic-analyzer.js +340 -0
  51. package/rules/common/C029_catch_block_logging/analyzer.js +17 -5
  52. package/rules/common/C033_separate_service_repository/README.md +78 -0
  53. package/rules/common/C033_separate_service_repository/analyzer.js +160 -0
  54. package/rules/common/C033_separate_service_repository/config.json +50 -0
  55. package/rules/common/C033_separate_service_repository/regex-based-analyzer.js +585 -0
  56. package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +368 -0
  57. package/rules/common/C035_error_logging_context/STRATEGY.md +99 -0
  58. package/rules/common/C035_error_logging_context/analyzer.js +230 -0
  59. package/rules/common/C035_error_logging_context/config.json +54 -0
  60. package/rules/common/C035_error_logging_context/regex-based-analyzer.js +299 -0
  61. package/rules/common/C035_error_logging_context/symbol-based-analyzer.js +454 -0
  62. package/rules/common/C040_centralized_validation/analyzer.js +165 -0
  63. package/rules/common/C040_centralized_validation/config.json +46 -0
  64. package/rules/common/C040_centralized_validation/regex-based-analyzer.js +243 -0
  65. package/rules/common/C040_centralized_validation/symbol-based-analyzer.js +416 -0
  66. package/rules/common/C047_no_duplicate_retry_logic/c047-semantic-rule.js +278 -0
  67. package/rules/common/C047_no_duplicate_retry_logic/symbol-analyzer-enhanced.js +968 -0
  68. package/rules/common/C047_no_duplicate_retry_logic/symbol-config.json +71 -0
  69. package/rules/common/{C076_single_test_behavior → C072_single_test_behavior}/analyzer.js +6 -6
  70. package/rules/common/C076_explicit_function_types/README.md +30 -0
  71. package/rules/common/C076_explicit_function_types/analyzer.js +172 -0
  72. package/rules/common/C076_explicit_function_types/config.json +15 -0
  73. package/rules/common/C076_explicit_function_types/semantic-analyzer.js +341 -0
  74. package/rules/index.js +8 -0
  75. package/rules/parser/rule-parser.js +13 -2
  76. package/rules/security/S005_no_origin_auth/README.md +226 -0
  77. package/rules/security/S005_no_origin_auth/analyzer.js +184 -0
  78. package/rules/security/S005_no_origin_auth/ast-analyzer.js +406 -0
  79. package/rules/security/S005_no_origin_auth/config.json +85 -0
  80. package/rules/security/S006_no_plaintext_recovery_codes/README.md +139 -0
  81. package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +306 -0
  82. package/rules/security/S006_no_plaintext_recovery_codes/config.json +48 -0
  83. package/rules/security/S007_no_plaintext_otp/README.md +198 -0
  84. package/rules/security/S007_no_plaintext_otp/analyzer.js +406 -0
  85. package/rules/security/S007_no_plaintext_otp/config.json +79 -0
  86. package/rules/security/S007_no_plaintext_otp/semantic-analyzer.js +609 -0
  87. package/rules/security/S007_no_plaintext_otp/semantic-config.json +195 -0
  88. package/rules/security/S007_no_plaintext_otp/semantic-wrapper.js +280 -0
  89. package/rules/security/S027_no_hardcoded_secrets/analyzer.js +180 -366
  90. package/rules/security/S027_no_hardcoded_secrets/categories.json +153 -0
  91. package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +250 -0
  92. package/scripts/category-manager.js +150 -0
  93. package/scripts/generate-rules-registry.js +88 -0
  94. package/scripts/migrate-rule-registry.js +157 -0
  95. package/scripts/prepare-release.sh +1 -1
  96. package/scripts/validate-system.js +48 -0
  97. package/.sunlint.json +0 -35
  98. package/config/README.md +0 -88
  99. package/config/engines/eslint-rule-mapping.json +0 -74
  100. package/config/schemas/sunlint-schema.json +0 -0
  101. package/config/testing/test-s005-working.ts +0 -22
  102. package/core/multi-rule-runner.js +0 -0
  103. package/docs/ESLINT-INTEGRATION-STRATEGY.md +0 -392
  104. package/docs/FUTURE_PACKAGES.md +0 -83
  105. package/docs/HEURISTIC_VS_AI.md +0 -113
  106. package/docs/PRODUCTION_DEPLOYMENT_ANALYSIS.md +0 -112
  107. package/docs/PRODUCTION_SIZE_IMPACT.md +0 -183
  108. package/docs/RELEASE_GUIDE.md +0 -230
  109. package/docs/STANDARDIZED-CATEGORY-FILTERING.md +0 -156
  110. package/engines/tree-sitter-parser.js +0 -0
  111. package/engines/universal-ast-engine.js +0 -0
  112. package/integrations/eslint/plugin/rules/common/c076-single-behavior-per-test.js +0 -254
  113. package/rules/common/C029_catch_block_logging/analyzer-backup.js +0 -426
  114. package/rules/common/C029_catch_block_logging/analyzer-fixed.js +0 -130
  115. package/rules/common/C029_catch_block_logging/analyzer-multi-tech.js +0 -487
  116. package/rules/common/C029_catch_block_logging/analyzer-simple.js +0 -110
  117. package/rules/common/C029_catch_block_logging/ast-analyzer-backup.js +0 -441
  118. package/rules/common/C029_catch_block_logging/ast-analyzer-new.js +0 -127
  119. package/rules/common/C029_catch_block_logging/ast-analyzer.js +0 -133
  120. package/rules/common/C029_catch_block_logging/cfg-analyzer.js +0 -408
  121. package/rules/common/C029_catch_block_logging/dataflow-analyzer.js +0 -454
  122. package/rules/common/C029_catch_block_logging/multi-language-ast-engine.js +0 -700
  123. package/rules/common/C029_catch_block_logging/pattern-learning-analyzer.js +0 -568
  124. package/rules/common/C029_catch_block_logging/semantic-analyzer.js +0 -459
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Heuristic analyzer for S006 - No Plaintext Recovery/Activation Codes
3
+ * Purpose: Detect sending recovery codes, activation codes, or reset codes in plaintext
4
+ * Based on OWASP A02:2021 - Cryptographic Failures
5
+ */
6
+
7
+ class S006Analyzer {
8
+ constructor() {
9
+ this.ruleId = 'S006';
10
+ this.ruleName = 'No Plaintext Recovery/Activation Codes';
11
+ this.description = 'Do not send recovery or activation codes in plaintext';
12
+
13
+ // Keywords that indicate sensitive codes
14
+ this.sensitiveCodeKeywords = [
15
+ 'recovery', 'activation', 'reset', 'verification', 'confirm', 'verify',
16
+ 'otp', 'totp', 'code', 'pin', 'token', 'secret', 'key', 'password'
17
+ ];
18
+
19
+ // Keywords that indicate code sending/transmission
20
+ this.sendingKeywords = [
21
+ 'send', 'email', 'sms', 'text', 'message', 'mail', 'push', 'notify',
22
+ 'transmit', 'deliver', 'dispatch', 'forward', 'post', 'put', 'create',
23
+ 'response', 'body', 'content', 'payload', 'data'
24
+ ];
25
+
26
+ // Patterns that indicate plaintext transmission
27
+ this.plaintextPatterns = [
28
+ // Email/SMS sending with codes
29
+ /(?:send|email|sms|text|message).*(?:recovery|activation|reset|verification|otp|code|pin)/i,
30
+ /(?:recovery|activation|reset|verification|otp|code|pin).*(?:send|email|sms|text|message)/i,
31
+
32
+ // HTTP responses with codes in body
33
+ /(?:response|body|json|data|payload).*(?:recovery|activation|reset|verification|otp|code|pin)/i,
34
+ /(?:recovery|activation|reset|verification|otp|code|pin).*(?:response|body|json|data|payload)/i,
35
+
36
+ // Direct code exposure in strings
37
+ /".*(?:recovery|activation|reset|verification|otp|code|pin).*"/i,
38
+ /'.*(?:recovery|activation|reset|verification|otp|code|pin).*'/i,
39
+ /`.*(?:recovery|activation|reset|verification|otp|code|pin).*`/i,
40
+
41
+ // Template strings with codes
42
+ /\$\{.*(?:recovery|activation|reset|verification|otp|code|pin).*\}/i,
43
+
44
+ // API endpoint responses
45
+ /return.*(?:recovery|activation|reset|verification|otp|code|pin)/i,
46
+ /res\.(?:send|json|end).*(?:recovery|activation|reset|verification|otp|code|pin)/i,
47
+ ];
48
+
49
+ // Patterns that should be excluded (safe practices)
50
+ this.safePatterns = [
51
+ // Hashed or encrypted codes
52
+ /hash|encrypt|cipher|bcrypt|crypto|secure/i,
53
+
54
+ // Environment variables or config
55
+ /process\.env|config\.|getenv/i,
56
+
57
+ // Database storage (not transmission)
58
+ /save|store|insert|update|database|db\./i,
59
+
60
+ // Logging patterns (depends on context but often acceptable for debugging)
61
+ /log|debug|trace|console/i,
62
+
63
+ // Comments and documentation
64
+ /\/\/|\/\*|\*\/|@param|@return|@example/,
65
+
66
+ // Type definitions and interfaces
67
+ /interface|type|enum|class.*\{/i,
68
+
69
+ // Import/export statements
70
+ /import|export|require|module\.exports/i,
71
+
72
+ // Safe message patterns (no actual codes exposed)
73
+ /instructions sent|sent to|check your|please enter|has been sent|successfully sent|we've sent|click the link|enter the code|will expire/i,
74
+
75
+ // Configuration and constants
76
+ /const\s+\w+\s*=|enum\s+\w+|type\s+\w+/i,
77
+
78
+ // Function definitions
79
+ /function\s+\w+|async\s+\w+|\w+\s*\(/i,
80
+
81
+ // Return statements with safe messages
82
+ /return\s*\{[^}]*success[^}]*\}/i,
83
+ ];
84
+
85
+ // Common safe variable names that might contain keywords
86
+ this.safeVariableNames = [
87
+ /^(is|has|can|should|will|enable|disable|show|hide|display).*code/i,
88
+ /^.*type$/i,
89
+ /^.*config$/i,
90
+ /^.*setting$/i,
91
+ /^.*option$/i,
92
+ /^.*flag$/i,
93
+ ];
94
+ }
95
+
96
+ async analyze(files, language, options = {}) {
97
+ const violations = [];
98
+
99
+ for (const filePath of files) {
100
+ // Skip test files, build directories, and node_modules
101
+ if (this.shouldSkipFile(filePath)) {
102
+ continue;
103
+ }
104
+
105
+ try {
106
+ const content = require('fs').readFileSync(filePath, 'utf8');
107
+ const fileViolations = this.analyzeFile(content, filePath, options);
108
+ violations.push(...fileViolations);
109
+ } catch (error) {
110
+ if (options.verbose) {
111
+ console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
112
+ }
113
+ }
114
+ }
115
+
116
+ return violations;
117
+ }
118
+
119
+ shouldSkipFile(filePath) {
120
+ const skipPatterns = [
121
+ 'test/', 'tests/', '__tests__/', '.test.', '.spec.',
122
+ 'node_modules/', 'build/', 'dist/', '.next/', 'coverage/',
123
+ 'vendor/', 'mocks/', '.mock.'
124
+ // Removed 'fixtures/' to allow testing
125
+ ];
126
+
127
+ return skipPatterns.some(pattern => filePath.includes(pattern));
128
+ }
129
+
130
+ analyzeFile(content, filePath, options = {}) {
131
+ const violations = [];
132
+ const lines = content.split('\n');
133
+
134
+ lines.forEach((line, index) => {
135
+ const lineNumber = index + 1;
136
+ const trimmedLine = line.trim();
137
+
138
+ // Skip comments, imports, and empty lines
139
+ if (this.shouldSkipLine(trimmedLine)) {
140
+ return;
141
+ }
142
+
143
+ // Check for potential plaintext code transmission
144
+ const violation = this.checkForPlaintextCodeTransmission(line, lineNumber, filePath);
145
+ if (violation) {
146
+ violations.push(violation);
147
+ }
148
+ });
149
+
150
+ return violations;
151
+ }
152
+
153
+ shouldSkipLine(line) {
154
+ // Skip comments, imports, and other non-code lines
155
+ return (
156
+ line.length === 0 ||
157
+ line.startsWith('//') ||
158
+ line.startsWith('/*') ||
159
+ line.startsWith('*') ||
160
+ line.startsWith('import ') ||
161
+ line.startsWith('export ') ||
162
+ line.startsWith('require(') ||
163
+ line.includes('module.exports')
164
+ );
165
+ }
166
+
167
+ checkForPlaintextCodeTransmission(line, lineNumber, filePath) {
168
+ const lowerLine = line.toLowerCase();
169
+
170
+ // First check if line contains safe patterns (early exit)
171
+ if (this.containsSafePattern(line)) {
172
+ return null;
173
+ }
174
+
175
+ // Check for variable assignments with sensitive names
176
+ const sensitiveAssignment = this.checkSensitiveAssignment(line, lineNumber, filePath);
177
+ if (sensitiveAssignment) {
178
+ return sensitiveAssignment;
179
+ }
180
+
181
+ // Check for direct plaintext patterns
182
+ for (const pattern of this.plaintextPatterns) {
183
+ if (pattern.test(line)) {
184
+ // Additional context check to reduce false positives
185
+ if (this.hasTransmissionContext(line)) {
186
+ return {
187
+ ruleId: this.ruleId,
188
+ severity: 'error',
189
+ message: 'Recovery/activation codes should not be transmitted in plaintext. Use encrypted channels or hash the codes.',
190
+ line: lineNumber,
191
+ column: this.findPatternColumn(line, pattern),
192
+ filePath: filePath,
193
+ type: 'plaintext_code_transmission',
194
+ details: 'Consider using encrypted communication or sending only hashed/masked versions of sensitive codes.'
195
+ };
196
+ }
197
+ }
198
+ }
199
+
200
+ return null;
201
+ }
202
+
203
+ containsSafePattern(line) {
204
+ return this.safePatterns.some(pattern => pattern.test(line));
205
+ }
206
+
207
+ checkSensitiveAssignment(line, lineNumber, filePath) {
208
+ // Look for variable assignments that combine sensitive codes with transmission
209
+ const assignmentMatch = line.match(/(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(.+)/);
210
+ if (!assignmentMatch) {
211
+ return null;
212
+ }
213
+
214
+ const [, variableName, valueExpr] = assignmentMatch;
215
+ const lowerVarName = variableName.toLowerCase();
216
+ const lowerValueExpr = valueExpr.toLowerCase();
217
+
218
+ // Skip safe variable names
219
+ if (this.safeVariableNames.some(pattern => pattern.test(lowerVarName))) {
220
+ return null;
221
+ }
222
+
223
+ // Check if variable name suggests code transmission
224
+ const hasSensitiveCodeKeyword = this.sensitiveCodeKeywords.some(keyword =>
225
+ lowerVarName.includes(keyword)
226
+ );
227
+
228
+ const hasSendingKeyword = this.sendingKeywords.some(keyword =>
229
+ lowerVarName.includes(keyword) || lowerValueExpr.includes(keyword)
230
+ );
231
+
232
+ if (hasSensitiveCodeKeyword && hasSendingKeyword) {
233
+ // Check if the value looks like it contains actual codes or sensitive data
234
+ if (this.valueContainsCodes(valueExpr)) {
235
+ return {
236
+ ruleId: this.ruleId,
237
+ severity: 'warning',
238
+ message: `Variable '${variableName}' appears to handle sensitive codes for transmission. Ensure codes are encrypted or hashed.`,
239
+ line: lineNumber,
240
+ column: line.indexOf(variableName) + 1,
241
+ filePath: filePath,
242
+ type: 'sensitive_code_variable',
243
+ variableName: variableName,
244
+ details: 'Consider encrypting sensitive codes before transmission or use secure communication channels.'
245
+ };
246
+ }
247
+ }
248
+
249
+ return null;
250
+ }
251
+
252
+ hasTransmissionContext(line) {
253
+ const transmissionIndicators = [
254
+ // HTTP response methods
255
+ /res\.(?:send|json|status|end)/i,
256
+ /response\.(?:send|json|status|end)/i,
257
+ /return.*(?:json|response|status)/i,
258
+
259
+ // Email/SMS functions
260
+ /(?:sendEmail|sendSMS|sendMessage|notify|mail)/i,
261
+
262
+ // Template rendering
263
+ /render|template|view|html|email/i,
264
+
265
+ // API responses
266
+ /\.json\(|\.send\(|\.end\(/,
267
+
268
+ // String concatenation or template literals with codes
269
+ /\+.*['"`]|['"`].*\+|\$\{.*\}/,
270
+ ];
271
+
272
+ return transmissionIndicators.some(indicator => indicator.test(line));
273
+ }
274
+
275
+ valueContainsCodes(valueExpr) {
276
+ // Check if the value expression contains actual code patterns or user data
277
+ const codePatterns = [
278
+ // Template strings with variables
279
+ /\$\{[^}]+\}/,
280
+
281
+ // String concatenation
282
+ /\+\s*[a-zA-Z_$]/,
283
+
284
+ // Function calls that might return codes
285
+ /\w+\([^)]*\)/,
286
+
287
+ // Property access that might be codes
288
+ /\w+\.\w+/,
289
+
290
+ // Array/object access
291
+ /\[.*\]/,
292
+
293
+ // Direct string literals that look like codes (6+ chars with mixed case/numbers)
294
+ /['"`][a-zA-Z0-9]{6,}['"`]/,
295
+ ];
296
+
297
+ return codePatterns.some(pattern => pattern.test(valueExpr));
298
+ }
299
+
300
+ findPatternColumn(line, pattern) {
301
+ const match = pattern.exec(line);
302
+ return match ? match.index + 1 : 1;
303
+ }
304
+ }
305
+
306
+ module.exports = S006Analyzer;
@@ -0,0 +1,48 @@
1
+ {
2
+ "ruleId": "S006",
3
+ "name": "No Plaintext Recovery/Activation Codes",
4
+ "description": "Do not send recovery or activation codes in plaintext",
5
+ "category": "security",
6
+ "severity": "error",
7
+ "languages": ["All languages"],
8
+ "tags": ["security", "owasp", "cryptographic-failures", "authentication"],
9
+ "enabled": true,
10
+ "fixable": false,
11
+ "engine": "heuristic",
12
+ "metadata": {
13
+ "owaspCategory": "A02:2021 - Cryptographic Failures",
14
+ "cweId": "CWE-319",
15
+ "description": "Sending recovery codes, activation codes, or reset codes in plaintext over insecure channels can lead to account takeover attacks. These sensitive codes should be encrypted during transmission or sent through secure channels.",
16
+ "impact": "High - Account takeover, unauthorized access",
17
+ "likelihood": "Medium",
18
+ "remediation": "Use encrypted communication channels, hash codes before transmission, or implement time-limited secure tokens"
19
+ },
20
+ "patterns": {
21
+ "vulnerable": [
22
+ "Sending activation codes in email body as plaintext",
23
+ "Including reset codes in unencrypted API responses",
24
+ "Transmitting OTP codes without encryption",
25
+ "Exposing verification codes in logs or debug output"
26
+ ],
27
+ "secure": [
28
+ "Using encrypted email for code transmission",
29
+ "Hashing codes before database storage",
30
+ "Implementing secure token-based authentication",
31
+ "Using HTTPS for all code-related API endpoints"
32
+ ]
33
+ },
34
+ "examples": {
35
+ "violations": [
36
+ "res.json({ resetCode: user.resetCode });",
37
+ "await sendEmail(`Your activation code is: ${activationCode}`);",
38
+ "const message = `OTP: ${otp}`;",
39
+ "console.log('Recovery code:', recoveryCode);"
40
+ ],
41
+ "fixes": [
42
+ "res.json({ message: 'Reset code sent to email' });",
43
+ "await sendEncryptedEmail(activationCode);",
44
+ "const hashedOtp = await hash(otp);",
45
+ "logger.info('Recovery code sent successfully');"
46
+ ]
47
+ }
48
+ }
@@ -0,0 +1,198 @@
1
+ # S007: No Plaintext OTP
2
+
3
+ ## Mô tả
4
+
5
+ Rule này phát hiện việc lưu trữ hoặc truyền tải OTP (One-Time Password) codes dưới dạng plaintext. Theo OWASP A02:2021 - Cryptographic Failures, việc lưu trữ OTP không được mã hóa có thể dẫn đến các cuộc tấn công chiếm quyền tài khoản.
6
+
7
+ ## Vấn đề bảo mật
8
+
9
+ - **CWE-256**: Unprotected Storage of Credentials
10
+ - **OWASP A02:2021**: Cryptographic Failures
11
+ - **Impact**: Cao - Chiếm quyền tài khoản, truy cập trái phép vào các tài khoản được bảo vệ bằng 2FA
12
+ - **Likelihood**: Trung bình
13
+
14
+ ## Các trường hợp vi phạm
15
+
16
+ ### 1. Lưu trữ OTP plaintext trong database
17
+
18
+ ❌ **Vi phạm:**
19
+ ```javascript
20
+ const otpCode = '123456';
21
+ await db.users.update({ userId }, { otpCode });
22
+
23
+ // Hoặc
24
+ const query = `INSERT INTO users (otp) VALUES ('${otp}')`;
25
+ ```
26
+
27
+ ✅ **Đúng cách:**
28
+ ```javascript
29
+ const hashedOtp = await bcrypt.hash(otpCode, 10);
30
+ await db.users.update({ userId }, { otpCode: hashedOtp });
31
+
32
+ // Hoặc
33
+ const salt = crypto.randomBytes(16).toString('hex');
34
+ const hashedOtp = crypto.createHash('sha256').update(otp + salt).digest('hex');
35
+ const query = `INSERT INTO users (otp_hash, salt) VALUES ('${hashedOtp}', '${salt}')`;
36
+ ```
37
+
38
+ ### 2. Trả về OTP trong API response
39
+
40
+ ❌ **Vi phạm:**
41
+ ```javascript
42
+ res.json({
43
+ success: true,
44
+ otp: user.otpCode,
45
+ verificationCode: generatedOtp
46
+ });
47
+ ```
48
+
49
+ ✅ **Đúng cách:**
50
+ ```javascript
51
+ res.json({
52
+ success: true,
53
+ message: 'OTP sent to registered phone number',
54
+ otpSent: true
55
+ });
56
+ ```
57
+
58
+ ### 3. Lưu trữ OTP trong localStorage/sessionStorage
59
+
60
+ ❌ **Vi phạm:**
61
+ ```javascript
62
+ localStorage.setItem('otp', otpCode);
63
+ sessionStorage.otpCode = generatedOtp;
64
+ ```
65
+
66
+ ✅ **Đúng cách:**
67
+ ```javascript
68
+ const encryptedOtp = encrypt(otpCode);
69
+ localStorage.setItem('otp', encryptedOtp);
70
+
71
+ // Hoặc tốt hơn: không lưu OTP trong browser storage
72
+ // Chỉ lưu token đã mã hóa hoặc session identifier
73
+ ```
74
+
75
+ ### 4. Ghi log OTP codes
76
+
77
+ ❌ **Vi phạm:**
78
+ ```javascript
79
+ console.log('User OTP:', otpCode);
80
+ logger.info(`Generated OTP: ${otp} for user ${userId}`);
81
+ ```
82
+
83
+ ✅ **Đúng cách:**
84
+ ```javascript
85
+ console.log('OTP sent successfully to user');
86
+ logger.info(`OTP generated and sent to user ${userId}`);
87
+ ```
88
+
89
+ ### 5. Gửi OTP qua email/SMS không mã hóa
90
+
91
+ ❌ **Vi phạm:**
92
+ ```javascript
93
+ await sendSMS(`Your OTP is: ${otpCode}`);
94
+ await sendEmail('OTP Verification', `Your code: ${otp}`);
95
+ ```
96
+
97
+ ✅ **Đúng cách:**
98
+ ```javascript
99
+ await sendEncryptedSMS(otpCode); // Sử dụng kênh mã hóa
100
+ await sendSecureEmail('OTP Verification', { otp: encryptedOtp });
101
+ ```
102
+
103
+ ## Cách khắc phục
104
+
105
+ ### 1. Mã hóa OTP trước khi lưu trữ
106
+
107
+ ```javascript
108
+ // Sử dụng bcrypt
109
+ const hashedOtp = await bcrypt.hash(otpCode, 10);
110
+
111
+ // Sử dụng crypto với salt
112
+ const salt = crypto.randomBytes(16).toString('hex');
113
+ const hashedOtp = crypto.createHash('sha256').update(otp + salt).digest('hex');
114
+
115
+ // Sử dụng HMAC
116
+ const hmac = crypto.createHmac('sha256', secretKey);
117
+ hmac.update(otp);
118
+ const hashedOtp = hmac.digest('hex');
119
+ ```
120
+
121
+ ### 2. Sử dụng token thay vì OTP plaintext
122
+
123
+ ```javascript
124
+ // Tạo encrypted token thay vì lưu OTP trực tiếp
125
+ const otpToken = jwt.sign(
126
+ { userId, otp: hashedOtp, exp: Date.now() + 300000 }, // 5 phút
127
+ process.env.JWT_SECRET
128
+ );
129
+
130
+ // Lưu token thay vì OTP
131
+ await db.otpTokens.create({ userId, token: otpToken });
132
+ ```
133
+
134
+ ### 3. Xác thực OTP an toàn
135
+
136
+ ```javascript
137
+ // Thay vì so sánh plaintext
138
+ async function verifyOtp(userId, inputOtp) {
139
+ const user = await db.users.findOne({ userId });
140
+
141
+ // So sánh với hash
142
+ const isValid = await bcrypt.compare(inputOtp, user.otpHash);
143
+
144
+ if (isValid) {
145
+ // Xóa OTP sau khi sử dụng
146
+ await db.users.update({ userId }, { otpHash: null });
147
+ }
148
+
149
+ return isValid;
150
+ }
151
+ ```
152
+
153
+ ### 4. Sử dụng thư viện OTP chuyên dụng
154
+
155
+ ```javascript
156
+ const speakeasy = require('speakeasy');
157
+
158
+ // Tạo TOTP (Time-based OTP)
159
+ const secret = speakeasy.generateSecret({
160
+ name: 'Your App',
161
+ length: 20
162
+ });
163
+
164
+ // Xác thực TOTP
165
+ const verified = speakeasy.totp.verify({
166
+ secret: user.secret,
167
+ encoding: 'base32',
168
+ token: userInputToken,
169
+ window: 2
170
+ });
171
+ ```
172
+
173
+ ## Cấu hình rule
174
+
175
+ ```json
176
+ {
177
+ "S007": {
178
+ "checkStorage": true,
179
+ "checkTransmission": true,
180
+ "checkLogging": true,
181
+ "checkResponses": true,
182
+ "checkLocalStorage": true,
183
+ "strictMode": false
184
+ }
185
+ }
186
+ ```
187
+
188
+ ## Các rule liên quan
189
+
190
+ - **S006**: No Plaintext Recovery/Activation Codes
191
+ - **S012**: No Hardcoded Secrets
192
+ - **S027**: No Hardcoded Secrets Advanced
193
+
194
+ ## Tài liệu tham khảo
195
+
196
+ - [OWASP Top 10 2021 - A02 Cryptographic Failures](https://owasp.org/Top10/A02_2021-Cryptographic_Failures/)
197
+ - [CWE-256: Unprotected Storage of Credentials](https://cwe.mitre.org/data/definitions/256.html)
198
+ - [NIST SP 800-63B Authentication Guidelines](https://pages.nist.gov/800-63-3/sp800-63b.html)