@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.
Files changed (69) hide show
  1. package/config/rules/enhanced-rules-registry.json +101 -17
  2. package/config/rules/rules-registry-generated.json +22 -22
  3. package/origin-rules/security-en.md +351 -338
  4. package/package.json +1 -1
  5. package/rules/common/C003_no_vague_abbreviations/analyzer.js +73 -21
  6. package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +206 -2
  7. package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +553 -58
  8. package/rules/common/C029_catch_block_logging/analyzer.js +47 -12
  9. package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +35 -15
  10. package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +9 -5
  11. package/rules/security/S003_open_redirect_protection/README.md +371 -0
  12. package/rules/security/S003_open_redirect_protection/analyzer.js +135 -0
  13. package/rules/security/S003_open_redirect_protection/config.json +58 -0
  14. package/rules/security/S003_open_redirect_protection/symbol-based-analyzer.js +884 -0
  15. package/rules/security/S004_sensitive_data_logging/analyzer.js +135 -0
  16. package/rules/security/S004_sensitive_data_logging/config.json +62 -0
  17. package/rules/security/S004_sensitive_data_logging/symbol-based-analyzer.js +592 -0
  18. package/rules/security/S005_no_origin_auth/analyzer.js +97 -148
  19. package/rules/security/S005_no_origin_auth/config.json +28 -67
  20. package/rules/security/S005_no_origin_auth/symbol-based-analyzer.js +708 -0
  21. package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +170 -31
  22. package/rules/security/S010_no_insecure_encryption/analyzer.js +8 -2
  23. package/rules/security/S012_hardcoded_secrets/analyzer.js +149 -0
  24. package/rules/security/S012_hardcoded_secrets/config.json +75 -0
  25. package/rules/security/S012_hardcoded_secrets/symbol-based-analyzer.js +1204 -0
  26. package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +87 -0
  27. package/rules/security/S017_use_parameterized_queries/analyzer.js +11 -78
  28. package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +1146 -1
  29. package/rules/security/S019_smtp_injection_protection/analyzer.js +120 -0
  30. package/rules/security/S019_smtp_injection_protection/config.json +35 -0
  31. package/rules/security/S019_smtp_injection_protection/symbol-based-analyzer.js +687 -0
  32. package/rules/security/S020_no_eval_dynamic_code/analyzer.js +55 -130
  33. package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +4 -19
  34. package/rules/security/S022_escape_output_context/README.md +254 -0
  35. package/rules/security/S022_escape_output_context/analyzer.js +510 -0
  36. package/rules/security/S022_escape_output_context/config.json +229 -0
  37. package/rules/security/S023_no_json_injection/analyzer.js +15 -0
  38. package/rules/security/S023_no_json_injection/ast-analyzer.js +18 -3
  39. package/rules/security/S023_no_json_injection/config.json +133 -0
  40. package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +41 -0
  41. package/rules/security/S027_no_hardcoded_secrets/analyzer.js +67 -8
  42. package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +29 -6
  43. package/rules/security/S029_csrf_protection/config.json +127 -0
  44. package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +160 -28
  45. package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +81 -19
  46. package/rules/security/S031_secure_session_cookies/analyzer.js +20 -2
  47. package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +100 -0
  48. package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +8 -1
  49. package/rules/security/S032_httponly_session_cookies/analyzer.js +2 -2
  50. package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +115 -0
  51. package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +39 -10
  52. package/rules/security/S036_lfi_rfi_protection/analyzer.js +224 -0
  53. package/rules/security/S036_lfi_rfi_protection/config.json +20 -0
  54. package/rules/security/S040_session_fixation_protection/analyzer.js +153 -0
  55. package/rules/security/S040_session_fixation_protection/config.json +20 -0
  56. package/rules/security/S042_require_re_authentication_for_long_lived/README.md +83 -0
  57. package/rules/security/S042_require_re_authentication_for_long_lived/analyzer.js +153 -0
  58. package/rules/security/S042_require_re_authentication_for_long_lived/config.json +41 -0
  59. package/rules/security/S042_require_re_authentication_for_long_lived/symbol-based-analyzer.js +1139 -0
  60. package/rules/security/S043_password_changes_invalidate_all_sessions/README.md +107 -0
  61. package/rules/security/S043_password_changes_invalidate_all_sessions/analyzer.js +153 -0
  62. package/rules/security/S043_password_changes_invalidate_all_sessions/config.json +41 -0
  63. package/rules/security/S043_password_changes_invalidate_all_sessions/symbol-based-analyzer.js +541 -0
  64. package/docs/COMMAND-EXAMPLES.md +0 -390
  65. package/docs/FILE_LIMITS_COMPLETION_REPORT.md +0 -151
  66. package/docs/FOLDER_STRUCTURE.md +0 -59
  67. package/docs/SIMPLIFIED_USAGE_GUIDE.md +0 -208
  68. package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +0 -541
  69. 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;