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