@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,687 @@
1
+ const { Project, SyntaxKind } = require("ts-morph");
2
+
3
+ /**
4
+ * S019 - SMTP Injection Protection (Symbol-Based Analyzer)
5
+ *
6
+ * Detects potential SMTP/IMAP injection vulnerabilities by identifying:
7
+ * - Unsanitized user input in email fields (to, subject, cc, bcc, reply-to)
8
+ * - Missing input validation for CRLF characters (\r\n)
9
+ * - Direct SMTP protocol manipulation without proper sanitization
10
+ * - Email sending without using secure email service APIs
11
+ *
12
+ * OWASP: A03:2021 - Injection
13
+ * CWE: CWE-93 (Improper Neutralization of CRLF Sequences in HTTP Headers)
14
+ * CWE-144 (Improper Neutralization of Line Delimiters)
15
+ */
16
+ class S019SymbolBasedAnalyzer {
17
+ constructor(semanticEngine = null) {
18
+ this.ruleId = "S019";
19
+ this.semanticEngine = semanticEngine;
20
+
21
+ // Email field names that are vulnerable to SMTP injection
22
+ this.emailFields = [
23
+ 'to', 'from', 'subject', 'cc', 'bcc', 'replyTo', 'reply-to',
24
+ 'sender', 'recipients', 'recipient', 'email', 'emailAddress',
25
+ 'toAddress', 'fromAddress', 'ccAddress', 'bccAddress',
26
+ ];
27
+
28
+ // SMTP/email library methods
29
+ this.emailMethods = [
30
+ 'sendMail', 'sendEmail', 'send',
31
+ 'setTo', 'setFrom', 'setSubject', 'setCc', 'setBcc', 'setReplyTo',
32
+ 'addTo', 'addCc', 'addBcc', 'addRecipient',
33
+ 'createTransport', 'createMessage',
34
+ 'dispatchEmail', 'dispatchMail', // Use specific email dispatch, not generic 'dispatch'
35
+ ];
36
+
37
+ // Popular email service libraries
38
+ this.emailLibraries = [
39
+ 'nodemailer', 'sendgrid', '@sendgrid/mail',
40
+ 'mailgun', 'mailgun-js', '@mailgun',
41
+ 'aws-sdk', '@aws-sdk/client-ses',
42
+ 'postmark', '@postmark',
43
+ 'mailchimp', '@mailchimp',
44
+ 'sparkpost', 'node-ses',
45
+ ];
46
+
47
+ // Dangerous SMTP protocol commands
48
+ this.smtpCommands = [
49
+ 'MAIL FROM', 'RCPT TO', 'DATA', 'RSET', 'VRFY', 'EXPN',
50
+ ];
51
+
52
+ // Safe sanitization functions
53
+ this.sanitizationFunctions = [
54
+ 'sanitize', 'clean', 'escape', 'strip', 'remove',
55
+ 'validate', 'filter', 'normalize',
56
+ 'replaceCRLF', 'removeCRLF', 'stripNewlines',
57
+ 'encodeHeader', 'escapeHeader',
58
+ ];
59
+
60
+ // User input sources
61
+ this.userInputSources = [
62
+ 'req.body', 'req.query', 'req.params',
63
+ 'request.body', 'request.query', 'request.params',
64
+ 'ctx.request.body', 'ctx.query', 'ctx.params',
65
+ 'input', 'formData', 'userData', 'userInput',
66
+ ];
67
+
68
+ // Safe library patterns (libraries with built-in sanitization)
69
+ this.safeLibraryPatterns = [
70
+ 'safe', 'secure', 'validated', 'sanitized',
71
+ 'createSafe', 'getSafe', 'withSanitization',
72
+ ];
73
+ }
74
+
75
+ async analyze(sourceFile, filePath = "") {
76
+ const violations = [];
77
+
78
+ // Check 1: Function calls with email methods
79
+ const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
80
+ for (const callExpr of callExpressions) {
81
+ const violation = this.checkEmailMethodCall(callExpr, filePath);
82
+ if (violation) violations.push(violation);
83
+ }
84
+
85
+ // Check 2: Object literals for email configuration
86
+ const objectLiterals = sourceFile.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression);
87
+ for (const objLit of objectLiterals) {
88
+ const violation = this.checkEmailConfigObject(objLit, filePath);
89
+ if (violation) violations.push(violation);
90
+ }
91
+
92
+ // Check 3: Variable assignments to email fields
93
+ const propertyAssignments = sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAssignment);
94
+ for (const propAssign of propertyAssignments) {
95
+ const violation = this.checkEmailFieldAssignment(propAssign, filePath);
96
+ if (violation) violations.push(violation);
97
+ }
98
+
99
+ // Check 4: String concatenation with SMTP commands
100
+ const binaryExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.BinaryExpression);
101
+ for (const binExpr of binaryExpressions) {
102
+ const violation = this.checkSMTPCommandConcatenation(binExpr, filePath);
103
+ if (violation) violations.push(violation);
104
+ }
105
+
106
+ return violations;
107
+ }
108
+
109
+ /**
110
+ * Check email method calls for unsanitized input
111
+ */
112
+ checkEmailMethodCall(callExpr, filePath) {
113
+ try {
114
+ const expression = callExpr.getExpression();
115
+ const methodName = this.getMethodName(expression);
116
+
117
+ if (!methodName || !this.isEmailMethod(methodName)) {
118
+ return null;
119
+ }
120
+
121
+ // Check if the method is called on a safe library object
122
+ if (this.isCalledOnSafeObject(expression)) {
123
+ return null;
124
+ }
125
+
126
+ // Get arguments passed to the email method
127
+ const args = callExpr.getArguments();
128
+ if (args.length === 0) return null;
129
+
130
+ // Check if arguments contain unsanitized user input
131
+ for (const arg of args) {
132
+ const argText = arg.getText();
133
+
134
+ // Skip if argument is sanitized
135
+ if (this.isSanitized(argText)) {
136
+ continue;
137
+ }
138
+
139
+ // Check if argument comes from user input
140
+ if (this.containsUserInput(argText)) {
141
+ return {
142
+ line: callExpr.getStartLineNumber(),
143
+ column: callExpr.getStart() - callExpr.getStartLinePos(),
144
+ message: `Potential SMTP injection: Email method '${methodName}' receives unsanitized user input. Sanitize input to remove CRLF characters (\\r\\n) before using in email fields.`,
145
+ severity: "error",
146
+ ruleId: this.ruleId,
147
+ method: methodName,
148
+ suggestion: "Use email service APIs (SendGrid, SES) or sanitize input with replaceCRLF/stripNewlines",
149
+ };
150
+ }
151
+
152
+ // Check if argument is an object literal with email fields
153
+ if (arg.getKind() === SyntaxKind.ObjectLiteralExpression) {
154
+ const objViolation = this.checkEmailConfigObject(arg, filePath);
155
+ if (objViolation) {
156
+ return objViolation;
157
+ }
158
+ }
159
+ }
160
+
161
+ return null;
162
+ } catch (error) {
163
+ return null;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Check email configuration objects
169
+ */
170
+ checkEmailConfigObject(objLit, filePath) {
171
+ try {
172
+ const properties = objLit.getProperties();
173
+
174
+ // Check if this object is used within a safe library context
175
+ if (this.isWithinSafeLibraryContext(objLit)) {
176
+ return null;
177
+ }
178
+
179
+ // Check if this object is passed to an email method
180
+ // If not, it's likely just a data object, not an email config
181
+ if (!this.isPassedToEmailMethod(objLit)) {
182
+ return null;
183
+ }
184
+
185
+ for (const prop of properties) {
186
+ if (prop.getKind() !== SyntaxKind.PropertyAssignment) continue;
187
+
188
+ const propName = prop.getName();
189
+ const initializer = prop.getInitializer();
190
+
191
+ if (!initializer || !this.isEmailField(propName)) continue;
192
+
193
+ const initText = initializer.getText();
194
+
195
+ // Skip if value is sanitized
196
+ if (this.isSanitized(initText)) continue;
197
+
198
+ // Skip if value is a constant string
199
+ if (this.isConstantString(initText)) continue;
200
+
201
+ // Skip if the variable was previously validated
202
+ if (this.isPreviouslyValidated(initializer, initText)) continue;
203
+
204
+ // Check if value contains user input
205
+ if (this.containsUserInput(initText)) {
206
+ return {
207
+ line: prop.getStartLineNumber(),
208
+ column: prop.getStart() - prop.getStartLinePos(),
209
+ message: `Potential SMTP injection: Email field '${propName}' contains unsanitized user input. Remove CRLF characters (\\r\\n) to prevent header injection.`,
210
+ severity: "error",
211
+ ruleId: this.ruleId,
212
+ field: propName,
213
+ suggestion: "Sanitize with: value.replace(/[\\r\\n]/g, '')",
214
+ };
215
+ }
216
+
217
+ // Check if value contains template literal with expressions
218
+ if (initializer.getKind() === SyntaxKind.TemplateExpression) {
219
+ const hasUnsafeExpression = this.hasUnsafeTemplateExpression(initializer);
220
+ if (hasUnsafeExpression) {
221
+ return {
222
+ line: prop.getStartLineNumber(),
223
+ column: prop.getStart() - prop.getStartLinePos(),
224
+ message: `Potential SMTP injection: Email field '${propName}' uses template literal with potentially unsafe expressions. Sanitize all dynamic values.`,
225
+ severity: "error",
226
+ ruleId: this.ruleId,
227
+ field: propName,
228
+ suggestion: "Sanitize each expression before interpolation",
229
+ };
230
+ }
231
+ }
232
+ }
233
+
234
+ return null;
235
+ } catch (error) {
236
+ return null;
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Check email field assignments
242
+ */
243
+ checkEmailFieldAssignment(propAssign, filePath) {
244
+ try {
245
+ const name = propAssign.getName();
246
+ const initializer = propAssign.getInitializer();
247
+
248
+ if (!initializer || !this.isEmailField(name)) {
249
+ return null;
250
+ }
251
+
252
+ // Check if this assignment is within a safe library context
253
+ if (this.isWithinSafeLibraryContext(propAssign)) {
254
+ return null;
255
+ }
256
+
257
+ // Check if this assignment is in an object that will be passed to an email method
258
+ // Find the parent object literal
259
+ let parentObj = propAssign.getFirstAncestor(n =>
260
+ n.getKind() === SyntaxKind.ObjectLiteralExpression
261
+ );
262
+ if (parentObj && !this.isPassedToEmailMethod(parentObj)) {
263
+ return null;
264
+ }
265
+
266
+ const initText = initializer.getText();
267
+
268
+ // Skip if sanitized
269
+ if (this.isSanitized(initText)) return null;
270
+
271
+ // Skip constant strings
272
+ if (this.isConstantString(initText)) return null;
273
+
274
+ // Skip if the variable was previously validated
275
+ if (this.isPreviouslyValidated(initializer, initText)) return null;
276
+
277
+ // Check for user input
278
+ if (this.containsUserInput(initText)) {
279
+ return {
280
+ line: propAssign.getStartLineNumber(),
281
+ column: propAssign.getStart() - propAssign.getStartLinePos(),
282
+ message: `Potential SMTP injection: Assignment to '${name}' contains unsanitized user input. Validate and sanitize to prevent SMTP header injection.`,
283
+ severity: "error",
284
+ ruleId: this.ruleId,
285
+ field: name,
286
+ suggestion: "Use validation: /^[a-zA-Z0-9@._ -]+$/.test(value)",
287
+ };
288
+ }
289
+
290
+ return null;
291
+ } catch (error) {
292
+ return null;
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Check string concatenation with SMTP commands
298
+ */
299
+ checkSMTPCommandConcatenation(binExpr, filePath) {
300
+ try {
301
+ if (binExpr.getOperatorToken().getText() !== '+') {
302
+ return null;
303
+ }
304
+
305
+ const leftText = binExpr.getLeft().getText();
306
+ const rightText = binExpr.getRight().getText();
307
+ const fullText = `${leftText} ${rightText}`;
308
+
309
+ // Check if concatenation involves SMTP commands
310
+ const hasSMTPCommand = this.smtpCommands.some(cmd =>
311
+ fullText.includes(cmd)
312
+ );
313
+
314
+ if (!hasSMTPCommand) return null;
315
+
316
+ // Check if right side contains user input
317
+ if (this.containsUserInput(rightText) && !this.isSanitized(rightText)) {
318
+ return {
319
+ line: binExpr.getStartLineNumber(),
320
+ column: binExpr.getStart() - binExpr.getStartLinePos(),
321
+ message: `Critical SMTP injection risk: Direct SMTP command manipulation with unsanitized user input. This allows arbitrary email injection and command execution.`,
322
+ severity: "error",
323
+ ruleId: this.ruleId,
324
+ suggestion: "Never concatenate user input with SMTP commands. Use email service APIs instead.",
325
+ };
326
+ }
327
+
328
+ return null;
329
+ } catch (error) {
330
+ return null;
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Get method name from expression
336
+ */
337
+ getMethodName(expression) {
338
+ try {
339
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
340
+ return expression.getName();
341
+ }
342
+ if (expression.getKind() === SyntaxKind.Identifier) {
343
+ return expression.getText();
344
+ }
345
+ return null;
346
+ } catch {
347
+ return null;
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Check if method is called on a safe object (e.g., safeMailer.send())
353
+ */
354
+ isCalledOnSafeObject(expression) {
355
+ try {
356
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
357
+ // Get the object part (e.g., "safeMailer" from "safeMailer.send")
358
+ const objectExpr = expression.getExpression();
359
+ const objectText = objectExpr.getText();
360
+ const objectLower = objectText.toLowerCase();
361
+
362
+ // Check if object name contains safe patterns
363
+ return this.safeLibraryPatterns.some(pattern =>
364
+ objectLower.includes(pattern.toLowerCase())
365
+ );
366
+ }
367
+ return false;
368
+ } catch {
369
+ return false;
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Check if method name is an email-related method
375
+ */
376
+ isEmailMethod(methodName) {
377
+ const nameLower = methodName.toLowerCase();
378
+
379
+ // Exclude methods that are clearly not email-related
380
+ const excludePatterns = [
381
+ 'csv', 'excel', 'pdf', 'json', 'xml', // File format methods
382
+ 'message', 'msg', // Generic message methods (could be chat, notifications)
383
+ ];
384
+
385
+ // If method contains excluded patterns, it's likely not email
386
+ if (excludePatterns.some(pattern => nameLower.includes(pattern))) {
387
+ return false;
388
+ }
389
+
390
+ return this.emailMethods.some(method =>
391
+ nameLower.includes(method.toLowerCase())
392
+ );
393
+ }
394
+
395
+ /**
396
+ * Check if property name is an email field
397
+ * Uses exact matching or very specific patterns to avoid false positives
398
+ */
399
+ isEmailField(propName) {
400
+ const nameLower = propName.toLowerCase();
401
+
402
+ // Exact match for common email fields
403
+ const exactMatches = [
404
+ 'to', 'from', 'subject', 'cc', 'bcc', 'replyto', 'reply-to',
405
+ 'sender', 'recipients', 'recipient', 'email', 'emailaddress',
406
+ 'toaddress', 'fromaddress', 'ccaddress', 'bccaddress',
407
+ 'toaddresses', 'ccaddresses', 'bccaddresses',
408
+ ];
409
+
410
+ if (exactMatches.includes(nameLower)) {
411
+ return true;
412
+ }
413
+
414
+ // Specific patterns that clearly indicate email fields
415
+ // Only match if it's a clear email-related property
416
+ const emailPatterns = [
417
+ /^to$/i, // Exact "to"
418
+ /^from$/i, // Exact "from"
419
+ /^subject$/i, // Exact "subject"
420
+ /^cc$/i, // Exact "cc"
421
+ /^bcc$/i, // Exact "bcc"
422
+ /^reply[-_]?to$/i, // replyTo, reply-to, reply_to
423
+ /^email$/i, // Exact "email"
424
+ /^.*email.*address$/i, // Contains "email" and ends with "address"
425
+ /^to[-_]address(es)?$/i, // to_address, toAddress, toAddresses
426
+ /^from[-_]address$/i, // from_address, fromAddress
427
+ /^cc[-_]address(es)?$/i, // cc_address, ccAddress
428
+ /^bcc[-_]address(es)?$/i, // bcc_address, bccAddress
429
+ ];
430
+
431
+ return emailPatterns.some(pattern => pattern.test(propName));
432
+ }
433
+
434
+ /**
435
+ * Check if text contains user input sources
436
+ */
437
+ containsUserInput(text) {
438
+ return this.userInputSources.some(source => text.includes(source));
439
+ }
440
+
441
+ /**
442
+ * Check if text is sanitized
443
+ */
444
+ isSanitized(text) {
445
+ // Check for sanitization function calls
446
+ const hasSanitization = this.sanitizationFunctions.some(func =>
447
+ text.includes(func)
448
+ );
449
+
450
+ // Check for CRLF removal patterns
451
+ const hasCRLFRemoval = /replace\([^)]*[\\r\\n][^)]*\)/i.test(text) ||
452
+ /replace\([^)]*[\r\n][^)]*\)/i.test(text);
453
+
454
+ // Check for validation patterns
455
+ const hasValidation = /test\(/.test(text) || /match\(/.test(text);
456
+
457
+ return hasSanitization || hasCRLFRemoval || hasValidation;
458
+ }
459
+
460
+ /**
461
+ * Check if text is a constant string (not dynamic)
462
+ */
463
+ isConstantString(text) {
464
+ // Check if it's a simple string literal
465
+ if (/^['"`][^${}]*['"`]$/.test(text)) return true;
466
+
467
+ // Check if it's an environment variable or config
468
+ if (text.includes('process.env') || text.includes('config.')) return true;
469
+
470
+ return false;
471
+ }
472
+
473
+ /**
474
+ * Check if template expression has unsafe parts
475
+ */
476
+ hasUnsafeTemplateExpression(templateExpr) {
477
+ try {
478
+ const templateSpans = templateExpr.getTemplateSpans();
479
+
480
+ for (const span of templateSpans) {
481
+ const expression = span.getExpression();
482
+ const exprText = expression.getText();
483
+
484
+ // Skip if expression is sanitized
485
+ if (this.isSanitized(exprText)) continue;
486
+
487
+ // Check if expression contains user input
488
+ if (this.containsUserInput(exprText)) return true;
489
+ }
490
+
491
+ return false;
492
+ } catch {
493
+ return false;
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Check if node is within a safe library context (method call or variable)
499
+ */
500
+ isWithinSafeLibraryContext(node) {
501
+ try {
502
+ let parent = node.getParent();
503
+ let depth = 0;
504
+ const maxDepth = 5;
505
+
506
+ while (parent && depth < maxDepth) {
507
+ const parentText = parent.getText();
508
+ const parentLower = parentText.toLowerCase();
509
+
510
+ // Check if parent is a call expression with a safe library pattern
511
+ if (parent.getKind() === SyntaxKind.CallExpression) {
512
+ const callExpr = parent;
513
+ const expression = callExpr.getExpression();
514
+ const exprText = expression.getText();
515
+ const exprLower = exprText.toLowerCase();
516
+
517
+ // Check if method/function name contains safe patterns
518
+ if (this.safeLibraryPatterns.some(pattern => exprLower.includes(pattern))) {
519
+ return true;
520
+ }
521
+ }
522
+
523
+ // Check if parent variable declaration has safe pattern in name
524
+ if (parent.getKind() === SyntaxKind.VariableDeclaration) {
525
+ const varDecl = parent;
526
+ const varName = varDecl.getName();
527
+ const varNameLower = varName.toLowerCase();
528
+
529
+ if (this.safeLibraryPatterns.some(pattern => varNameLower.includes(pattern))) {
530
+ return true;
531
+ }
532
+ }
533
+
534
+ parent = parent.getParent();
535
+ depth++;
536
+ }
537
+
538
+ return false;
539
+ } catch {
540
+ return false;
541
+ }
542
+ }
543
+
544
+ /**
545
+ * Check if object literal is passed to an email method
546
+ * This helps reduce false positives for data objects that happen to have 'email' fields
547
+ */
548
+ isPassedToEmailMethod(objLit) {
549
+ try {
550
+ let parent = objLit.getParent();
551
+ let depth = 0;
552
+ const maxDepth = 3;
553
+
554
+ while (parent && depth < maxDepth) {
555
+ // Check if parent is a call expression
556
+ if (parent.getKind() === SyntaxKind.CallExpression) {
557
+ const callExpr = parent;
558
+ const expression = callExpr.getExpression();
559
+ const methodName = this.getMethodName(expression);
560
+
561
+ // Check if it's an email method
562
+ if (methodName && this.isEmailMethod(methodName)) {
563
+ return true;
564
+ }
565
+ }
566
+
567
+ parent = parent.getParent();
568
+ depth++;
569
+ }
570
+
571
+ return false;
572
+ } catch {
573
+ return false;
574
+ }
575
+ }
576
+
577
+ /**
578
+ * Check if a variable was validated before usage
579
+ * This helps reduce false positives for validated inputs
580
+ */
581
+ isPreviouslyValidated(node, valueText) {
582
+ try {
583
+ // Get the full path and just the variable name
584
+ const fullPath = valueText.trim();
585
+ const variableName = this.extractVariableName(valueText);
586
+ if (!variableName) return false;
587
+
588
+ // Get the containing function or block
589
+ let functionScope = node.getFirstAncestor(n =>
590
+ n.getKind() === SyntaxKind.FunctionDeclaration ||
591
+ n.getKind() === SyntaxKind.FunctionExpression ||
592
+ n.getKind() === SyntaxKind.ArrowFunction ||
593
+ n.getKind() === SyntaxKind.MethodDeclaration
594
+ );
595
+
596
+ if (!functionScope) return false;
597
+
598
+ // Get the current line number
599
+ const currentLine = node.getStartLineNumber();
600
+
601
+ // Look for validation patterns before the current line
602
+ const functionText = functionScope.getText();
603
+ const functionStart = functionScope.getStartLineNumber();
604
+
605
+ // Split function text into lines
606
+ const lines = functionText.split('\n');
607
+ const relevantLines = lines.slice(0, currentLine - functionStart);
608
+
609
+ // Check for validation patterns in preceding lines
610
+ for (const line of relevantLines) {
611
+ // Check if the full path or variable name is mentioned
612
+ const hasFullPath = line.includes(fullPath);
613
+ const hasVariableName = line.includes(variableName);
614
+
615
+ if (!hasFullPath && !hasVariableName) continue;
616
+
617
+ // Pattern 1: if (!regex.test(variable))
618
+ if (/test\s*\(/.test(line) && /if\s*\(/.test(line)) {
619
+ return true;
620
+ }
621
+
622
+ // Pattern 2: if (regex.test(variable))
623
+ if (/test\s*\(/.test(line)) {
624
+ return true;
625
+ }
626
+
627
+ // Pattern 3: const validated = validate(variable)
628
+ if (this.sanitizationFunctions.some(func => line.includes(func))) {
629
+ return true;
630
+ }
631
+
632
+ // Pattern 4: if (!validator.isEmail(variable))
633
+ if (/isEmail|isValid/.test(line)) {
634
+ return true;
635
+ }
636
+
637
+ // Pattern 5: throw/return in conditional after check
638
+ if (/throw|return/.test(line) && /if\s*\(/.test(line)) {
639
+ return true;
640
+ }
641
+ }
642
+
643
+ return false;
644
+ } catch {
645
+ return false;
646
+ }
647
+ }
648
+
649
+ /**
650
+ * Extract variable name from text (e.g., "req.query.recipient" -> "recipient")
651
+ */
652
+ extractVariableName(text) {
653
+ try {
654
+ // Remove whitespace
655
+ text = text.trim();
656
+
657
+ // Pattern: req.query.recipient, request.body.email, etc.
658
+ const dotAccessMatch = text.match(/\.([\w]+)$/);
659
+ if (dotAccessMatch) {
660
+ return dotAccessMatch[1];
661
+ }
662
+
663
+ // Pattern: req.query['recipient'], request.body["email"]
664
+ const bracketMatch = text.match(/\[['"](\w+)['"]\]$/);
665
+ if (bracketMatch) {
666
+ return bracketMatch[1];
667
+ }
668
+
669
+ // Pattern: userData.email
670
+ const simpleMatch = text.match(/^(\w+)\.(\w+)$/);
671
+ if (simpleMatch) {
672
+ return simpleMatch[2];
673
+ }
674
+
675
+ // Simple variable name
676
+ if (/^\w+$/.test(text)) {
677
+ return text;
678
+ }
679
+
680
+ return null;
681
+ } catch {
682
+ return null;
683
+ }
684
+ }
685
+ }
686
+
687
+ module.exports = S019SymbolBasedAnalyzer;