@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,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)
|