@sun-asterisk/sunlint 1.3.26 → 1.3.28
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/config/rules/enhanced-rules-registry.json +101 -17
- package/config/rules/rules-registry-generated.json +22 -22
- package/origin-rules/security-en.md +351 -338
- package/package.json +1 -1
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +73 -21
- package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +206 -2
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +553 -58
- package/rules/common/C029_catch_block_logging/analyzer.js +47 -12
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +35 -15
- package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +9 -5
- package/rules/security/S003_open_redirect_protection/README.md +371 -0
- package/rules/security/S003_open_redirect_protection/analyzer.js +135 -0
- package/rules/security/S003_open_redirect_protection/config.json +58 -0
- package/rules/security/S003_open_redirect_protection/symbol-based-analyzer.js +884 -0
- package/rules/security/S004_sensitive_data_logging/analyzer.js +135 -0
- package/rules/security/S004_sensitive_data_logging/config.json +62 -0
- package/rules/security/S004_sensitive_data_logging/symbol-based-analyzer.js +592 -0
- package/rules/security/S005_no_origin_auth/analyzer.js +97 -148
- package/rules/security/S005_no_origin_auth/config.json +28 -67
- package/rules/security/S005_no_origin_auth/symbol-based-analyzer.js +708 -0
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +170 -31
- package/rules/security/S010_no_insecure_encryption/analyzer.js +8 -2
- package/rules/security/S012_hardcoded_secrets/analyzer.js +149 -0
- package/rules/security/S012_hardcoded_secrets/config.json +75 -0
- package/rules/security/S012_hardcoded_secrets/symbol-based-analyzer.js +1204 -0
- package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +87 -0
- package/rules/security/S017_use_parameterized_queries/analyzer.js +11 -78
- package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +1146 -1
- package/rules/security/S019_smtp_injection_protection/analyzer.js +120 -0
- package/rules/security/S019_smtp_injection_protection/config.json +35 -0
- package/rules/security/S019_smtp_injection_protection/symbol-based-analyzer.js +687 -0
- package/rules/security/S020_no_eval_dynamic_code/analyzer.js +55 -130
- package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +4 -19
- package/rules/security/S022_escape_output_context/README.md +254 -0
- package/rules/security/S022_escape_output_context/analyzer.js +510 -0
- package/rules/security/S022_escape_output_context/config.json +229 -0
- package/rules/security/S023_no_json_injection/analyzer.js +15 -0
- package/rules/security/S023_no_json_injection/ast-analyzer.js +18 -3
- package/rules/security/S023_no_json_injection/config.json +133 -0
- package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +41 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +67 -8
- package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +29 -6
- package/rules/security/S029_csrf_protection/config.json +127 -0
- package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +160 -28
- package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +81 -19
- package/rules/security/S031_secure_session_cookies/analyzer.js +20 -2
- package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +100 -0
- package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +8 -1
- package/rules/security/S032_httponly_session_cookies/analyzer.js +2 -2
- package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +115 -0
- package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +39 -10
- package/rules/security/S036_lfi_rfi_protection/analyzer.js +224 -0
- package/rules/security/S036_lfi_rfi_protection/config.json +20 -0
- package/rules/security/S040_session_fixation_protection/analyzer.js +153 -0
- package/rules/security/S040_session_fixation_protection/config.json +20 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/README.md +83 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/analyzer.js +153 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/config.json +41 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/symbol-based-analyzer.js +1139 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/README.md +107 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/analyzer.js +153 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/config.json +41 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/symbol-based-analyzer.js +541 -0
- package/docs/COMMAND-EXAMPLES.md +0 -390
- package/docs/FILE_LIMITS_COMPLETION_REPORT.md +0 -151
- package/docs/FOLDER_STRUCTURE.md +0 -59
- package/docs/SIMPLIFIED_USAGE_GUIDE.md +0 -208
- package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +0 -541
- package/rules/security/S020_no_eval_dynamic_code/regex-based-analyzer.js +0 -307
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S005 - No Origin Header Authentication (Symbol-based Analyzer)
|
|
3
|
+
*
|
|
4
|
+
* Detects use of Origin header for authentication or authorization decisions.
|
|
5
|
+
* Origin header can be easily spoofed and should only be used for CORS/CSRF.
|
|
6
|
+
*
|
|
7
|
+
* Based on:
|
|
8
|
+
* - OWASP A07:2021 - Identification and Authentication Failures
|
|
9
|
+
* - CWE-290: Authentication Bypass by Spoofing
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { SyntaxKind } = require("ts-morph");
|
|
13
|
+
|
|
14
|
+
class S005SymbolBasedAnalyzer {
|
|
15
|
+
constructor(semanticEngine = null) {
|
|
16
|
+
this.ruleId = "S005";
|
|
17
|
+
this.semanticEngine = semanticEngine;
|
|
18
|
+
|
|
19
|
+
// Origin header access patterns
|
|
20
|
+
this.originAccessPatterns = [
|
|
21
|
+
"origin",
|
|
22
|
+
"req.headers.origin",
|
|
23
|
+
"req.headers['origin']",
|
|
24
|
+
'req.headers["origin"]',
|
|
25
|
+
"request.headers.origin",
|
|
26
|
+
"request.headers['origin']",
|
|
27
|
+
'request.headers["origin"]',
|
|
28
|
+
"headers.origin",
|
|
29
|
+
"headers['origin']",
|
|
30
|
+
'headers["origin"]',
|
|
31
|
+
"req.header('origin')",
|
|
32
|
+
'req.header("origin")',
|
|
33
|
+
"req.get('origin')",
|
|
34
|
+
'req.get("origin")',
|
|
35
|
+
"request.getHeader('origin')",
|
|
36
|
+
'request.getHeader("origin")',
|
|
37
|
+
"ctx.headers.origin", // Koa
|
|
38
|
+
"ctx.request.headers.origin",
|
|
39
|
+
"event.headers.origin", // AWS Lambda/Nuxt.js
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// Authentication/Authorization keywords (UNSAFE use cases)
|
|
43
|
+
this.authKeywords = [
|
|
44
|
+
"auth",
|
|
45
|
+
"authenticate",
|
|
46
|
+
"authorization",
|
|
47
|
+
"login",
|
|
48
|
+
"signin",
|
|
49
|
+
"user",
|
|
50
|
+
"session",
|
|
51
|
+
"token",
|
|
52
|
+
"permission",
|
|
53
|
+
"role",
|
|
54
|
+
"access",
|
|
55
|
+
"allow",
|
|
56
|
+
"deny",
|
|
57
|
+
"grant",
|
|
58
|
+
"check",
|
|
59
|
+
"verify",
|
|
60
|
+
"validate",
|
|
61
|
+
"isallowed",
|
|
62
|
+
"isauthorized",
|
|
63
|
+
"hasaccess",
|
|
64
|
+
"haspermission",
|
|
65
|
+
"canaccess",
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// SAFE use cases (CORS/CSRF protection)
|
|
69
|
+
this.safeUseCasePatterns = [
|
|
70
|
+
"cors",
|
|
71
|
+
"csrf",
|
|
72
|
+
"allowedorigins",
|
|
73
|
+
"allowed_origins",
|
|
74
|
+
"trustedorigins",
|
|
75
|
+
"trusted_origins",
|
|
76
|
+
"whitelistorigins",
|
|
77
|
+
"whitelist_origins",
|
|
78
|
+
"originwhitelist",
|
|
79
|
+
"origin_whitelist",
|
|
80
|
+
"checkorigin", // CORS check
|
|
81
|
+
"verifyorigin", // CORS verification
|
|
82
|
+
"validateorigin", // CORS validation
|
|
83
|
+
"setaccesscontrolalloworigin",
|
|
84
|
+
"access-control-allow-origin",
|
|
85
|
+
"sameorigin",
|
|
86
|
+
"crossorigin",
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
// Framework CORS configuration (SAFE)
|
|
90
|
+
this.corsConfigPatterns = [
|
|
91
|
+
"cors(",
|
|
92
|
+
"enablecors",
|
|
93
|
+
"usecors",
|
|
94
|
+
"corsOptions",
|
|
95
|
+
"cors_options",
|
|
96
|
+
"CorsMiddleware",
|
|
97
|
+
"corsconfig",
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async initialize(semanticEngine) {
|
|
102
|
+
this.semanticEngine = semanticEngine;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async analyze(sourceFile, filePath) {
|
|
106
|
+
const violations = [];
|
|
107
|
+
const reportedLines = new Set();
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// Step 1: Build a map of variables that hold origin values
|
|
111
|
+
const originVariables = this.buildOriginVariablesMap(sourceFile);
|
|
112
|
+
|
|
113
|
+
// Step 2: Find all property access expressions (e.g., req.headers.origin)
|
|
114
|
+
this.checkPropertyAccess(
|
|
115
|
+
sourceFile,
|
|
116
|
+
filePath,
|
|
117
|
+
violations,
|
|
118
|
+
reportedLines
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Step 3: Find all element access expressions (e.g., req.headers['origin'])
|
|
122
|
+
this.checkElementAccess(
|
|
123
|
+
sourceFile,
|
|
124
|
+
filePath,
|
|
125
|
+
violations,
|
|
126
|
+
reportedLines
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Step 4: Find all call expressions (e.g., req.get('origin'))
|
|
130
|
+
this.checkCallExpressions(
|
|
131
|
+
sourceFile,
|
|
132
|
+
filePath,
|
|
133
|
+
violations,
|
|
134
|
+
reportedLines
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Step 5: Check usage of variables that contain origin
|
|
138
|
+
this.checkOriginVariableUsage(
|
|
139
|
+
sourceFile,
|
|
140
|
+
filePath,
|
|
141
|
+
violations,
|
|
142
|
+
reportedLines,
|
|
143
|
+
originVariables
|
|
144
|
+
);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.warn(`⚠ [S005] Analysis error in ${filePath}: ${error.message}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return violations;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Build a map of variables that contain origin values
|
|
154
|
+
* Returns: Set of variable names
|
|
155
|
+
*/
|
|
156
|
+
buildOriginVariablesMap(sourceFile) {
|
|
157
|
+
const originVars = new Set();
|
|
158
|
+
|
|
159
|
+
// Find variable declarations
|
|
160
|
+
const variableDeclarations = sourceFile.getDescendantsOfKind(
|
|
161
|
+
SyntaxKind.VariableDeclaration
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
for (const varDecl of variableDeclarations) {
|
|
165
|
+
const varName = varDecl.getName();
|
|
166
|
+
const initializer = varDecl.getInitializer();
|
|
167
|
+
|
|
168
|
+
if (!initializer) continue;
|
|
169
|
+
|
|
170
|
+
const initText = initializer.getText().toLowerCase();
|
|
171
|
+
|
|
172
|
+
// Check if initializer is origin access
|
|
173
|
+
if (this.isOriginAccess(initText) ||
|
|
174
|
+
initText.includes("req.get('origin')") ||
|
|
175
|
+
initText.includes('req.get("origin")') ||
|
|
176
|
+
initText.includes("req.header('origin')") ||
|
|
177
|
+
initText.includes('req.header("origin")') ||
|
|
178
|
+
initText.includes("req.headers['origin']") ||
|
|
179
|
+
initText.includes('req.headers["origin"]')) {
|
|
180
|
+
originVars.add(varName.toLowerCase());
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return originVars;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Check usage of variables that contain origin
|
|
189
|
+
*/
|
|
190
|
+
checkOriginVariableUsage(sourceFile, filePath, violations, reportedLines, originVariables) {
|
|
191
|
+
if (originVariables.size === 0) return;
|
|
192
|
+
|
|
193
|
+
// Find all identifiers (variable usages)
|
|
194
|
+
const identifiers = sourceFile.getDescendantsOfKind(
|
|
195
|
+
SyntaxKind.Identifier
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
for (const identifier of identifiers) {
|
|
199
|
+
const idText = identifier.getText().toLowerCase();
|
|
200
|
+
|
|
201
|
+
// Skip if not an origin variable
|
|
202
|
+
if (!originVariables.has(idText)) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const line = identifier.getStartLineNumber();
|
|
207
|
+
if (reportedLines.has(line)) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Get context
|
|
212
|
+
const context = this.getContext(identifier);
|
|
213
|
+
|
|
214
|
+
// Check if it's a safe use case
|
|
215
|
+
if (this.isSafeUseCase(context)) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Check if used in auth context
|
|
220
|
+
if (this.isAuthContext(context)) {
|
|
221
|
+
violations.push({
|
|
222
|
+
ruleId: this.ruleId,
|
|
223
|
+
severity: "error",
|
|
224
|
+
message: `Origin header authentication: variable '${identifier.getText()}' (containing Origin header) should not be used for authentication or authorization - Origin can be spoofed.`,
|
|
225
|
+
line: line,
|
|
226
|
+
column: identifier.getStart() - identifier.getStartLinePos() + 1,
|
|
227
|
+
filePath: filePath,
|
|
228
|
+
file: filePath,
|
|
229
|
+
});
|
|
230
|
+
reportedLines.add(line);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Check property access: req.headers.origin
|
|
237
|
+
*/
|
|
238
|
+
checkPropertyAccess(sourceFile, filePath, violations, reportedLines) {
|
|
239
|
+
const propertyAccesses = sourceFile.getDescendantsOfKind(
|
|
240
|
+
SyntaxKind.PropertyAccessExpression
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
for (const propAccess of propertyAccesses) {
|
|
244
|
+
const fullText = propAccess.getText().toLowerCase();
|
|
245
|
+
|
|
246
|
+
// Check if accessing origin header
|
|
247
|
+
if (!this.isOriginAccess(fullText)) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const line = propAccess.getStartLineNumber();
|
|
252
|
+
if (reportedLines.has(line)) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Get surrounding context
|
|
257
|
+
const context = this.getContext(propAccess);
|
|
258
|
+
|
|
259
|
+
// Check if it's a safe use case (CORS/CSRF)
|
|
260
|
+
if (this.isSafeUseCase(context)) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Check if used in authentication/authorization context
|
|
265
|
+
if (this.isAuthContext(context)) {
|
|
266
|
+
violations.push({
|
|
267
|
+
ruleId: this.ruleId,
|
|
268
|
+
severity: "error",
|
|
269
|
+
message: `Origin header authentication: '${propAccess.getText()}' should not be used for authentication or authorization - Origin header can be spoofed. Use verified tokens or sessions instead.`,
|
|
270
|
+
line: line,
|
|
271
|
+
column: propAccess.getStart() - propAccess.getStartLinePos() + 1,
|
|
272
|
+
filePath: filePath,
|
|
273
|
+
file: filePath,
|
|
274
|
+
});
|
|
275
|
+
reportedLines.add(line);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Check element access: req.headers['origin']
|
|
282
|
+
*/
|
|
283
|
+
checkElementAccess(sourceFile, filePath, violations, reportedLines) {
|
|
284
|
+
const elementAccesses = sourceFile.getDescendantsOfKind(
|
|
285
|
+
SyntaxKind.ElementAccessExpression
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
for (const elemAccess of elementAccesses) {
|
|
289
|
+
const fullText = elemAccess.getText().toLowerCase();
|
|
290
|
+
|
|
291
|
+
// Check if accessing origin header
|
|
292
|
+
if (!fullText.includes("origin")) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Verify it's actually accessing 'origin' key
|
|
297
|
+
const arg = elemAccess.getArgumentExpression();
|
|
298
|
+
if (!arg) continue;
|
|
299
|
+
|
|
300
|
+
const argText = arg.getText().toLowerCase();
|
|
301
|
+
if (
|
|
302
|
+
!argText.includes("'origin'") &&
|
|
303
|
+
!argText.includes('"origin"')
|
|
304
|
+
) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const line = elemAccess.getStartLineNumber();
|
|
309
|
+
if (reportedLines.has(line)) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Get context
|
|
314
|
+
const context = this.getContext(elemAccess);
|
|
315
|
+
|
|
316
|
+
// Check safe use cases
|
|
317
|
+
if (this.isSafeUseCase(context)) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Check auth context
|
|
322
|
+
if (this.isAuthContext(context)) {
|
|
323
|
+
violations.push({
|
|
324
|
+
ruleId: this.ruleId,
|
|
325
|
+
severity: "error",
|
|
326
|
+
message: `Origin header authentication: '${elemAccess.getText()}' should not be used for authentication or authorization - use verified credentials instead.`,
|
|
327
|
+
line: line,
|
|
328
|
+
column: elemAccess.getStart() - elemAccess.getStartLinePos() + 1,
|
|
329
|
+
filePath: filePath,
|
|
330
|
+
file: filePath,
|
|
331
|
+
});
|
|
332
|
+
reportedLines.add(line);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Check call expressions: req.get('origin'), req.header('origin')
|
|
339
|
+
*/
|
|
340
|
+
checkCallExpressions(sourceFile, filePath, violations, reportedLines) {
|
|
341
|
+
const callExprs = sourceFile.getDescendantsOfKind(
|
|
342
|
+
SyntaxKind.CallExpression
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
for (const callExpr of callExprs) {
|
|
346
|
+
const expression = callExpr.getExpression();
|
|
347
|
+
const exprText = expression.getText().toLowerCase();
|
|
348
|
+
|
|
349
|
+
// Check if it's a header getter method
|
|
350
|
+
if (
|
|
351
|
+
!exprText.includes("getheader") &&
|
|
352
|
+
!exprText.includes(".get") &&
|
|
353
|
+
!exprText.includes(".header")
|
|
354
|
+
) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Check arguments for 'origin'
|
|
359
|
+
const args = callExpr.getArguments();
|
|
360
|
+
if (args.length === 0) continue;
|
|
361
|
+
|
|
362
|
+
const firstArg = args[0].getText().toLowerCase();
|
|
363
|
+
if (
|
|
364
|
+
!firstArg.includes("'origin'") &&
|
|
365
|
+
!firstArg.includes('"origin"')
|
|
366
|
+
) {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const line = callExpr.getStartLineNumber();
|
|
371
|
+
if (reportedLines.has(line)) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Get context
|
|
376
|
+
const context = this.getContext(callExpr);
|
|
377
|
+
|
|
378
|
+
// Check safe use cases
|
|
379
|
+
if (this.isSafeUseCase(context)) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Check auth context
|
|
384
|
+
if (this.isAuthContext(context)) {
|
|
385
|
+
violations.push({
|
|
386
|
+
ruleId: this.ruleId,
|
|
387
|
+
severity: "error",
|
|
388
|
+
message: `Origin header authentication: '${callExpr.getText()}' should not be used for authentication decisions - Origin can be spoofed by attackers.`,
|
|
389
|
+
line: line,
|
|
390
|
+
column: callExpr.getStart() - callExpr.getStartLinePos() + 1,
|
|
391
|
+
filePath: filePath,
|
|
392
|
+
file: filePath,
|
|
393
|
+
});
|
|
394
|
+
reportedLines.add(line);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Check if the text is accessing origin header
|
|
401
|
+
*/
|
|
402
|
+
isOriginAccess(text) {
|
|
403
|
+
// Must contain "origin" and some header access pattern
|
|
404
|
+
if (!text.includes("origin")) return false;
|
|
405
|
+
|
|
406
|
+
// Check for header access patterns
|
|
407
|
+
return (
|
|
408
|
+
text.includes("headers.origin") ||
|
|
409
|
+
text.includes("header('origin')") ||
|
|
410
|
+
text.includes('header("origin")') ||
|
|
411
|
+
text.includes(".get('origin')") ||
|
|
412
|
+
text.includes('.get("origin")') ||
|
|
413
|
+
text.includes("getheader('origin')") ||
|
|
414
|
+
text.includes('getheader("origin")')
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Get surrounding code context
|
|
420
|
+
*/
|
|
421
|
+
getContext(node) {
|
|
422
|
+
try {
|
|
423
|
+
// Get parent function/method/block
|
|
424
|
+
let parent = node.getParent();
|
|
425
|
+
let depth = 0;
|
|
426
|
+
const maxDepth = 20;
|
|
427
|
+
|
|
428
|
+
while (parent && depth < maxDepth) {
|
|
429
|
+
const kind = parent.getKind();
|
|
430
|
+
if (
|
|
431
|
+
kind === SyntaxKind.FunctionDeclaration ||
|
|
432
|
+
kind === SyntaxKind.FunctionExpression ||
|
|
433
|
+
kind === SyntaxKind.ArrowFunction ||
|
|
434
|
+
kind === SyntaxKind.MethodDeclaration ||
|
|
435
|
+
kind === SyntaxKind.Block
|
|
436
|
+
) {
|
|
437
|
+
return parent.getText().toLowerCase();
|
|
438
|
+
}
|
|
439
|
+
parent = parent.getParent();
|
|
440
|
+
depth++;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Fallback: get surrounding 500 chars
|
|
444
|
+
const sourceFile = node.getSourceFile();
|
|
445
|
+
const pos = node.getStart();
|
|
446
|
+
const text = sourceFile.getText();
|
|
447
|
+
const start = Math.max(0, pos - 250);
|
|
448
|
+
const end = Math.min(text.length, pos + 250);
|
|
449
|
+
return text.substring(start, end).toLowerCase();
|
|
450
|
+
} catch {
|
|
451
|
+
return "";
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Check if it's a safe use case (CORS/CSRF protection)
|
|
457
|
+
*/
|
|
458
|
+
isSafeUseCase(context) {
|
|
459
|
+
// Check for CORS/CSRF patterns
|
|
460
|
+
if (this.safeUseCasePatterns.some((pattern) => context.includes(pattern))) {
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Check for CORS configuration
|
|
465
|
+
if (this.corsConfigPatterns.some((pattern) => context.includes(pattern))) {
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Check if setting response headers (CORS response)
|
|
470
|
+
if (
|
|
471
|
+
context.includes("setHeader") &&
|
|
472
|
+
context.includes("access-control-allow-origin")
|
|
473
|
+
) {
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Check if it's just logging/debugging
|
|
478
|
+
if (
|
|
479
|
+
(context.includes("console.log") || context.includes("logger")) &&
|
|
480
|
+
context.includes("origin")
|
|
481
|
+
) {
|
|
482
|
+
// Only safe if not used in conditional logic
|
|
483
|
+
const originIndex = context.indexOf("origin");
|
|
484
|
+
const beforeOrigin = context.substring(
|
|
485
|
+
Math.max(0, originIndex - 100),
|
|
486
|
+
originIndex
|
|
487
|
+
);
|
|
488
|
+
if (
|
|
489
|
+
beforeOrigin.includes("console.log") ||
|
|
490
|
+
beforeOrigin.includes("logger.")
|
|
491
|
+
) {
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Check if used in authentication/authorization context
|
|
501
|
+
*/
|
|
502
|
+
isAuthContext(context) {
|
|
503
|
+
// Check for auth keywords
|
|
504
|
+
const hasAuthKeyword = this.authKeywords.some((keyword) =>
|
|
505
|
+
context.includes(keyword)
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
if (!hasAuthKeyword) {
|
|
509
|
+
// No auth context detected, but check for suspicious patterns
|
|
510
|
+
return this.hasSuspiciousAuthPattern(context);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Has auth keyword - check if origin is used in conditional/assignment
|
|
514
|
+
const originIndex = context.indexOf("origin");
|
|
515
|
+
if (originIndex === -1) return false;
|
|
516
|
+
|
|
517
|
+
// Get text around origin usage
|
|
518
|
+
const beforeOrigin = context.substring(
|
|
519
|
+
Math.max(0, originIndex - 150),
|
|
520
|
+
originIndex
|
|
521
|
+
);
|
|
522
|
+
const afterOrigin = context.substring(
|
|
523
|
+
originIndex,
|
|
524
|
+
Math.min(context.length, originIndex + 150)
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
// Check for conditional usage (if statements)
|
|
528
|
+
if (
|
|
529
|
+
beforeOrigin.includes("if") ||
|
|
530
|
+
beforeOrigin.includes("else") ||
|
|
531
|
+
beforeOrigin.includes("switch")
|
|
532
|
+
) {
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Check for comparison operators
|
|
537
|
+
if (
|
|
538
|
+
afterOrigin.includes("===") ||
|
|
539
|
+
afterOrigin.includes("==") ||
|
|
540
|
+
afterOrigin.includes("!==") ||
|
|
541
|
+
afterOrigin.includes("!=") ||
|
|
542
|
+
afterOrigin.includes("includes")
|
|
543
|
+
) {
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Check for assignment to auth-related variables
|
|
548
|
+
if (
|
|
549
|
+
beforeOrigin.includes("const") ||
|
|
550
|
+
beforeOrigin.includes("let") ||
|
|
551
|
+
beforeOrigin.includes("var")
|
|
552
|
+
) {
|
|
553
|
+
// Check if assigned to auth-related variable
|
|
554
|
+
const assignmentMatch = beforeOrigin.match(/(?:const|let|var)\s+(\w+)/);
|
|
555
|
+
if (assignmentMatch) {
|
|
556
|
+
const varName = assignmentMatch[1].toLowerCase();
|
|
557
|
+
if (this.authKeywords.some((kw) => varName.includes(kw))) {
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Check for suspicious authentication patterns even without explicit auth keywords
|
|
568
|
+
*/
|
|
569
|
+
hasSuspiciousAuthPattern(context) {
|
|
570
|
+
// Pattern 1: if (origin === something) { allow/deny }
|
|
571
|
+
if (
|
|
572
|
+
context.includes("if") &&
|
|
573
|
+
context.includes("origin") &&
|
|
574
|
+
(context.includes("===") || context.includes("==") || context.includes("includes"))
|
|
575
|
+
) {
|
|
576
|
+
// Check if followed by access control logic
|
|
577
|
+
const originIndex = context.indexOf("origin");
|
|
578
|
+
const afterOrigin = context.substring(originIndex);
|
|
579
|
+
|
|
580
|
+
// Strong indicators of access control
|
|
581
|
+
if (
|
|
582
|
+
afterOrigin.includes("return") ||
|
|
583
|
+
afterOrigin.includes("throw") ||
|
|
584
|
+
afterOrigin.includes("error") ||
|
|
585
|
+
afterOrigin.includes("403") ||
|
|
586
|
+
afterOrigin.includes("401") ||
|
|
587
|
+
afterOrigin.includes("unauthorized") ||
|
|
588
|
+
afterOrigin.includes("forbidden") ||
|
|
589
|
+
afterOrigin.includes("hasaccess") ||
|
|
590
|
+
afterOrigin.includes("canaccess") ||
|
|
591
|
+
afterOrigin.includes("req.user") ||
|
|
592
|
+
afterOrigin.includes("request.user")
|
|
593
|
+
) {
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Check for suspicious return values
|
|
598
|
+
// Pattern: return { hasAccess: true }, return { verified: true }, etc.
|
|
599
|
+
if (
|
|
600
|
+
afterOrigin.match(/return\s*\{[^}]*(access|verified|valid|authorized|authenticated|permission|granted|allowed)/i)
|
|
601
|
+
) {
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Pattern 2: switch/case on origin
|
|
607
|
+
if (context.includes("switch") && context.includes("origin")) {
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Pattern 3: origin-based routing/logic with req.user assignment
|
|
612
|
+
if (
|
|
613
|
+
context.includes("origin") &&
|
|
614
|
+
(context.includes("req.user") ||
|
|
615
|
+
context.includes("request.user") ||
|
|
616
|
+
context.includes("route") ||
|
|
617
|
+
context.includes("redirect") ||
|
|
618
|
+
context.includes("next("))
|
|
619
|
+
) {
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Pattern 4: Array.includes() with origin - likely allowlist check
|
|
624
|
+
// This is VERY likely to be auth-related even if no explicit keywords
|
|
625
|
+
if (
|
|
626
|
+
context.match(/\.includes\([^)]*origin[^)]*\)/i)
|
|
627
|
+
) {
|
|
628
|
+
// Check if followed by return/access control
|
|
629
|
+
const includesIndex = context.indexOf(".includes");
|
|
630
|
+
const afterIncludes = context.substring(includesIndex);
|
|
631
|
+
|
|
632
|
+
if (
|
|
633
|
+
afterIncludes.match(/return\s*\{/) ||
|
|
634
|
+
afterIncludes.includes("return true") ||
|
|
635
|
+
afterIncludes.includes("return false") ||
|
|
636
|
+
afterIncludes.includes("req.user") ||
|
|
637
|
+
afterIncludes.includes("throw") ||
|
|
638
|
+
afterIncludes.includes("next(") ||
|
|
639
|
+
afterIncludes.includes("unauthorized") ||
|
|
640
|
+
afterIncludes.includes("forbidden")
|
|
641
|
+
) {
|
|
642
|
+
// But still exclude if it's clearly CORS
|
|
643
|
+
const hasCorsCors = context.includes("setaccesscontrolalloworigin") ||
|
|
644
|
+
context.includes("access-control-allow-origin") ||
|
|
645
|
+
context.includes("corsOptions") ||
|
|
646
|
+
context.includes("cors(");
|
|
647
|
+
|
|
648
|
+
if (!hasCorsCors) {
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Pattern 5: origin?.includes(), origin?.endsWith(), origin?.startsWith()
|
|
655
|
+
// These are often used for domain/subdomain checks for access control
|
|
656
|
+
if (
|
|
657
|
+
context.match(/origin\??\.(includes|endswith|startswith)\(/i)
|
|
658
|
+
) {
|
|
659
|
+
// Check if followed by access control logic
|
|
660
|
+
const originIndex = context.indexOf("origin");
|
|
661
|
+
const afterOrigin = context.substring(originIndex);
|
|
662
|
+
|
|
663
|
+
if (
|
|
664
|
+
afterOrigin.match(/return\s*\{/) || // Returning object
|
|
665
|
+
afterOrigin.includes("return true") ||
|
|
666
|
+
afterOrigin.includes("return false") ||
|
|
667
|
+
afterOrigin.includes("req.user") ||
|
|
668
|
+
afterOrigin.includes("permission")
|
|
669
|
+
) {
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Pattern 6: Function name suggests auth/access control
|
|
675
|
+
// Even without keywords in code, function name tells us the intent
|
|
676
|
+
const functionNamePatterns = [
|
|
677
|
+
'checkaccess',
|
|
678
|
+
'hasaccess',
|
|
679
|
+
'canaccess',
|
|
680
|
+
'verifyuser',
|
|
681
|
+
'identifyuser',
|
|
682
|
+
'getuser',
|
|
683
|
+
'authenticate',
|
|
684
|
+
'authorize',
|
|
685
|
+
'validate',
|
|
686
|
+
];
|
|
687
|
+
|
|
688
|
+
const lowerContext = context.toLowerCase();
|
|
689
|
+
if (
|
|
690
|
+
functionNamePatterns.some(pattern => lowerContext.includes(`function ${pattern}`)) ||
|
|
691
|
+
functionNamePatterns.some(pattern => lowerContext.includes(`async function ${pattern}`)) ||
|
|
692
|
+
functionNamePatterns.some(pattern => lowerContext.includes(`const ${pattern} =`))
|
|
693
|
+
) {
|
|
694
|
+
// Function name suggests access control, and it uses origin
|
|
695
|
+
if (lowerContext.includes("origin")) {
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
cleanup() {
|
|
704
|
+
// Cleanup if needed
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
module.exports = S005SymbolBasedAnalyzer;
|