@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.
- package/CHANGELOG.md +107 -1
- package/CONTRIBUTING.md +1654 -66
- package/README.md +19 -6
- package/config/ci-cd.json +54 -0
- package/config/development.json +56 -0
- package/config/engines/engines-enhanced.json +86 -0
- package/config/engines/semantic-config.json +114 -0
- package/config/eslint-rule-mapping.json +50 -38
- 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 +23 -4
- package/config/rules/S027-categories.json +122 -0
- package/config/rules/enhanced-rules-registry.json +2564 -0
- package/config/rules/rules-registry-generated.json +785 -837
- package/config/rules/rules-registry.json +13 -1
- package/core/adapters/sunlint-rule-adapter.js +25 -30
- package/core/analysis-orchestrator.js +42 -2
- package/core/categories.js +52 -0
- package/core/category-constants.js +39 -0
- package/core/cli-action-handler.js +53 -32
- package/core/cli-program.js +11 -3
- package/core/config-manager.js +111 -0
- package/core/config-merger.js +88 -0
- package/core/constants/categories.js +168 -0
- package/core/constants/defaults.js +165 -0
- package/core/constants/engines.js +185 -0
- package/core/constants/index.js +30 -0
- package/core/constants/rules.js +215 -0
- package/core/enhanced-rules-registry.js +3 -3
- package/core/file-targeting-service.js +128 -7
- package/core/interfaces/rule-plugin.interface.js +207 -0
- package/core/plugin-manager.js +448 -0
- package/core/rule-selection-service.js +42 -15
- package/core/semantic-engine.js +658 -0
- package/core/semantic-rule-base.js +433 -0
- package/core/unified-rule-registry.js +484 -0
- package/docs/COMMAND-EXAMPLES.md +134 -0
- package/docs/CONSTANTS-ARCHITECTURE.md +288 -0
- package/docs/LARGE-PROJECT-GUIDE.md +324 -0
- package/engines/core/base-engine.js +249 -0
- package/engines/engine-factory.js +275 -0
- package/engines/eslint-engine.js +171 -19
- package/engines/heuristic-engine.js +569 -78
- package/integrations/eslint/plugin/index.js +26 -28
- package/origin-rules/common-en.md +8 -8
- package/package.json +10 -6
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +1 -1
- package/rules/common/C017_constructor_logic/analyzer.js +254 -17
- package/rules/common/C017_constructor_logic/semantic-analyzer.js +340 -0
- package/rules/common/C029_catch_block_logging/analyzer.js +17 -5
- 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 +230 -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/C047_no_duplicate_retry_logic/c047-semantic-rule.js +278 -0
- package/rules/common/C047_no_duplicate_retry_logic/symbol-analyzer-enhanced.js +968 -0
- package/rules/common/C047_no_duplicate_retry_logic/symbol-config.json +71 -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 +8 -0
- 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/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/scripts/category-manager.js +150 -0
- package/scripts/generate-rules-registry.js +88 -0
- package/scripts/migrate-rule-registry.js +157 -0
- package/scripts/prepare-release.sh +1 -1
- package/scripts/validate-system.js +48 -0
- package/.sunlint.json +0 -35
- package/config/README.md +0 -88
- package/config/engines/eslint-rule-mapping.json +0 -74
- package/config/schemas/sunlint-schema.json +0 -0
- package/config/testing/test-s005-working.ts +0 -22
- package/core/multi-rule-runner.js +0 -0
- 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/engines/tree-sitter-parser.js +0 -0
- package/engines/universal-ast-engine.js +0 -0
- package/integrations/eslint/plugin/rules/common/c076-single-behavior-per-test.js +0 -254
- package/rules/common/C029_catch_block_logging/analyzer-backup.js +0 -426
- package/rules/common/C029_catch_block_logging/analyzer-fixed.js +0 -130
- package/rules/common/C029_catch_block_logging/analyzer-multi-tech.js +0 -487
- package/rules/common/C029_catch_block_logging/analyzer-simple.js +0 -110
- package/rules/common/C029_catch_block_logging/ast-analyzer-backup.js +0 -441
- package/rules/common/C029_catch_block_logging/ast-analyzer-new.js +0 -127
- package/rules/common/C029_catch_block_logging/ast-analyzer.js +0 -133
- package/rules/common/C029_catch_block_logging/cfg-analyzer.js +0 -408
- package/rules/common/C029_catch_block_logging/dataflow-analyzer.js +0 -454
- package/rules/common/C029_catch_block_logging/multi-language-ast-engine.js +0 -700
- package/rules/common/C029_catch_block_logging/pattern-learning-analyzer.js +0 -568
- package/rules/common/C029_catch_block_logging/semantic-analyzer.js +0 -459
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST-based analyzer for S005 - No Origin Header Authentication
|
|
3
|
+
* Detects usage of Origin header for authentication/access control through AST analysis
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const babel = require('@babel/parser');
|
|
7
|
+
const traverse = require('@babel/traverse').default;
|
|
8
|
+
|
|
9
|
+
class S005ASTAnalyzer {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.ruleId = 'S005';
|
|
12
|
+
this.ruleName = 'No Origin Header Authentication';
|
|
13
|
+
this.description = 'Do not use Origin header for authentication or access control';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async analyze(files, language, options = {}) {
|
|
17
|
+
const violations = [];
|
|
18
|
+
|
|
19
|
+
for (const filePath of files) {
|
|
20
|
+
try {
|
|
21
|
+
const content = require('fs').readFileSync(filePath, 'utf8');
|
|
22
|
+
const fileViolations = await this.analyzeFile(content, filePath, options);
|
|
23
|
+
violations.push(...fileViolations);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (options.verbose) {
|
|
26
|
+
console.warn(`⚠️ S005 AST analysis failed for ${filePath}: ${error.message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return violations;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async analyzeFile(content, filePath, options = {}) {
|
|
35
|
+
const violations = [];
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Parse with TypeScript/JavaScript support
|
|
39
|
+
const ast = babel.parse(content, {
|
|
40
|
+
sourceType: 'module',
|
|
41
|
+
allowImportExportEverywhere: true,
|
|
42
|
+
allowReturnOutsideFunction: true,
|
|
43
|
+
plugins: [
|
|
44
|
+
'typescript',
|
|
45
|
+
'jsx',
|
|
46
|
+
'objectRestSpread',
|
|
47
|
+
'functionBind',
|
|
48
|
+
'exportDefaultFrom',
|
|
49
|
+
'decorators-legacy',
|
|
50
|
+
'classProperties',
|
|
51
|
+
'asyncGenerators',
|
|
52
|
+
'dynamicImport'
|
|
53
|
+
]
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Traverse AST to find Origin header usage in authentication contexts
|
|
57
|
+
traverse(ast, {
|
|
58
|
+
MemberExpression: (path) => {
|
|
59
|
+
this.checkOriginHeaderAccess(path, violations, filePath);
|
|
60
|
+
},
|
|
61
|
+
CallExpression: (path) => {
|
|
62
|
+
this.checkOriginHeaderMethods(path, violations, filePath);
|
|
63
|
+
},
|
|
64
|
+
IfStatement: (path) => {
|
|
65
|
+
this.checkConditionalOriginAuth(path, violations, filePath);
|
|
66
|
+
},
|
|
67
|
+
AssignmentExpression: (path) => {
|
|
68
|
+
this.checkOriginAssignment(path, violations, filePath);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
} catch (parseError) {
|
|
73
|
+
if (options.verbose) {
|
|
74
|
+
console.warn(`⚠️ S005 parse failed for ${filePath}: ${parseError.message}`);
|
|
75
|
+
}
|
|
76
|
+
// Fall back to regex analysis if AST parsing fails
|
|
77
|
+
return this.analyzeWithRegex(content, filePath, options);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return violations;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
checkOriginHeaderAccess(path, violations, filePath) {
|
|
84
|
+
const node = path.node;
|
|
85
|
+
|
|
86
|
+
// Check for req.headers.origin, headers.origin, req.headers['origin']
|
|
87
|
+
if (this.isOriginHeaderAccess(node)) {
|
|
88
|
+
// Check if this is in an authentication context
|
|
89
|
+
if (this.isInAuthenticationContext(path)) {
|
|
90
|
+
violations.push({
|
|
91
|
+
ruleId: this.ruleId,
|
|
92
|
+
severity: 'error',
|
|
93
|
+
message: 'Origin header should not be used for authentication. Origin can be spoofed and is not secure for access control.',
|
|
94
|
+
line: node.loc ? node.loc.start.line : 1,
|
|
95
|
+
column: node.loc ? node.loc.start.column + 1 : 1,
|
|
96
|
+
filePath: filePath,
|
|
97
|
+
type: 'origin_header_access'
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
checkOriginHeaderMethods(path, violations, filePath) {
|
|
104
|
+
const node = path.node;
|
|
105
|
+
|
|
106
|
+
// Check for req.get('origin'), req.header('origin'), getHeader('origin')
|
|
107
|
+
if (this.isOriginHeaderMethod(node)) {
|
|
108
|
+
if (this.isInAuthenticationContext(path)) {
|
|
109
|
+
violations.push({
|
|
110
|
+
ruleId: this.ruleId,
|
|
111
|
+
severity: 'error',
|
|
112
|
+
message: 'Origin header retrieval methods should not be used for authentication purposes.',
|
|
113
|
+
line: node.loc ? node.loc.start.line : 1,
|
|
114
|
+
column: node.loc ? node.loc.start.column + 1 : 1,
|
|
115
|
+
filePath: filePath,
|
|
116
|
+
type: 'origin_header_method'
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check for CORS configuration with origin-based auth
|
|
122
|
+
if (this.isCORSOriginAuth(node)) {
|
|
123
|
+
violations.push({
|
|
124
|
+
ruleId: this.ruleId,
|
|
125
|
+
severity: 'warning',
|
|
126
|
+
message: 'CORS origin configuration should not replace proper authentication mechanisms.',
|
|
127
|
+
line: node.loc ? node.loc.start.line : 1,
|
|
128
|
+
column: node.loc ? node.loc.start.column + 1 : 1,
|
|
129
|
+
filePath: filePath,
|
|
130
|
+
type: 'cors_origin_auth'
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
checkConditionalOriginAuth(path, violations, filePath) {
|
|
136
|
+
const node = path.node;
|
|
137
|
+
|
|
138
|
+
// Check if condition involves origin header and authentication
|
|
139
|
+
if (this.hasOriginInCondition(node.test) && this.hasAuthInBlock(node.consequent)) {
|
|
140
|
+
violations.push({
|
|
141
|
+
ruleId: this.ruleId,
|
|
142
|
+
severity: 'error',
|
|
143
|
+
message: 'Conditional authentication based on Origin header is insecure. Use proper authentication tokens.',
|
|
144
|
+
line: node.loc ? node.loc.start.line : 1,
|
|
145
|
+
column: node.loc ? node.loc.start.column + 1 : 1,
|
|
146
|
+
filePath: filePath,
|
|
147
|
+
type: 'conditional_origin_auth'
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
checkOriginAssignment(path, violations, filePath) {
|
|
153
|
+
const node = path.node;
|
|
154
|
+
|
|
155
|
+
// Check for assignments involving origin header in auth context
|
|
156
|
+
if (this.isOriginRelatedAssignment(node) && this.isInAuthenticationContext(path)) {
|
|
157
|
+
violations.push({
|
|
158
|
+
ruleId: this.ruleId,
|
|
159
|
+
severity: 'warning',
|
|
160
|
+
message: 'Origin header values should not be assigned for authentication purposes.',
|
|
161
|
+
line: node.loc ? node.loc.start.line : 1,
|
|
162
|
+
column: node.loc ? node.loc.start.column + 1 : 1,
|
|
163
|
+
filePath: filePath,
|
|
164
|
+
type: 'origin_assignment_auth'
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
isOriginHeaderAccess(node) {
|
|
170
|
+
// req.headers.origin
|
|
171
|
+
if (node.type === 'MemberExpression' &&
|
|
172
|
+
node.object && node.object.type === 'MemberExpression' &&
|
|
173
|
+
node.object.property && node.object.property.name === 'headers' &&
|
|
174
|
+
node.property && (node.property.name === 'origin' ||
|
|
175
|
+
(node.property.type === 'StringLiteral' && node.property.value === 'origin'))) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// headers.origin or headers['origin']
|
|
180
|
+
if (node.type === 'MemberExpression' &&
|
|
181
|
+
node.object && node.object.name === 'headers' &&
|
|
182
|
+
node.property && (node.property.name === 'origin' ||
|
|
183
|
+
(node.property.type === 'StringLiteral' && node.property.value === 'origin'))) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
isOriginHeaderMethod(node) {
|
|
191
|
+
if (node.type !== 'CallExpression' || !node.callee) return false;
|
|
192
|
+
|
|
193
|
+
const callee = node.callee;
|
|
194
|
+
|
|
195
|
+
// req.get('origin'), req.header('origin')
|
|
196
|
+
if (callee.type === 'MemberExpression' &&
|
|
197
|
+
callee.property && (callee.property.name === 'get' || callee.property.name === 'header') &&
|
|
198
|
+
node.arguments && node.arguments.length > 0 &&
|
|
199
|
+
node.arguments[0].type === 'StringLiteral' &&
|
|
200
|
+
node.arguments[0].value.toLowerCase() === 'origin') {
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// getHeader('origin')
|
|
205
|
+
if (callee.type === 'Identifier' && callee.name === 'getHeader' &&
|
|
206
|
+
node.arguments && node.arguments.length > 0 &&
|
|
207
|
+
node.arguments[0].type === 'StringLiteral' &&
|
|
208
|
+
node.arguments[0].value.toLowerCase() === 'origin') {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
isCORSOriginAuth(node) {
|
|
216
|
+
if (node.type !== 'CallExpression' || !node.callee) return false;
|
|
217
|
+
|
|
218
|
+
// Check for CORS configuration calls
|
|
219
|
+
const callee = node.callee;
|
|
220
|
+
if (callee.type === 'Identifier' && callee.name === 'cors' ||
|
|
221
|
+
(callee.type === 'MemberExpression' && callee.property && callee.property.name === 'cors')) {
|
|
222
|
+
|
|
223
|
+
// Check if arguments contain auth-related configuration
|
|
224
|
+
if (node.arguments && node.arguments.length > 0) {
|
|
225
|
+
const config = node.arguments[0];
|
|
226
|
+
if (config.type === 'ObjectExpression') {
|
|
227
|
+
return config.properties.some(prop =>
|
|
228
|
+
this.isPropertyWithAuthKeyword(prop) && this.hasOriginReference(prop)
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
hasOriginInCondition(testNode) {
|
|
238
|
+
if (!testNode) return false;
|
|
239
|
+
|
|
240
|
+
// Recursively check for origin references in condition
|
|
241
|
+
if (testNode.type === 'MemberExpression') {
|
|
242
|
+
return this.isOriginHeaderAccess(testNode);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (testNode.type === 'CallExpression') {
|
|
246
|
+
return this.isOriginHeaderMethod(testNode);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (testNode.type === 'BinaryExpression') {
|
|
250
|
+
return this.hasOriginInCondition(testNode.left) || this.hasOriginInCondition(testNode.right);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (testNode.type === 'LogicalExpression') {
|
|
254
|
+
return this.hasOriginInCondition(testNode.left) || this.hasOriginInCondition(testNode.right);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
hasAuthInBlock(blockNode) {
|
|
261
|
+
if (!blockNode) return false;
|
|
262
|
+
|
|
263
|
+
const authKeywords = ['auth', 'login', 'token', 'session', 'user', 'permission', 'access'];
|
|
264
|
+
|
|
265
|
+
// Simple check for auth-related identifiers in the block
|
|
266
|
+
let hasAuth = false;
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
traverse(blockNode, {
|
|
270
|
+
Identifier: (path) => {
|
|
271
|
+
if (path.node && path.node.name && authKeywords.some(keyword =>
|
|
272
|
+
path.node.name.toLowerCase().includes(keyword))) {
|
|
273
|
+
hasAuth = true;
|
|
274
|
+
path.stop();
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
StringLiteral: (path) => {
|
|
278
|
+
if (path.node && path.node.value && authKeywords.some(keyword =>
|
|
279
|
+
path.node.value.toLowerCase().includes(keyword))) {
|
|
280
|
+
hasAuth = true;
|
|
281
|
+
path.stop();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}, this);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
// Ignore traverse errors, return false
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return hasAuth;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
isInAuthenticationContext(path) {
|
|
294
|
+
// Check parent nodes for authentication context
|
|
295
|
+
let currentPath = path;
|
|
296
|
+
let depth = 0;
|
|
297
|
+
const maxDepth = 10;
|
|
298
|
+
|
|
299
|
+
while (currentPath && depth < maxDepth) {
|
|
300
|
+
const node = currentPath.node;
|
|
301
|
+
|
|
302
|
+
// Check function names
|
|
303
|
+
if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' ||
|
|
304
|
+
node.type === 'ArrowFunctionExpression') {
|
|
305
|
+
if (this.hasAuthInName(node.id?.name)) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Check variable declarations
|
|
311
|
+
if (node.type === 'VariableDeclarator' && this.hasAuthInName(node.id?.name)) {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Check object property names
|
|
316
|
+
if (node.type === 'ObjectProperty' && this.hasAuthInName(node.key?.name || node.key?.value)) {
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
currentPath = currentPath.parent;
|
|
321
|
+
depth++;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
hasAuthInName(name) {
|
|
328
|
+
if (!name) return false;
|
|
329
|
+
|
|
330
|
+
const authKeywords = [
|
|
331
|
+
'auth', 'login', 'logout', 'authenticate', 'authorize',
|
|
332
|
+
'permission', 'access', 'token', 'session', 'user',
|
|
333
|
+
'verify', 'validate', 'check', 'guard', 'protect',
|
|
334
|
+
'middleware', 'passport', 'jwt', 'bearer'
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
const lowerName = name.toLowerCase();
|
|
338
|
+
return authKeywords.some(keyword => lowerName.includes(keyword));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
isOriginRelatedAssignment(node) {
|
|
342
|
+
if (node.type !== 'AssignmentExpression') return false;
|
|
343
|
+
|
|
344
|
+
// Check if right side involves origin header
|
|
345
|
+
return this.hasOriginReference(node.right);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
hasOriginReference(node) {
|
|
349
|
+
if (!node) return false;
|
|
350
|
+
|
|
351
|
+
if (node.type === 'MemberExpression') {
|
|
352
|
+
return this.isOriginHeaderAccess(node);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (node.type === 'CallExpression') {
|
|
356
|
+
return this.isOriginHeaderMethod(node);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (node.type === 'Identifier' && node.name.toLowerCase().includes('origin')) {
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (node.type === 'StringLiteral' && node.value.toLowerCase().includes('origin')) {
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
isPropertyWithAuthKeyword(prop) {
|
|
371
|
+
if (!prop || prop.type !== 'ObjectProperty') return false;
|
|
372
|
+
|
|
373
|
+
const key = prop.key?.name || prop.key?.value;
|
|
374
|
+
if (!key) return false;
|
|
375
|
+
|
|
376
|
+
return this.hasAuthInName(key);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Fallback regex analysis
|
|
380
|
+
analyzeWithRegex(content, filePath, options = {}) {
|
|
381
|
+
const violations = [];
|
|
382
|
+
const lines = content.split('\n');
|
|
383
|
+
|
|
384
|
+
lines.forEach((line, index) => {
|
|
385
|
+
const lineNumber = index + 1;
|
|
386
|
+
|
|
387
|
+
// Basic regex check for origin header in auth context
|
|
388
|
+
const originAuthPattern = /(?:req\.headers\.origin|req\.get\s*\(\s*['"`]origin['"`]\s*\)).*(?:auth|login|token|permission)/i;
|
|
389
|
+
if (originAuthPattern.test(line)) {
|
|
390
|
+
violations.push({
|
|
391
|
+
ruleId: this.ruleId,
|
|
392
|
+
severity: 'error',
|
|
393
|
+
message: 'Origin header should not be used for authentication (detected via regex fallback).',
|
|
394
|
+
line: lineNumber,
|
|
395
|
+
column: line.search(/origin/i) + 1,
|
|
396
|
+
filePath: filePath,
|
|
397
|
+
type: 'origin_auth_regex'
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
return violations;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
module.exports = S005ASTAnalyzer;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ruleId": "S005",
|
|
3
|
+
"name": "No Origin Header Authentication",
|
|
4
|
+
"description": "Do not use Origin header for authentication or access control",
|
|
5
|
+
"category": "security",
|
|
6
|
+
"severity": "error",
|
|
7
|
+
"languages": ["typescript", "javascript"],
|
|
8
|
+
"version": "1.0.0",
|
|
9
|
+
"status": "stable",
|
|
10
|
+
"tags": ["security", "authentication", "headers", "origin", "access-control"],
|
|
11
|
+
|
|
12
|
+
"patterns": {
|
|
13
|
+
"vulnerable": [
|
|
14
|
+
"req.headers.origin in authentication context",
|
|
15
|
+
"req.get('origin') for access control",
|
|
16
|
+
"Origin-based conditional authentication",
|
|
17
|
+
"CORS origin configuration mixed with auth",
|
|
18
|
+
"Express middleware using origin for security"
|
|
19
|
+
],
|
|
20
|
+
"secure": [
|
|
21
|
+
"JWT token authentication",
|
|
22
|
+
"Session-based authentication",
|
|
23
|
+
"API key authentication",
|
|
24
|
+
"OAuth 2.0 flows",
|
|
25
|
+
"Proper CORS configuration without auth reliance"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
"configuration": {
|
|
30
|
+
"checkAuthContext": true,
|
|
31
|
+
"checkMiddleware": true,
|
|
32
|
+
"checkConditionals": true,
|
|
33
|
+
"checkCORSMixing": true,
|
|
34
|
+
"contextDepth": 3,
|
|
35
|
+
"ignoreComments": true
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
"examples": {
|
|
39
|
+
"violations": [
|
|
40
|
+
{
|
|
41
|
+
"code": "if (req.headers.origin === 'trusted.com') { req.authenticated = true; }",
|
|
42
|
+
"reason": "Using Origin header for authentication is insecure"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"code": "const authMiddleware = (req, res, next) => { if (req.get('origin') === 'admin.com') next(); }",
|
|
46
|
+
"reason": "Middleware should not rely on Origin header for access control"
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
"valid": [
|
|
50
|
+
{
|
|
51
|
+
"code": "const token = req.headers.authorization; jwt.verify(token, secret, callback);",
|
|
52
|
+
"reason": "Proper JWT token authentication"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"code": "console.log('Request from:', req.headers.origin);",
|
|
56
|
+
"reason": "Using Origin header for logging only, not authentication"
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
"remediation": {
|
|
62
|
+
"recommendations": [
|
|
63
|
+
"Use JWT tokens for authentication",
|
|
64
|
+
"Implement session-based authentication",
|
|
65
|
+
"Use API keys for service authentication",
|
|
66
|
+
"Implement OAuth 2.0 for third-party authentication",
|
|
67
|
+
"Use proper CORS configuration without relying on it for authentication"
|
|
68
|
+
],
|
|
69
|
+
"resources": [
|
|
70
|
+
"https://owasp.org/www-community/vulnerabilities/CORS_OriginHeaderScrutiny",
|
|
71
|
+
"https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin",
|
|
72
|
+
"https://auth0.com/docs/secure/tokens/json-web-tokens"
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
"performance": {
|
|
77
|
+
"complexity": "O(n)",
|
|
78
|
+
"accuracy": {
|
|
79
|
+
"ast": 95,
|
|
80
|
+
"regex": 85
|
|
81
|
+
},
|
|
82
|
+
"falsePositiveRate": "< 5%",
|
|
83
|
+
"coverage": "High for TypeScript/JavaScript"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# S006 - No Plaintext Recovery/Activation Codes
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
Quy tắc này phát hiện việc gửi mã khôi phục (recovery codes), mã kích hoạt (activation codes), hoặc mã xác thực (verification codes) dưới dạng plaintext, có thể dẫn đến các lỗ hổng bảo mật nghiêm trọng.
|
|
5
|
+
|
|
6
|
+
## OWASP Classification
|
|
7
|
+
- **Category**: A02:2021 - Cryptographic Failures
|
|
8
|
+
- **CWE**: CWE-319 - Cleartext Transmission of Sensitive Information
|
|
9
|
+
- **Severity**: Error
|
|
10
|
+
- **Impact**: High (Account takeover, unauthorized access)
|
|
11
|
+
|
|
12
|
+
## Vấn đề
|
|
13
|
+
Khi gửi các mã nhạy cảm như recovery codes, activation codes, hoặc OTP codes dưới dạng plaintext:
|
|
14
|
+
|
|
15
|
+
1. **Nguy cơ bị chặn bắt**: Mã có thể bị đánh cắp qua các kênh không an toàn
|
|
16
|
+
2. **Account takeover**: Kẻ tấn công có thể sử dụng mã để chiếm quyền điều khiển tài khoản
|
|
17
|
+
3. **Lưu trữ không an toàn**: Mã có thể bị lưu trong logs hoặc cache
|
|
18
|
+
4. **Man-in-the-middle attacks**: Mã có thể bị chặn bắt trong quá trình truyền tải
|
|
19
|
+
|
|
20
|
+
## Các trường hợp vi phạm
|
|
21
|
+
|
|
22
|
+
### 1. Gửi mã qua email không mã hóa
|
|
23
|
+
```javascript
|
|
24
|
+
// ❌ Vi phạm - gửi activation code dạng plaintext
|
|
25
|
+
const activationCode = generateCode();
|
|
26
|
+
await sendEmail(user.email, \`Your activation code is: \${activationCode}\`);
|
|
27
|
+
|
|
28
|
+
// ❌ Vi phạm - trả về recovery code trong API response
|
|
29
|
+
app.post('/forgot-password', (req, res) => {
|
|
30
|
+
const recoveryCode = generateRecoveryCode();
|
|
31
|
+
res.json({
|
|
32
|
+
message: 'Password reset initiated',
|
|
33
|
+
recoveryCode: recoveryCode // Nguy hiểm!
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Lưu mã trong logs
|
|
39
|
+
```javascript
|
|
40
|
+
// ❌ Vi phạm - log OTP code
|
|
41
|
+
const otpCode = generateOTP();
|
|
42
|
+
console.log(\`Generated OTP for user \${userId}: \${otpCode}\`);
|
|
43
|
+
logger.info(\`Sending verification code: \${verificationCode}\`);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 3. Hiển thị mã trong response body
|
|
47
|
+
```javascript
|
|
48
|
+
// ❌ Vi phạm - trả về mã trong response
|
|
49
|
+
const resetCode = await generateResetCode(userId);
|
|
50
|
+
return {
|
|
51
|
+
success: true,
|
|
52
|
+
resetCode: resetCode,
|
|
53
|
+
message: 'Check your email'
|
|
54
|
+
};
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Giải pháp an toàn
|
|
58
|
+
|
|
59
|
+
### 1. Mã hóa mã trước khi gửi
|
|
60
|
+
```javascript
|
|
61
|
+
// ✅ An toàn - hash mã trước khi lưu trữ
|
|
62
|
+
const activationCode = generateCode();
|
|
63
|
+
const hashedCode = await bcrypt.hash(activationCode, 10);
|
|
64
|
+
await saveToDatabase({ userId, hashedCode });
|
|
65
|
+
|
|
66
|
+
// Chỉ gửi mã qua kênh an toàn
|
|
67
|
+
await sendSecureEmail(user.email, activationCode);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 2. Sử dụng token thay vì mã trực tiếp
|
|
71
|
+
```javascript
|
|
72
|
+
// ✅ An toàn - sử dụng secure token
|
|
73
|
+
const resetToken = jwt.sign({ userId, purpose: 'reset' }, SECRET_KEY, { expiresIn: '15m' });
|
|
74
|
+
const resetLink = \`https://app.com/reset?token=\${resetToken}\`;
|
|
75
|
+
await sendEmail(user.email, \`Click here to reset: \${resetLink}\`);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 3. Chỉ thông báo thành công, không trả về mã
|
|
79
|
+
```javascript
|
|
80
|
+
// ✅ An toàn - không expose mã
|
|
81
|
+
app.post('/send-otp', async (req, res) => {
|
|
82
|
+
const otp = generateOTP();
|
|
83
|
+
await sendSMSOTP(user.phone, otp);
|
|
84
|
+
|
|
85
|
+
res.json({
|
|
86
|
+
success: true,
|
|
87
|
+
message: 'OTP sent to your phone'
|
|
88
|
+
// Không trả về OTP
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 4. Logging an toàn
|
|
94
|
+
```javascript
|
|
95
|
+
// ✅ An toàn - log mà không expose mã
|
|
96
|
+
const verificationCode = generateCode();
|
|
97
|
+
logger.info(\`Verification code sent to user \${userId}\`);
|
|
98
|
+
// Không log mã thực tế
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Phương pháp phát hiện
|
|
102
|
+
|
|
103
|
+
Rule này sử dụng heuristic analysis để phát hiện:
|
|
104
|
+
|
|
105
|
+
1. **Pattern matching**: Tìm kiếm các từ khóa như `recovery`, `activation`, `reset`, `otp`, `verification` kết hợp với `send`, `email`, `response`
|
|
106
|
+
2. **Variable analysis**: Phân tích tên biến có chứa từ khóa nhạy cảm
|
|
107
|
+
3. **Context analysis**: Kiểm tra ngữ cảnh truyền tải (HTTP response, email, SMS)
|
|
108
|
+
4. **String literal analysis**: Phát hiện mã được hardcode trong chuỗi
|
|
109
|
+
|
|
110
|
+
## Cấu hình
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"S006": {
|
|
115
|
+
"enabled": true,
|
|
116
|
+
"severity": "error",
|
|
117
|
+
"excludePatterns": [
|
|
118
|
+
"test/**",
|
|
119
|
+
"**/*.test.js",
|
|
120
|
+
"**/*.spec.js"
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Best Practices
|
|
127
|
+
|
|
128
|
+
1. **Luôn mã hóa**: Mã hóa tất cả mã nhạy cảm trước khi truyền tải
|
|
129
|
+
2. **Sử dụng HTTPS**: Đảm bảo tất cả API endpoints sử dụng HTTPS
|
|
130
|
+
3. **Time-limited tokens**: Sử dụng tokens có thời hạn thay vì mã tĩnh
|
|
131
|
+
4. **Secure channels**: Sử dụng các kênh truyền tải an toàn (encrypted email, secure SMS)
|
|
132
|
+
5. **No logging**: Không bao giờ log mã nhạy cảm
|
|
133
|
+
6. **Hash storage**: Luôn hash mã trước khi lưu vào database
|
|
134
|
+
|
|
135
|
+
## Tài liệu tham khảo
|
|
136
|
+
|
|
137
|
+
- [OWASP A02:2021 - Cryptographic Failures](https://owasp.org/Top10/A02_2021-Cryptographic_Failures/)
|
|
138
|
+
- [CWE-319: Cleartext Transmission of Sensitive Information](https://cwe.mitre.org/data/definitions/319.html)
|
|
139
|
+
- [NIST Guidelines for Password Recovery](https://pages.nist.gov/800-63-3/sp800-63b.html)
|