@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,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic analyzer for: S022 – Escape data properly based on output context
|
|
3
|
+
* Purpose: Prevent XSS attacks by ensuring proper escaping/sanitization based on output context
|
|
4
|
+
* Detects: unsafe innerHTML, eval, location assignments, dangerous attributes, etc.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
class S022Analyzer {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.ruleId = 'S022';
|
|
13
|
+
this.ruleName = 'Escape data properly based on output context';
|
|
14
|
+
this.description = 'Ensure data is properly escaped based on output context to prevent XSS';
|
|
15
|
+
|
|
16
|
+
// HTML context - dangerous methods
|
|
17
|
+
this.htmlDangerousMethods = [
|
|
18
|
+
'innerHTML',
|
|
19
|
+
'outerHTML',
|
|
20
|
+
'insertAdjacentHTML',
|
|
21
|
+
'document.write',
|
|
22
|
+
'document.writeln'
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// JavaScript context - dangerous methods
|
|
26
|
+
this.jsDangerousMethods = [
|
|
27
|
+
'eval',
|
|
28
|
+
'Function',
|
|
29
|
+
'setTimeout',
|
|
30
|
+
'setInterval',
|
|
31
|
+
'execScript'
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// URL context - dangerous assignments
|
|
35
|
+
this.urlDangerousMethods = [
|
|
36
|
+
'location.href',
|
|
37
|
+
'window.location',
|
|
38
|
+
'location.assign',
|
|
39
|
+
'location.replace',
|
|
40
|
+
'window.open'
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// Framework-specific patterns
|
|
44
|
+
this.frameworkPatterns = {
|
|
45
|
+
react: /dangerouslySetInnerHTML\s*=\s*\{\{?\s*__html\s*:\s*([^}]+)\}\}?/g,
|
|
46
|
+
vue: /v-html\s*=\s*["']([^"']+)["']/g,
|
|
47
|
+
angular: /\[innerHTML\]\s*=\s*["']([^"']+)["']/g
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// User input patterns
|
|
51
|
+
this.userInputPatterns = [
|
|
52
|
+
/req\.(body|query|params)/,
|
|
53
|
+
/request\.(body|query|params)/,
|
|
54
|
+
/localStorage\.getItem/,
|
|
55
|
+
/sessionStorage\.getItem/,
|
|
56
|
+
/window\.location/,
|
|
57
|
+
/location\.(search|hash|href)/,
|
|
58
|
+
/URLSearchParams/,
|
|
59
|
+
/document\.cookie/,
|
|
60
|
+
/window\.name/
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
// Safe escaping/sanitization functions
|
|
64
|
+
this.safeEscapingFunctions = [
|
|
65
|
+
'escape',
|
|
66
|
+
'escapeHtml',
|
|
67
|
+
'sanitize',
|
|
68
|
+
'DOMPurify.sanitize',
|
|
69
|
+
'textContent',
|
|
70
|
+
'innerText',
|
|
71
|
+
'setAttribute',
|
|
72
|
+
'encodeURIComponent',
|
|
73
|
+
'encodeURI',
|
|
74
|
+
'validator.escape',
|
|
75
|
+
'xss.filterXSS',
|
|
76
|
+
'xss-filters'
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async analyze(files, language, options = {}) {
|
|
81
|
+
const violations = [];
|
|
82
|
+
|
|
83
|
+
for (const filePath of files) {
|
|
84
|
+
if (options.verbose) {
|
|
85
|
+
console.log(`🔍 Running S022 analysis on ${path.basename(filePath)}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
90
|
+
const fileViolations = await this.analyzeFile(filePath, content, language, options);
|
|
91
|
+
violations.push(...fileViolations);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (options.verbose) {
|
|
94
|
+
console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return violations;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async analyzeFile(filePath, content, language, options = {}) {
|
|
103
|
+
switch (language) {
|
|
104
|
+
case 'typescript':
|
|
105
|
+
case 'javascript':
|
|
106
|
+
return this.analyzeJavaScript(filePath, content, options);
|
|
107
|
+
default:
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async analyzeJavaScript(filePath, content, options = {}) {
|
|
113
|
+
const violations = [];
|
|
114
|
+
const lines = content.split('\n');
|
|
115
|
+
|
|
116
|
+
// 1. Check HTML context violations
|
|
117
|
+
violations.push(...this.checkHtmlContext(content, lines, filePath));
|
|
118
|
+
|
|
119
|
+
// 2. Check JavaScript context violations
|
|
120
|
+
violations.push(...this.checkJavaScriptContext(content, lines, filePath));
|
|
121
|
+
|
|
122
|
+
// 3. Check URL context violations
|
|
123
|
+
violations.push(...this.checkUrlContext(content, lines, filePath));
|
|
124
|
+
|
|
125
|
+
// 4. Check framework-specific violations
|
|
126
|
+
violations.push(...this.checkFrameworkPatterns(content, lines, filePath));
|
|
127
|
+
|
|
128
|
+
// 5. Check dangerous event handlers
|
|
129
|
+
violations.push(...this.checkDangerousEventHandlers(content, lines, filePath));
|
|
130
|
+
|
|
131
|
+
return violations;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
checkHtmlContext(content, lines, filePath) {
|
|
135
|
+
const violations = [];
|
|
136
|
+
|
|
137
|
+
for (const method of this.htmlDangerousMethods) {
|
|
138
|
+
// Pattern: element.innerHTML = userInput
|
|
139
|
+
const pattern = new RegExp(`\\.${method}\\s*=\\s*([^;]+)`, 'gi');
|
|
140
|
+
let match;
|
|
141
|
+
|
|
142
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
143
|
+
const lineNumber = content.substring(0, match.index).split('\n').length;
|
|
144
|
+
const lineText = lines[lineNumber - 1] || '';
|
|
145
|
+
const assignment = match[1].trim();
|
|
146
|
+
|
|
147
|
+
// Check if value comes from user input
|
|
148
|
+
if (this.isUserInput(assignment)) {
|
|
149
|
+
// Check if there's sanitization
|
|
150
|
+
if (!this.hasSanitization(assignment)) {
|
|
151
|
+
violations.push({
|
|
152
|
+
ruleId: this.ruleId,
|
|
153
|
+
file: filePath,
|
|
154
|
+
line: lineNumber,
|
|
155
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
156
|
+
message: `Unsafe use of '${method}' with unsanitized user input. Use textContent or sanitize with DOMPurify.`,
|
|
157
|
+
severity: 'error',
|
|
158
|
+
code: lineText.trim(),
|
|
159
|
+
type: 'html_context_unsafe',
|
|
160
|
+
context: 'html',
|
|
161
|
+
confidence: 0.9,
|
|
162
|
+
suggestion: `Use 'textContent' instead of '${method}' or sanitize with DOMPurify.sanitize()`
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Even if not direct user input, innerHTML is risky
|
|
167
|
+
else if (!this.hasSanitization(assignment) && !this.isLiteralString(assignment)) {
|
|
168
|
+
violations.push({
|
|
169
|
+
ruleId: this.ruleId,
|
|
170
|
+
file: filePath,
|
|
171
|
+
line: lineNumber,
|
|
172
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
173
|
+
message: `Potentially unsafe use of '${method}'. Consider using textContent or sanitizing the input.`,
|
|
174
|
+
severity: 'warning',
|
|
175
|
+
code: lineText.trim(),
|
|
176
|
+
type: 'html_context_potential',
|
|
177
|
+
context: 'html',
|
|
178
|
+
confidence: 0.6,
|
|
179
|
+
suggestion: `Verify the source of data and use appropriate escaping/sanitization`
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return violations;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
checkJavaScriptContext(content, lines, filePath) {
|
|
189
|
+
const violations = [];
|
|
190
|
+
|
|
191
|
+
// Check eval() - always dangerous
|
|
192
|
+
const evalPattern = /\beval\s*\(\s*([^)]+)\)/gi;
|
|
193
|
+
let match;
|
|
194
|
+
|
|
195
|
+
while ((match = evalPattern.exec(content)) !== null) {
|
|
196
|
+
const lineNumber = content.substring(0, match.index).split('\n').length;
|
|
197
|
+
const lineText = lines[lineNumber - 1] || '';
|
|
198
|
+
const argument = match[1].trim();
|
|
199
|
+
|
|
200
|
+
// Skip if it's clearly safe (literal string)
|
|
201
|
+
if (!this.isLiteralString(argument)) {
|
|
202
|
+
const isUserInput = this.isUserInput(argument);
|
|
203
|
+
|
|
204
|
+
violations.push({
|
|
205
|
+
ruleId: this.ruleId,
|
|
206
|
+
file: filePath,
|
|
207
|
+
line: lineNumber,
|
|
208
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
209
|
+
message: `Dangerous use of 'eval' with ${isUserInput ? 'user input' : 'dynamic code'}. Avoid using eval entirely.`,
|
|
210
|
+
severity: isUserInput ? 'error' : 'warning',
|
|
211
|
+
code: lineText.trim(),
|
|
212
|
+
type: 'javascript_context_unsafe',
|
|
213
|
+
context: 'javascript',
|
|
214
|
+
confidence: isUserInput ? 0.95 : 0.7,
|
|
215
|
+
suggestion: `Never use 'eval' with user input. Use safer alternatives like JSON.parse()`
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check new Function() constructor - dangerous
|
|
221
|
+
const functionConstructorPattern = /new\s+Function\s*\(\s*([^)]+)\)/gi;
|
|
222
|
+
|
|
223
|
+
while ((match = functionConstructorPattern.exec(content)) !== null) {
|
|
224
|
+
const lineNumber = content.substring(0, match.index).split('\n').length;
|
|
225
|
+
const lineText = lines[lineNumber - 1] || '';
|
|
226
|
+
const argument = match[1].trim();
|
|
227
|
+
|
|
228
|
+
const isUserInput = this.isUserInput(argument);
|
|
229
|
+
|
|
230
|
+
violations.push({
|
|
231
|
+
ruleId: this.ruleId,
|
|
232
|
+
file: filePath,
|
|
233
|
+
line: lineNumber,
|
|
234
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
235
|
+
message: `Dangerous use of 'Function' constructor with ${isUserInput ? 'user input' : 'dynamic code'}. Avoid using Function constructor.`,
|
|
236
|
+
severity: isUserInput ? 'error' : 'warning',
|
|
237
|
+
code: lineText.trim(),
|
|
238
|
+
type: 'javascript_context_unsafe',
|
|
239
|
+
context: 'javascript',
|
|
240
|
+
confidence: isUserInput ? 0.95 : 0.7,
|
|
241
|
+
suggestion: `Never use 'new Function()' with user input. Use safer alternatives.`
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Check setTimeout/setInterval with STRING arguments (not function callbacks)
|
|
246
|
+
// Only flag if the first argument is a string, not a function
|
|
247
|
+
const timerPatterns = [
|
|
248
|
+
{ method: 'setTimeout', pattern: /\bsetTimeout\s*\(\s*['"`]([^'"`]+)['"`]/gi },
|
|
249
|
+
{ method: 'setInterval', pattern: /\bsetInterval\s*\(\s*['"`]([^'"`]+)['"`]/gi }
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
for (const { method, pattern } of timerPatterns) {
|
|
253
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
254
|
+
const lineNumber = content.substring(0, match.index).split('\n').length;
|
|
255
|
+
const lineText = lines[lineNumber - 1] || '';
|
|
256
|
+
const codeString = match[1].trim();
|
|
257
|
+
|
|
258
|
+
violations.push({
|
|
259
|
+
ruleId: this.ruleId,
|
|
260
|
+
file: filePath,
|
|
261
|
+
line: lineNumber,
|
|
262
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
263
|
+
message: `Dangerous use of '${method}' with string code. Use function callback instead.`,
|
|
264
|
+
severity: 'error',
|
|
265
|
+
code: lineText.trim(),
|
|
266
|
+
type: 'javascript_context_unsafe',
|
|
267
|
+
context: 'javascript',
|
|
268
|
+
confidence: 0.95,
|
|
269
|
+
suggestion: `Use ${method}(() => { /* code */ }, delay) instead of ${method}("code", delay)`
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return violations;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
checkUrlContext(content, lines, filePath) {
|
|
278
|
+
const violations = [];
|
|
279
|
+
|
|
280
|
+
// Patterns for URL assignments
|
|
281
|
+
const urlPatterns = [
|
|
282
|
+
/location\.href\s*=\s*([^;]+)/gi,
|
|
283
|
+
/window\.location\s*=\s*([^;]+)/gi,
|
|
284
|
+
/location\.assign\s*\(\s*([^)]+)\)/gi,
|
|
285
|
+
/location\.replace\s*\(\s*([^)]+)\)/gi,
|
|
286
|
+
/window\.open\s*\(\s*([^,)]+)/gi
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
for (const pattern of urlPatterns) {
|
|
290
|
+
let match;
|
|
291
|
+
|
|
292
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
293
|
+
const lineNumber = content.substring(0, match.index).split('\n').length;
|
|
294
|
+
const lineText = lines[lineNumber - 1] || '';
|
|
295
|
+
const urlValue = match[1].trim();
|
|
296
|
+
|
|
297
|
+
// Skip if it's a literal string or constant
|
|
298
|
+
if (this.isLiteralString(urlValue) || this.isConstantOrPropertyAccess(urlValue)) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Check if URL comes from user input
|
|
303
|
+
if (this.isUserInput(urlValue)) {
|
|
304
|
+
// Check if there's validation
|
|
305
|
+
if (!this.hasUrlValidation(content, match.index)) {
|
|
306
|
+
violations.push({
|
|
307
|
+
ruleId: this.ruleId,
|
|
308
|
+
file: filePath,
|
|
309
|
+
line: lineNumber,
|
|
310
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
311
|
+
message: `Unsafe URL assignment with user input. Validate and whitelist URLs to prevent open redirect vulnerabilities.`,
|
|
312
|
+
severity: 'error',
|
|
313
|
+
code: lineText.trim(),
|
|
314
|
+
type: 'url_context_unsafe',
|
|
315
|
+
context: 'url',
|
|
316
|
+
confidence: 0.85,
|
|
317
|
+
suggestion: 'Validate URLs against a whitelist of allowed hosts/domains'
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return violations;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
checkFrameworkPatterns(content, lines, filePath) {
|
|
328
|
+
const violations = [];
|
|
329
|
+
|
|
330
|
+
// React: dangerouslySetInnerHTML
|
|
331
|
+
const reactPattern = this.frameworkPatterns.react;
|
|
332
|
+
let match;
|
|
333
|
+
|
|
334
|
+
while ((match = reactPattern.exec(content)) !== null) {
|
|
335
|
+
const lineNumber = content.substring(0, match.index).split('\n').length;
|
|
336
|
+
const lineText = lines[lineNumber - 1] || '';
|
|
337
|
+
const htmlValue = match[1].trim();
|
|
338
|
+
|
|
339
|
+
// Check if sanitized
|
|
340
|
+
if (!this.hasSanitization(htmlValue)) {
|
|
341
|
+
violations.push({
|
|
342
|
+
ruleId: this.ruleId,
|
|
343
|
+
file: filePath,
|
|
344
|
+
line: lineNumber,
|
|
345
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
346
|
+
message: `React 'dangerouslySetInnerHTML' without sanitization. Use DOMPurify.sanitize() or avoid using this prop.`,
|
|
347
|
+
severity: 'error',
|
|
348
|
+
code: lineText.trim(),
|
|
349
|
+
type: 'react_unsafe_html',
|
|
350
|
+
context: 'html',
|
|
351
|
+
framework: 'react',
|
|
352
|
+
confidence: 0.9,
|
|
353
|
+
suggestion: 'Sanitize HTML with DOMPurify: dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(html)}}'
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Vue: v-html
|
|
359
|
+
const vuePattern = this.frameworkPatterns.vue;
|
|
360
|
+
while ((match = vuePattern.exec(content)) !== null) {
|
|
361
|
+
const lineNumber = content.substring(0, match.index).split('\n').length;
|
|
362
|
+
const lineText = lines[lineNumber - 1] || '';
|
|
363
|
+
|
|
364
|
+
violations.push({
|
|
365
|
+
ruleId: this.ruleId,
|
|
366
|
+
file: filePath,
|
|
367
|
+
line: lineNumber,
|
|
368
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
369
|
+
message: `Vue 'v-html' directive can lead to XSS. Use 'v-text' or sanitize with DOMPurify.`,
|
|
370
|
+
severity: 'warning',
|
|
371
|
+
code: lineText.trim(),
|
|
372
|
+
type: 'vue_unsafe_html',
|
|
373
|
+
context: 'html',
|
|
374
|
+
framework: 'vue',
|
|
375
|
+
confidence: 0.8,
|
|
376
|
+
suggestion: 'Use v-text for plain text or sanitize HTML before using v-html'
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Angular: [innerHTML]
|
|
381
|
+
const angularPattern = this.frameworkPatterns.angular;
|
|
382
|
+
while ((match = angularPattern.exec(content)) !== null) {
|
|
383
|
+
const lineNumber = content.substring(0, match.index).split('\n').length;
|
|
384
|
+
const lineText = lines[lineNumber - 1] || '';
|
|
385
|
+
|
|
386
|
+
violations.push({
|
|
387
|
+
ruleId: this.ruleId,
|
|
388
|
+
file: filePath,
|
|
389
|
+
line: lineNumber,
|
|
390
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
391
|
+
message: `Angular '[innerHTML]' binding can lead to XSS. Use Angular's DomSanitizer or avoid innerHTML binding.`,
|
|
392
|
+
severity: 'warning',
|
|
393
|
+
code: lineText.trim(),
|
|
394
|
+
type: 'angular_unsafe_html',
|
|
395
|
+
context: 'html',
|
|
396
|
+
framework: 'angular',
|
|
397
|
+
confidence: 0.8,
|
|
398
|
+
suggestion: 'Use DomSanitizer.sanitize() or bind to textContent instead'
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return violations;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
checkDangerousEventHandlers(content, lines, filePath) {
|
|
406
|
+
const violations = [];
|
|
407
|
+
|
|
408
|
+
// Pattern: element.setAttribute('onclick', ...)
|
|
409
|
+
const eventHandlerPattern = /\.setAttribute\s*\(\s*['"]on\w+['"]\s*,\s*([^)]+)\)/gi;
|
|
410
|
+
let match;
|
|
411
|
+
|
|
412
|
+
while ((match = eventHandlerPattern.exec(content)) !== null) {
|
|
413
|
+
const lineNumber = content.substring(0, match.index).split('\n').length;
|
|
414
|
+
const lineText = lines[lineNumber - 1] || '';
|
|
415
|
+
const handlerValue = match[1].trim();
|
|
416
|
+
|
|
417
|
+
// Check if handler contains user input
|
|
418
|
+
if (this.isUserInput(handlerValue) || !this.isLiteralString(handlerValue)) {
|
|
419
|
+
violations.push({
|
|
420
|
+
ruleId: this.ruleId,
|
|
421
|
+
file: filePath,
|
|
422
|
+
line: lineNumber,
|
|
423
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
424
|
+
message: `Dangerous dynamic event handler assignment. Avoid setting event handlers with user input.`,
|
|
425
|
+
severity: 'error',
|
|
426
|
+
code: lineText.trim(),
|
|
427
|
+
type: 'event_handler_unsafe',
|
|
428
|
+
context: 'attribute',
|
|
429
|
+
confidence: 0.85,
|
|
430
|
+
suggestion: 'Use addEventListener with proper event handling instead of setAttribute for events'
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return violations;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
isUserInput(code) {
|
|
439
|
+
return this.userInputPatterns.some(pattern => pattern.test(code));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
hasSanitization(code) {
|
|
443
|
+
return this.safeEscapingFunctions.some(func =>
|
|
444
|
+
code.toLowerCase().includes(func.toLowerCase())
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
isLiteralString(code) {
|
|
449
|
+
// Check if it's a string literal (quoted)
|
|
450
|
+
const trimmed = code.trim();
|
|
451
|
+
return (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
452
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'")) ||
|
|
453
|
+
(trimmed.startsWith('`') && trimmed.endsWith('`') && !trimmed.includes('${'));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
isConstantOrPropertyAccess(code) {
|
|
457
|
+
// Check if it's accessing a constant or configuration object
|
|
458
|
+
const trimmed = code.trim();
|
|
459
|
+
|
|
460
|
+
// Common constant patterns: routes.X, CONSTANTS.X, config.X, CONFIG.X
|
|
461
|
+
const constantPatterns = [
|
|
462
|
+
/^routes\./i,
|
|
463
|
+
/^ROUTES\./,
|
|
464
|
+
/^constants\./i,
|
|
465
|
+
/^CONSTANTS\./,
|
|
466
|
+
/^config\./i,
|
|
467
|
+
/^CONFIG\./,
|
|
468
|
+
/^settings\./i,
|
|
469
|
+
/^SETTINGS\./,
|
|
470
|
+
/^[A-Z_]+\./, // ALL_CAPS.something
|
|
471
|
+
/^process\.env\./,
|
|
472
|
+
/^environment\./i,
|
|
473
|
+
];
|
|
474
|
+
|
|
475
|
+
// Check if matches constant patterns
|
|
476
|
+
if (constantPatterns.some(pattern => pattern.test(trimmed))) {
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Check if it's a simple variable without property access from user input
|
|
481
|
+
// e.g., "redirectUrl" (variable) vs "req.query.redirect" (user input)
|
|
482
|
+
if (!trimmed.includes('.') && !this.isUserInput(trimmed)) {
|
|
483
|
+
// Simple variable, likely from constants or params
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
hasUrlValidation(content, matchIndex) {
|
|
491
|
+
// Check surrounding code for URL validation patterns
|
|
492
|
+
const contextStart = Math.max(0, matchIndex - 300);
|
|
493
|
+
const contextEnd = Math.min(content.length, matchIndex + 100);
|
|
494
|
+
const context = content.substring(contextStart, contextEnd);
|
|
495
|
+
|
|
496
|
+
const validationPatterns = [
|
|
497
|
+
/new\s+URL\s*\(/i,
|
|
498
|
+
/allowedHosts/i,
|
|
499
|
+
/whitelist/i,
|
|
500
|
+
/validateUrl/i,
|
|
501
|
+
/isValidUrl/i,
|
|
502
|
+
/\.hostname/i,
|
|
503
|
+
/\.protocol/i
|
|
504
|
+
];
|
|
505
|
+
|
|
506
|
+
return validationPatterns.some(pattern => pattern.test(context));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
module.exports = new S022Analyzer();
|