@sun-asterisk/sunlint 1.3.36 → 1.3.38
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/cli.js +34 -0
- package/config/rules/enhanced-rules-registry.json +387 -98
- package/config/rules/rules-registry-generated.json +202 -174
- package/config/rules-summary.json +1 -1
- package/core/architecture-integration.js +115 -17
- package/core/cli-action-handler.js +103 -28
- package/core/cli-program.js +7 -2
- package/core/github-annotate-service.js +62 -0
- package/core/impact-integration.js +31 -16
- package/core/init-command.js +261 -0
- package/core/output-service.js +64 -10
- package/core/performance-optimizer.js +1 -1
- package/core/summary-report-service.js +46 -0
- package/core/unified-rule-registry.js +4 -3
- package/docs/DART_RULE_EXECUTION_FLOW.md +1 -1
- package/docs/REGISTRY_GENERATION_DIAGRAM.md +289 -0
- package/docs/REGISTRY_GENERATION_FLOW.md +486 -0
- package/docs/skills/CREATE_NEW_DART_RULE.md +932 -0
- package/engines/eslint-engine.js +6 -0
- package/engines/heuristic-engine.js +23 -10
- package/engines/impact/core/detectors/database-detector.js +1 -1
- package/engines/impact/core/detectors/endpoint-detector.js +1 -1
- package/engines/impact/core/report-generator.js +235 -73
- package/origin-rules/dart-en.md +4 -4
- package/origin-rules/security-en.md +470 -282
- package/package.json +1 -1
- package/rules/dart/D001_recommended_lint_rules/config.json +134 -0
- package/rules/index.js +6 -4
- package/rules/security/S001_backend_auth_communications/dart/analyzer.js +44 -0
- package/rules/security/S001_backend_auth_communications/index.js +87 -0
- package/rules/security/S001_backend_auth_communications/typescript/analyzer.js +164 -0
- package/rules/security/S002_os_command_injection/dart/analyzer.js +44 -0
- package/rules/security/S002_os_command_injection/index.js +87 -0
- package/rules/security/S002_os_command_injection/typescript/analyzer.js +194 -0
- package/rules/security/S008_svg_content_validation/dart/analyzer.js +44 -0
- package/rules/security/S008_svg_content_validation/index.js +87 -0
- package/rules/security/S008_svg_content_validation/typescript/analyzer.js +216 -0
- package/rules/security/S018_no_sensitive_browser_storage/dart/analyzer.js +44 -0
- package/rules/security/S018_no_sensitive_browser_storage/index.js +86 -0
- package/rules/security/S018_no_sensitive_browser_storage/typescript/analyzer.js +193 -0
- package/rules/security/S021_referrer_policy/dart/analyzer.js +44 -0
- package/rules/security/S021_referrer_policy/index.js +86 -0
- package/rules/security/S021_referrer_policy/typescript/analyzer.js +183 -0
- package/rules/security/S023_no_json_injection/config.json +133 -44
- package/rules/security/S023_no_json_injection/dart/analyzer.js +7 -6
- package/rules/security/S023_no_json_injection/typescript/analyzer.js +402 -126
- package/rules/security/S023_no_json_injection/typescript/ast-analyzer.js +571 -154
- package/rules/security/S026_tls_all_connections/config.json +30 -0
- package/rules/security/S026_tls_all_connections/typescript/analyzer.js +339 -0
- package/rules/security/S027_mtls_certificate_validation/config.json +30 -0
- package/rules/security/S027_mtls_certificate_validation/typescript/analyzer.js +225 -0
- package/rules/security/S035_separate_app_hostnames/config.json +28 -0
- package/rules/security/S035_separate_app_hostnames/typescript/analyzer.js +186 -0
- package/rules/security/S036_lfi_rfi_protection/config.json +2 -2
- package/rules/security/S039_tls_certificate_validation/config.json +29 -0
- package/rules/security/S039_tls_certificate_validation/typescript/analyzer.js +229 -0
- package/rules/security/S046_jwt_algorithm_allowlist/config.json +28 -0
- package/rules/security/S046_jwt_algorithm_allowlist/dart/analyzer.js +44 -0
- package/rules/security/S046_jwt_algorithm_allowlist/index.js +87 -0
- package/rules/security/S046_jwt_algorithm_allowlist/typescript/analyzer.js +235 -0
- package/rules/security/S047_oauth_pkce_protection/config.json +31 -0
- package/rules/security/S047_oauth_pkce_protection/dart/analyzer.js +44 -0
- package/rules/security/S047_oauth_pkce_protection/index.js +86 -0
- package/rules/security/S047_oauth_pkce_protection/typescript/analyzer.js +78 -0
- package/rules/security/S048_oauth_redirect_uri_validation/config.json +30 -0
- package/rules/security/S048_oauth_redirect_uri_validation/typescript/analyzer.js +278 -0
- package/rules/security/S049_short_validity_tokens/typescript/config.json +10 -3
- package/rules/security/S050_reference_tokens_entropy/config.json +28 -0
- package/rules/security/S050_reference_tokens_entropy/dart/analyzer.js +45 -0
- package/rules/security/S050_reference_tokens_entropy/index.js +86 -0
- package/rules/security/S050_reference_tokens_entropy/typescript/analyzer.js +74 -0
- package/rules/security/S053_generic_error_messages/config.json +28 -0
- package/rules/security/S053_generic_error_messages/dart/analyzer.js +45 -0
- package/rules/security/S053_generic_error_messages/index.js +86 -0
- package/rules/security/S053_generic_error_messages/typescript/analyzer.js +80 -0
- package/rules/security/S055_content_type_validation/typescript/symbol-based-analyzer.js +64 -2
- package/rules/security/S059_disable_debug_mode/config.json +28 -0
- package/rules/security/S059_disable_debug_mode/dart/analyzer.js +45 -0
- package/rules/security/S059_disable_debug_mode/index.js +86 -0
- package/rules/security/S059_disable_debug_mode/typescript/analyzer.js +85 -0
- package/rules/security/S060_password_minimum_length/config.json +28 -0
- package/rules/security/S060_password_minimum_length/dart/analyzer.js +45 -0
- package/rules/security/S060_password_minimum_length/index.js +86 -0
- package/rules/security/S060_password_minimum_length/typescript/analyzer.js +78 -0
- package/rules/security/S026_json_schema_validation/config.json +0 -27
- package/rules/security/S026_json_schema_validation/typescript/analyzer.js +0 -251
- package/rules/security/S027_no_hardcoded_secrets/config.json +0 -29
- package/rules/security/S027_no_hardcoded_secrets/typescript/analyzer.js +0 -309
- package/rules/security/S027_no_hardcoded_secrets/typescript/categories.json +0 -153
- package/rules/security/S035_path_session_cookies/config.json +0 -99
- package/rules/security/S035_path_session_cookies/typescript/analyzer.js +0 -316
- package/rules/security/S035_path_session_cookies/typescript/regex-based-analyzer.js +0 -724
- package/rules/security/S035_path_session_cookies/typescript/symbol-based-analyzer.js +0 -373
- package/rules/security/S039_no_session_tokens_in_url/config.json +0 -92
- package/rules/security/S039_no_session_tokens_in_url/typescript/analyzer.js +0 -262
- package/rules/security/S039_no_session_tokens_in_url/typescript/regex-based-analyzer.js +0 -337
- package/rules/security/S039_no_session_tokens_in_url/typescript/symbol-based-analyzer.js +0 -443
- package/rules/security/S048_no_current_password_in_reset/config.json +0 -48
- package/rules/security/S048_no_current_password_in_reset/typescript/analyzer.js +0 -366
- /package/rules/security/{S026_json_schema_validation → S026_tls_all_connections}/dart/analyzer.js +0 -0
- /package/rules/security/{S026_json_schema_validation → S026_tls_all_connections}/index.js +0 -0
- /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/dart/analyzer.js +0 -0
- /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/index.js +0 -0
- /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/typescript/categorized-analyzer.js +0 -0
- /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/dart/analyzer.js +0 -0
- /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/index.js +0 -0
- /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/typescript/README.md +0 -0
- /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/dart/analyzer.js +0 -0
- /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/index.js +0 -0
- /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/typescript/README.md +0 -0
- /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/dart/analyzer.js +0 -0
- /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/index.js +0 -0
- /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/typescript/README.md +0 -0
|
@@ -1,11 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S023 AST Analyzer – Use output encoding when building dynamic JavaScript/JSON
|
|
3
|
+
*
|
|
4
|
+
* AST-based detection for:
|
|
5
|
+
* - eval() with user data
|
|
6
|
+
* - new Function() with user data
|
|
7
|
+
* - String concatenation to build JavaScript code
|
|
8
|
+
* - Template literals with unescaped user input in JS/HTML context
|
|
9
|
+
* - Inline event handlers with user data
|
|
10
|
+
* - JSON.stringify in HTML context without proper escaping
|
|
11
|
+
*/
|
|
12
|
+
|
|
1
13
|
const fs = require('fs');
|
|
2
14
|
const path = require('path');
|
|
3
15
|
|
|
4
16
|
class S023ASTAnalyzer {
|
|
5
17
|
constructor() {
|
|
6
18
|
this.ruleId = 'S023';
|
|
7
|
-
this.ruleName = '
|
|
8
|
-
this.description = 'AST-based detection of unsafe JSON
|
|
19
|
+
this.ruleName = 'Use output encoding when building dynamic JavaScript/JSON';
|
|
20
|
+
this.description = 'AST-based detection of unsafe dynamic JS/JSON building and injection vulnerabilities';
|
|
21
|
+
|
|
22
|
+
// User input source patterns
|
|
23
|
+
this.userInputSources = [
|
|
24
|
+
'localStorage', 'sessionStorage', 'location', 'URLSearchParams',
|
|
25
|
+
'req', 'request', 'document', 'window', 'fetch', 'axios',
|
|
26
|
+
'getElementById', 'querySelector', 'formData', 'event'
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// Safe encoding functions
|
|
30
|
+
this.safeEncoders = [
|
|
31
|
+
'encodeURIComponent', 'encodeURI', 'escape', 'escapeHtml',
|
|
32
|
+
'sanitize', 'DOMPurify', 'textContent', 'htmlEncode', 'jsEncode'
|
|
33
|
+
];
|
|
9
34
|
}
|
|
10
35
|
|
|
11
36
|
async analyze(files, language, options = {}) {
|
|
@@ -22,13 +47,12 @@ class S023ASTAnalyzer {
|
|
|
22
47
|
];
|
|
23
48
|
|
|
24
49
|
for (const filePath of files) {
|
|
25
|
-
// Skip test files
|
|
26
50
|
if (skipPatterns.some(pattern => pattern.test(filePath))) {
|
|
27
51
|
continue;
|
|
28
52
|
}
|
|
29
53
|
|
|
30
54
|
if (options.verbose) {
|
|
31
|
-
console.log(`🎯
|
|
55
|
+
console.log(`🎯 S023 AST analysis on ${path.basename(filePath)}`);
|
|
32
56
|
}
|
|
33
57
|
|
|
34
58
|
try {
|
|
@@ -36,7 +60,9 @@ class S023ASTAnalyzer {
|
|
|
36
60
|
const fileViolations = await this.analyzeFile(filePath, content, language, options);
|
|
37
61
|
violations.push(...fileViolations);
|
|
38
62
|
} catch (error) {
|
|
39
|
-
|
|
63
|
+
if (options.verbose) {
|
|
64
|
+
console.warn(`⚠️ S023 AST: Failed to analyze ${filePath}: ${error.message}`);
|
|
65
|
+
}
|
|
40
66
|
}
|
|
41
67
|
}
|
|
42
68
|
|
|
@@ -55,30 +81,27 @@ class S023ASTAnalyzer {
|
|
|
55
81
|
|
|
56
82
|
async analyzeJSTS(filePath, content, config) {
|
|
57
83
|
const violations = [];
|
|
58
|
-
|
|
84
|
+
|
|
59
85
|
try {
|
|
60
|
-
// Try AST analysis first (like ESLint approach)
|
|
61
86
|
const astViolations = await this.analyzeWithAST(filePath, content, config);
|
|
62
87
|
if (astViolations.length > 0) {
|
|
63
88
|
violations.push(...astViolations);
|
|
64
89
|
}
|
|
65
90
|
} catch (astError) {
|
|
66
91
|
if (config.verbose) {
|
|
67
|
-
console.log(`⚠️ AST
|
|
92
|
+
console.log(`⚠️ S023 AST: AST parsing failed for ${path.basename(filePath)}, falling back to regex`);
|
|
68
93
|
}
|
|
69
|
-
|
|
70
|
-
// Fallback to regex-based analysis
|
|
94
|
+
|
|
71
95
|
const regexViolations = await this.analyzeWithRegex(filePath, content, config);
|
|
72
96
|
violations.push(...regexViolations);
|
|
73
97
|
}
|
|
74
|
-
|
|
98
|
+
|
|
75
99
|
return violations;
|
|
76
100
|
}
|
|
77
101
|
|
|
78
102
|
async analyzeWithAST(filePath, content, config) {
|
|
79
103
|
const violations = [];
|
|
80
|
-
|
|
81
|
-
// Import AST modules dynamically
|
|
104
|
+
|
|
82
105
|
let astModules;
|
|
83
106
|
try {
|
|
84
107
|
astModules = require('../../../core/ast-modules');
|
|
@@ -86,7 +109,6 @@ class S023ASTAnalyzer {
|
|
|
86
109
|
throw new Error('AST modules not available');
|
|
87
110
|
}
|
|
88
111
|
|
|
89
|
-
// Try to parse with AST
|
|
90
112
|
let ast;
|
|
91
113
|
try {
|
|
92
114
|
ast = await astModules.parseCode(content, 'javascript', filePath);
|
|
@@ -97,31 +119,48 @@ class S023ASTAnalyzer {
|
|
|
97
119
|
throw new Error(`Parse error: ${parseError.message}`);
|
|
98
120
|
}
|
|
99
121
|
|
|
100
|
-
// Traverse AST to find JSON injection vulnerabilities - mimicking ESLint's approach
|
|
101
122
|
const rootNode = ast.program || ast;
|
|
102
123
|
this.traverseAST(rootNode, (node) => {
|
|
103
|
-
// Check
|
|
124
|
+
// 1. Check eval() calls
|
|
125
|
+
if (this.isEvalCall(node)) {
|
|
126
|
+
const violation = this.checkEvalForUnsafeUsage(node, filePath, content);
|
|
127
|
+
if (violation) violations.push(violation);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 2. Check new Function() calls
|
|
131
|
+
if (this.isNewFunctionCall(node)) {
|
|
132
|
+
const violation = this.checkNewFunctionForUnsafeUsage(node, filePath, content);
|
|
133
|
+
if (violation) violations.push(violation);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 3. Check JSON.parse() calls
|
|
104
137
|
if (this.isJsonParseCall(node)) {
|
|
105
138
|
const violation = this.checkJsonParseForUnsafeUsage(node, filePath, content);
|
|
106
|
-
if (violation)
|
|
107
|
-
violations.push(violation);
|
|
108
|
-
}
|
|
139
|
+
if (violation) violations.push(violation);
|
|
109
140
|
}
|
|
110
|
-
|
|
111
|
-
// Check
|
|
112
|
-
if (this.isEvalCall(node)) {
|
|
113
|
-
const violation = this.checkEvalForJsonUsage(node, filePath, content);
|
|
114
|
-
if (violation) {
|
|
115
|
-
violations.push(violation);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Check JSON.stringify in HTML context
|
|
141
|
+
|
|
142
|
+
// 4. Check JSON.stringify in HTML context
|
|
120
143
|
if (this.isJsonStringifyCall(node)) {
|
|
121
144
|
const violation = this.checkJsonStringifyInHtmlContext(node, filePath, content);
|
|
122
|
-
if (violation)
|
|
123
|
-
|
|
124
|
-
|
|
145
|
+
if (violation) violations.push(violation);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 5. Check template literals in dangerous contexts
|
|
149
|
+
if (this.isTemplateLiteral(node)) {
|
|
150
|
+
const violation = this.checkTemplateLiteralForUnsafeUsage(node, filePath, content);
|
|
151
|
+
if (violation) violations.push(violation);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 6. Check setAttribute for inline event handlers
|
|
155
|
+
if (this.isSetAttributeCall(node)) {
|
|
156
|
+
const violation = this.checkSetAttributeForEventHandler(node, filePath, content);
|
|
157
|
+
if (violation) violations.push(violation);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 7. Check assignment to innerHTML/outerHTML with dynamic content
|
|
161
|
+
if (this.isAssignmentToHtmlProperty(node)) {
|
|
162
|
+
const violation = this.checkHtmlPropertyAssignment(node, filePath, content);
|
|
163
|
+
if (violation) violations.push(violation);
|
|
125
164
|
}
|
|
126
165
|
});
|
|
127
166
|
|
|
@@ -130,12 +169,12 @@ class S023ASTAnalyzer {
|
|
|
130
169
|
|
|
131
170
|
traverseAST(node, callback) {
|
|
132
171
|
if (!node || typeof node !== 'object') return;
|
|
133
|
-
|
|
172
|
+
|
|
134
173
|
callback(node);
|
|
135
|
-
|
|
174
|
+
|
|
136
175
|
for (const key in node) {
|
|
137
176
|
if (key === 'parent' || key === 'leadingComments' || key === 'trailingComments') continue;
|
|
138
|
-
|
|
177
|
+
|
|
139
178
|
const child = node[key];
|
|
140
179
|
if (Array.isArray(child)) {
|
|
141
180
|
child.forEach(item => this.traverseAST(item, callback));
|
|
@@ -145,228 +184,606 @@ class S023ASTAnalyzer {
|
|
|
145
184
|
}
|
|
146
185
|
}
|
|
147
186
|
|
|
187
|
+
// Node type checkers
|
|
188
|
+
isEvalCall(node) {
|
|
189
|
+
return node.type === 'CallExpression' &&
|
|
190
|
+
node.callee &&
|
|
191
|
+
node.callee.type === 'Identifier' &&
|
|
192
|
+
node.callee.name === 'eval';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
isNewFunctionCall(node) {
|
|
196
|
+
return node.type === 'NewExpression' &&
|
|
197
|
+
node.callee &&
|
|
198
|
+
node.callee.type === 'Identifier' &&
|
|
199
|
+
node.callee.name === 'Function';
|
|
200
|
+
}
|
|
201
|
+
|
|
148
202
|
isJsonParseCall(node) {
|
|
149
203
|
return node.type === 'CallExpression' &&
|
|
150
|
-
node.callee &&
|
|
204
|
+
node.callee &&
|
|
151
205
|
node.callee.type === 'MemberExpression' &&
|
|
152
206
|
node.callee.object && node.callee.object.name === 'JSON' &&
|
|
153
207
|
node.callee.property && node.callee.property.name === 'parse';
|
|
154
208
|
}
|
|
155
209
|
|
|
156
|
-
isEvalCall(node) {
|
|
157
|
-
return node.type === 'CallExpression' &&
|
|
158
|
-
node.callee &&
|
|
159
|
-
node.callee.type === 'Identifier' &&
|
|
160
|
-
node.callee.name === 'eval';
|
|
161
|
-
}
|
|
162
|
-
|
|
163
210
|
isJsonStringifyCall(node) {
|
|
164
211
|
return node.type === 'CallExpression' &&
|
|
165
|
-
node.callee &&
|
|
212
|
+
node.callee &&
|
|
166
213
|
node.callee.type === 'MemberExpression' &&
|
|
167
214
|
node.callee.object && node.callee.object.name === 'JSON' &&
|
|
168
215
|
node.callee.property && node.callee.property.name === 'stringify';
|
|
169
216
|
}
|
|
170
217
|
|
|
218
|
+
isTemplateLiteral(node) {
|
|
219
|
+
return node.type === 'TemplateLiteral' &&
|
|
220
|
+
node.expressions &&
|
|
221
|
+
node.expressions.length > 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
isSetAttributeCall(node) {
|
|
225
|
+
return node.type === 'CallExpression' &&
|
|
226
|
+
node.callee &&
|
|
227
|
+
node.callee.type === 'MemberExpression' &&
|
|
228
|
+
node.callee.property &&
|
|
229
|
+
node.callee.property.name === 'setAttribute';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
isAssignmentToHtmlProperty(node) {
|
|
233
|
+
return node.type === 'AssignmentExpression' &&
|
|
234
|
+
node.left &&
|
|
235
|
+
node.left.type === 'MemberExpression' &&
|
|
236
|
+
node.left.property &&
|
|
237
|
+
(node.left.property.name === 'innerHTML' || node.left.property.name === 'outerHTML');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Violation checkers
|
|
241
|
+
checkEvalForUnsafeUsage(node, filePath, content) {
|
|
242
|
+
if (!node.arguments || node.arguments.length === 0) return null;
|
|
243
|
+
|
|
244
|
+
const lines = content.split('\n');
|
|
245
|
+
const lineNumber = node.loc.start.line;
|
|
246
|
+
const lineText = lines[lineNumber - 1] || '';
|
|
247
|
+
|
|
248
|
+
const argument = node.arguments[0];
|
|
249
|
+
|
|
250
|
+
// Skip legitimate dynamic import pattern: eval(`import('module-name')`)
|
|
251
|
+
if (this.isDynamicImportPattern(argument, content)) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (this.containsUserInputOrDynamic(argument, content)) {
|
|
256
|
+
return {
|
|
257
|
+
ruleId: this.ruleId,
|
|
258
|
+
file: filePath,
|
|
259
|
+
line: lineNumber,
|
|
260
|
+
column: node.loc.start.column + 1,
|
|
261
|
+
message: 'Never use eval() with user data - use JSON.parse() for JSON or safer alternatives',
|
|
262
|
+
severity: 'error',
|
|
263
|
+
code: lineText.trim(),
|
|
264
|
+
type: 'eval_with_user_data',
|
|
265
|
+
confidence: 0.95,
|
|
266
|
+
suggestion: 'Use JSON.parse() for JSON data or refactor to avoid dynamic code execution'
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Check if eval argument is a legitimate dynamic import pattern
|
|
275
|
+
* Pattern: eval(`import('module-name')`) - used for ESM imports in CommonJS
|
|
276
|
+
*/
|
|
277
|
+
isDynamicImportPattern(node, content) {
|
|
278
|
+
if (!node) return false;
|
|
279
|
+
|
|
280
|
+
// Check for template literal containing only import()
|
|
281
|
+
if (node.type === 'TemplateLiteral') {
|
|
282
|
+
const nodeText = this.getNodeText(node, content);
|
|
283
|
+
// Match: `import('...')` or `import("...")`
|
|
284
|
+
return /^`\s*import\s*\(\s*['"][^'"]+['"]\s*\)\s*`$/.test(nodeText);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Also check the text representation for regex fallback
|
|
288
|
+
const argText = this.getNodeText(node, content);
|
|
289
|
+
return /^`\s*import\s*\(\s*['"][^'"]+['"]\s*\)\s*`$/.test(argText);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
checkNewFunctionForUnsafeUsage(node, filePath, content) {
|
|
293
|
+
if (!node.arguments || node.arguments.length === 0) return null;
|
|
294
|
+
|
|
295
|
+
const lines = content.split('\n');
|
|
296
|
+
const lineNumber = node.loc.start.line;
|
|
297
|
+
const lineText = lines[lineNumber - 1] || '';
|
|
298
|
+
|
|
299
|
+
// Check if any argument contains user input or dynamic data
|
|
300
|
+
const hasUnsafeArg = node.arguments.some(arg =>
|
|
301
|
+
this.containsUserInputOrDynamic(arg, content)
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
if (hasUnsafeArg) {
|
|
305
|
+
return {
|
|
306
|
+
ruleId: this.ruleId,
|
|
307
|
+
file: filePath,
|
|
308
|
+
line: lineNumber,
|
|
309
|
+
column: node.loc.start.column + 1,
|
|
310
|
+
message: 'Never use new Function() with user data - this is equivalent to eval()',
|
|
311
|
+
severity: 'error',
|
|
312
|
+
code: lineText.trim(),
|
|
313
|
+
type: 'new_function_with_user_data',
|
|
314
|
+
confidence: 0.95,
|
|
315
|
+
suggestion: 'Refactor to avoid dynamic function creation with user input'
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
|
|
171
322
|
checkJsonParseForUnsafeUsage(node, filePath, content) {
|
|
172
323
|
if (!node.arguments || node.arguments.length === 0) return null;
|
|
173
|
-
|
|
324
|
+
|
|
174
325
|
const lines = content.split('\n');
|
|
175
326
|
const lineNumber = node.loc.start.line;
|
|
176
327
|
const lineText = lines[lineNumber - 1] || '';
|
|
177
|
-
|
|
178
|
-
// Check if the argument is from user input (similar to ESLint logic)
|
|
328
|
+
|
|
179
329
|
const argument = node.arguments[0];
|
|
180
330
|
if (this.isUserInputSource(argument, content)) {
|
|
181
|
-
// Check if there's validation before JSON.parse
|
|
182
331
|
if (!this.hasValidationBefore(node, content)) {
|
|
183
332
|
return {
|
|
184
333
|
ruleId: this.ruleId,
|
|
185
334
|
file: filePath,
|
|
186
335
|
line: lineNumber,
|
|
187
336
|
column: node.loc.start.column + 1,
|
|
188
|
-
message: '
|
|
337
|
+
message: 'Validate JSON structure before parsing user input',
|
|
189
338
|
severity: 'warning',
|
|
190
339
|
code: lineText.trim(),
|
|
191
340
|
type: 'unsafe_json_parse',
|
|
192
341
|
confidence: 0.8,
|
|
193
|
-
suggestion: '
|
|
342
|
+
suggestion: 'Wrap JSON.parse in try-catch and validate the parsed structure'
|
|
194
343
|
};
|
|
195
344
|
}
|
|
196
345
|
}
|
|
197
|
-
|
|
346
|
+
|
|
198
347
|
return null;
|
|
199
348
|
}
|
|
200
349
|
|
|
201
|
-
|
|
202
|
-
if (!node.arguments || node.arguments.length === 0) return null;
|
|
203
|
-
|
|
350
|
+
checkJsonStringifyInHtmlContext(node, filePath, content) {
|
|
204
351
|
const lines = content.split('\n');
|
|
205
352
|
const lineNumber = node.loc.start.line;
|
|
206
353
|
const lineText = lines[lineNumber - 1] || '';
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
354
|
+
|
|
355
|
+
if (this.isInHtmlContext(node, content)) {
|
|
356
|
+
const surroundingText = this.getSurroundingText(node, content, 3);
|
|
357
|
+
|
|
358
|
+
if (/<script|<\/script>/i.test(surroundingText)) {
|
|
359
|
+
const hasEscaping = /replace\s*\(\s*[/']<\\?\/script/i.test(surroundingText);
|
|
360
|
+
|
|
361
|
+
if (!hasEscaping) {
|
|
362
|
+
return {
|
|
363
|
+
ruleId: this.ruleId,
|
|
364
|
+
file: filePath,
|
|
365
|
+
line: lineNumber,
|
|
366
|
+
column: node.loc.start.column + 1,
|
|
367
|
+
message: 'JSON.stringify in HTML/script context must escape </script> sequences',
|
|
368
|
+
severity: 'warning',
|
|
369
|
+
code: lineText.trim(),
|
|
370
|
+
type: 'json_stringify_html_no_escape',
|
|
371
|
+
confidence: 0.8,
|
|
372
|
+
suggestion: "Escape </script> with .replace(/<\\/script/gi, '<\\\\/script')"
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
}
|
|
223
376
|
}
|
|
224
|
-
|
|
377
|
+
|
|
225
378
|
return null;
|
|
226
379
|
}
|
|
227
380
|
|
|
228
|
-
|
|
381
|
+
checkTemplateLiteralForUnsafeUsage(node, filePath, content) {
|
|
229
382
|
const lines = content.split('\n');
|
|
230
383
|
const lineNumber = node.loc.start.line;
|
|
231
384
|
const lineText = lines[lineNumber - 1] || '';
|
|
232
|
-
|
|
233
|
-
// Check if
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
385
|
+
|
|
386
|
+
// Check if template literal is in dangerous context
|
|
387
|
+
const surroundingText = this.getSurroundingText(node, content, 2);
|
|
388
|
+
|
|
389
|
+
const isDangerousContext =
|
|
390
|
+
/innerHTML\s*=/.test(surroundingText) ||
|
|
391
|
+
/outerHTML\s*=/.test(surroundingText) ||
|
|
392
|
+
/insertAdjacentHTML/.test(surroundingText) ||
|
|
393
|
+
/document\.write/.test(surroundingText);
|
|
394
|
+
|
|
395
|
+
if (isDangerousContext) {
|
|
396
|
+
// Check if any expression contains user input
|
|
397
|
+
const hasUserInputExpression = node.expressions.some(expr =>
|
|
398
|
+
this.containsUserInputOrDynamic(expr, content)
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
if (hasUserInputExpression && !this.hasSafeEncoding(lineText)) {
|
|
402
|
+
return {
|
|
403
|
+
ruleId: this.ruleId,
|
|
404
|
+
file: filePath,
|
|
405
|
+
line: lineNumber,
|
|
406
|
+
column: node.loc.start.column + 1,
|
|
407
|
+
message: 'Template literal with user input in dangerous context - escape data before insertion',
|
|
408
|
+
severity: 'warning',
|
|
409
|
+
code: lineText.trim(),
|
|
410
|
+
type: 'unsafe_template_literal',
|
|
411
|
+
confidence: 0.75,
|
|
412
|
+
suggestion: 'Use textContent for text, or properly escape data before inserting into HTML context'
|
|
413
|
+
};
|
|
414
|
+
}
|
|
247
415
|
}
|
|
248
|
-
|
|
416
|
+
|
|
249
417
|
return null;
|
|
250
418
|
}
|
|
251
419
|
|
|
252
|
-
|
|
420
|
+
checkSetAttributeForEventHandler(node, filePath, content) {
|
|
421
|
+
if (!node.arguments || node.arguments.length < 2) return null;
|
|
422
|
+
|
|
423
|
+
const lines = content.split('\n');
|
|
424
|
+
const lineNumber = node.loc.start.line;
|
|
425
|
+
const lineText = lines[lineNumber - 1] || '';
|
|
426
|
+
|
|
427
|
+
const firstArg = node.arguments[0];
|
|
428
|
+
const attrName = this.getStringValue(firstArg);
|
|
429
|
+
|
|
430
|
+
// Check if it's an event handler attribute
|
|
431
|
+
if (attrName && /^on\w+$/i.test(attrName)) {
|
|
432
|
+
const valueArg = node.arguments[1];
|
|
433
|
+
|
|
434
|
+
if (this.containsUserInputOrDynamic(valueArg, content)) {
|
|
435
|
+
return {
|
|
436
|
+
ruleId: this.ruleId,
|
|
437
|
+
file: filePath,
|
|
438
|
+
line: lineNumber,
|
|
439
|
+
column: node.loc.start.column + 1,
|
|
440
|
+
message: 'Avoid inline event handlers with user data - use addEventListener instead',
|
|
441
|
+
severity: 'warning',
|
|
442
|
+
code: lineText.trim(),
|
|
443
|
+
type: 'inline_event_handler_user_data',
|
|
444
|
+
confidence: 0.8,
|
|
445
|
+
suggestion: 'Use element.addEventListener() and pass data via data attributes or closures'
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
checkHtmlPropertyAssignment(node, filePath, content) {
|
|
454
|
+
const lines = content.split('\n');
|
|
455
|
+
const lineNumber = node.loc.start.line;
|
|
456
|
+
const lineText = lines[lineNumber - 1] || '';
|
|
457
|
+
|
|
458
|
+
const rightSide = node.right;
|
|
459
|
+
|
|
460
|
+
// Check if right side contains user input without encoding
|
|
461
|
+
if (this.containsUserInputOrDynamic(rightSide, content)) {
|
|
462
|
+
if (!this.hasSafeEncoding(lineText)) {
|
|
463
|
+
return {
|
|
464
|
+
ruleId: this.ruleId,
|
|
465
|
+
file: filePath,
|
|
466
|
+
line: lineNumber,
|
|
467
|
+
column: node.loc.start.column + 1,
|
|
468
|
+
message: 'Assignment to innerHTML/outerHTML with dynamic content - escape user input',
|
|
469
|
+
severity: 'warning',
|
|
470
|
+
code: lineText.trim(),
|
|
471
|
+
type: 'unsafe_html_assignment',
|
|
472
|
+
confidence: 0.75,
|
|
473
|
+
suggestion: 'Use textContent for text or properly escape/sanitize HTML content'
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Helper methods
|
|
482
|
+
containsUserInputOrDynamic(node, content) {
|
|
253
483
|
if (!node) return false;
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
484
|
+
|
|
485
|
+
const nodeText = this.getNodeText(node, content);
|
|
486
|
+
|
|
487
|
+
// Check for user input sources
|
|
488
|
+
const isUserInput = this.userInputSources.some(source =>
|
|
489
|
+
nodeText.includes(source)
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
// Check for dynamic patterns (concatenation, template literals)
|
|
493
|
+
const isDynamic =
|
|
494
|
+
node.type === 'BinaryExpression' ||
|
|
495
|
+
node.type === 'TemplateLiteral' ||
|
|
496
|
+
/\+/.test(nodeText) ||
|
|
497
|
+
/\$\{/.test(nodeText);
|
|
498
|
+
|
|
499
|
+
return isUserInput || isDynamic;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
isUserInputSource(node, content) {
|
|
503
|
+
const nodeText = this.getNodeText(node, content);
|
|
504
|
+
return this.userInputSources.some(source => nodeText.includes(source));
|
|
270
505
|
}
|
|
271
506
|
|
|
272
507
|
hasValidationBefore(node, content) {
|
|
273
|
-
// Simple check for validation patterns before JSON.parse
|
|
274
508
|
const lines = content.split('\n');
|
|
275
509
|
const lineNumber = node.loc.start.line;
|
|
276
|
-
|
|
510
|
+
|
|
277
511
|
// Check previous lines for validation patterns
|
|
278
512
|
for (let i = Math.max(0, lineNumber - 5); i < lineNumber - 1; i++) {
|
|
279
513
|
const line = lines[i] || '';
|
|
280
|
-
if (
|
|
514
|
+
if (/try\s*\{|catch\s*\(|if\s*\(|typeof\s+|validate|check|isValid/i.test(line)) {
|
|
281
515
|
return true;
|
|
282
516
|
}
|
|
283
517
|
}
|
|
284
|
-
|
|
518
|
+
|
|
285
519
|
return false;
|
|
286
520
|
}
|
|
287
521
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
/if\s*\(/,
|
|
293
|
-
/typeof\s+/,
|
|
294
|
-
/instanceof\s+/,
|
|
295
|
-
/\.length\s*>/,
|
|
296
|
-
/validate/i,
|
|
297
|
-
/check/i,
|
|
298
|
-
/isValid/i
|
|
299
|
-
];
|
|
300
|
-
|
|
301
|
-
return validationPatterns.some(pattern => pattern.test(line));
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
containsJsonPattern(node, content) {
|
|
305
|
-
const nodeText = this.getNodeText(node, content);
|
|
306
|
-
return /json|JSON|\{|\[/.test(nodeText);
|
|
522
|
+
hasSafeEncoding(text) {
|
|
523
|
+
return this.safeEncoders.some(encoder =>
|
|
524
|
+
text.toLowerCase().includes(encoder.toLowerCase())
|
|
525
|
+
);
|
|
307
526
|
}
|
|
308
527
|
|
|
309
528
|
isInHtmlContext(node, content) {
|
|
310
|
-
// Check if JSON.stringify is used in HTML context
|
|
311
|
-
const htmlPatterns = [
|
|
312
|
-
/innerHTML/,
|
|
313
|
-
/outerHTML/,
|
|
314
|
-
/insertAdjacentHTML/,
|
|
315
|
-
/document\.write/,
|
|
316
|
-
/\.html\(/
|
|
317
|
-
];
|
|
318
|
-
|
|
319
529
|
const surroundingText = this.getSurroundingText(node, content, 3);
|
|
320
|
-
return
|
|
530
|
+
return /innerHTML|outerHTML|insertAdjacentHTML|document\.write|<script|<\/script>/i.test(surroundingText);
|
|
321
531
|
}
|
|
322
532
|
|
|
323
533
|
getNodeText(node, content) {
|
|
324
534
|
if (!node || !node.loc) return '';
|
|
325
|
-
|
|
326
|
-
|
|
535
|
+
try {
|
|
536
|
+
const lines = content.split('\n');
|
|
537
|
+
const startLine = node.loc.start.line - 1;
|
|
538
|
+
const endLine = node.loc.end.line - 1;
|
|
539
|
+
|
|
540
|
+
if (startLine === endLine) {
|
|
541
|
+
return lines[startLine].substring(node.loc.start.column, node.loc.end.column);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return lines.slice(startLine, endLine + 1).join('\n');
|
|
545
|
+
} catch (e) {
|
|
546
|
+
return '';
|
|
547
|
+
}
|
|
327
548
|
}
|
|
328
549
|
|
|
329
550
|
getSurroundingText(node, content, radius = 2) {
|
|
330
551
|
if (!node || !node.loc) return '';
|
|
331
552
|
const lines = content.split('\n');
|
|
332
553
|
const lineNumber = node.loc.start.line;
|
|
333
|
-
|
|
554
|
+
|
|
334
555
|
const startLine = Math.max(0, lineNumber - radius - 1);
|
|
335
556
|
const endLine = Math.min(lines.length, lineNumber + radius);
|
|
336
|
-
|
|
557
|
+
|
|
337
558
|
return lines.slice(startLine, endLine).join('\n');
|
|
338
559
|
}
|
|
339
560
|
|
|
561
|
+
getStringValue(node) {
|
|
562
|
+
if (!node) return null;
|
|
563
|
+
if (node.type === 'Literal' && typeof node.value === 'string') {
|
|
564
|
+
return node.value;
|
|
565
|
+
}
|
|
566
|
+
if (node.type === 'StringLiteral') {
|
|
567
|
+
return node.value;
|
|
568
|
+
}
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
|
|
340
572
|
async analyzeWithRegex(filePath, content, config) {
|
|
341
|
-
// Fallback regex analysis for basic JSON.parse detection
|
|
342
573
|
const violations = [];
|
|
343
574
|
const lines = content.split('\n');
|
|
344
|
-
|
|
345
|
-
const jsonParsePattern = /JSON\.parse\s*\(\s*([^)]+)\)/g;
|
|
346
575
|
let match;
|
|
347
|
-
|
|
576
|
+
|
|
577
|
+
// User input patterns for detection
|
|
578
|
+
const userInputPattern = /localStorage|sessionStorage|location|req\.|request\.|\.value|getElementById|querySelector|URLSearchParams|formData|event\.data/i;
|
|
579
|
+
|
|
580
|
+
// 1. Check for eval with dynamic content
|
|
581
|
+
// Use a more flexible pattern that can capture multiline arguments
|
|
582
|
+
const evalPattern = /\beval\s*\(\s*(`[\s\S]*?`|[^)]+)\)/g;
|
|
583
|
+
|
|
584
|
+
// Pattern for legitimate dynamic imports: eval(`import('...')`) or eval(`import("...")`)
|
|
585
|
+
// Allows whitespace/newlines between tokens
|
|
586
|
+
const dynamicImportPattern = /^`\s*import\s*\(\s*['"][^'"]+['"]\s*\)\s*`$/s;
|
|
587
|
+
|
|
588
|
+
while ((match = evalPattern.exec(content)) !== null) {
|
|
589
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
590
|
+
const lineText = lines[line - 1] || '';
|
|
591
|
+
const argument = match[1].trim().replace(/\s+/g, ' '); // Normalize whitespace
|
|
592
|
+
|
|
593
|
+
// Skip legitimate dynamic import pattern: eval(`import('module-name')`)
|
|
594
|
+
// Also normalize argument for checking
|
|
595
|
+
const normalizedArg = argument.replace(/\s+/g, '');
|
|
596
|
+
if (/^`import\(['"][^'"]+['"]\)`$/.test(normalizedArg)) {
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (/\+|`|\$\{/.test(argument) || userInputPattern.test(argument)) {
|
|
601
|
+
violations.push({
|
|
602
|
+
ruleId: this.ruleId,
|
|
603
|
+
file: filePath,
|
|
604
|
+
line: line,
|
|
605
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
606
|
+
message: 'Never use eval() with user data - use JSON.parse() for JSON',
|
|
607
|
+
severity: 'error',
|
|
608
|
+
code: lineText.trim(),
|
|
609
|
+
type: 'eval_with_user_data_regex',
|
|
610
|
+
confidence: 0.8,
|
|
611
|
+
suggestion: 'Use JSON.parse() or refactor to avoid dynamic code execution'
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// 2. Check for new Function with dynamic content
|
|
617
|
+
const newFunctionPattern = /new\s+Function\s*\(\s*([^)]*)\)/g;
|
|
618
|
+
|
|
619
|
+
while ((match = newFunctionPattern.exec(content)) !== null) {
|
|
620
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
621
|
+
const lineText = lines[line - 1] || '';
|
|
622
|
+
const argument = match[1];
|
|
623
|
+
|
|
624
|
+
if (/\+|`|\$\{/.test(argument) || userInputPattern.test(argument)) {
|
|
625
|
+
violations.push({
|
|
626
|
+
ruleId: this.ruleId,
|
|
627
|
+
file: filePath,
|
|
628
|
+
line: line,
|
|
629
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
630
|
+
message: 'Never use new Function() with user data - equivalent to eval()',
|
|
631
|
+
severity: 'error',
|
|
632
|
+
code: lineText.trim(),
|
|
633
|
+
type: 'new_function_with_user_data_regex',
|
|
634
|
+
confidence: 0.8,
|
|
635
|
+
suggestion: 'Refactor to avoid dynamic function creation'
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// 3. Check for innerHTML/outerHTML with template literals or concatenation
|
|
641
|
+
const htmlAssignmentPattern = /\.(innerHTML|outerHTML)\s*=\s*(`[^`]*\$\{|[^;]*\+)/g;
|
|
642
|
+
|
|
643
|
+
while ((match = htmlAssignmentPattern.exec(content)) !== null) {
|
|
644
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
645
|
+
const lineText = lines[line - 1] || '';
|
|
646
|
+
|
|
647
|
+
// Check if there's encoding/sanitization
|
|
648
|
+
if (!this.hasSafeEncoding(lineText)) {
|
|
649
|
+
violations.push({
|
|
650
|
+
ruleId: this.ruleId,
|
|
651
|
+
file: filePath,
|
|
652
|
+
line: line,
|
|
653
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
654
|
+
message: 'Avoid innerHTML/outerHTML with dynamic content - use textContent or sanitize input',
|
|
655
|
+
severity: 'warning',
|
|
656
|
+
code: lineText.trim(),
|
|
657
|
+
type: 'unsafe_html_assignment_regex',
|
|
658
|
+
confidence: 0.75,
|
|
659
|
+
suggestion: 'Use textContent for text, or DOMPurify/escapeHtml for HTML content'
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// 4. Check for setAttribute with event handlers
|
|
665
|
+
const setAttributePattern = /setAttribute\s*\(\s*['"]on\w+['"]\s*,\s*([^)]+)\)/gi;
|
|
666
|
+
|
|
667
|
+
while ((match = setAttributePattern.exec(content)) !== null) {
|
|
668
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
669
|
+
const lineText = lines[line - 1] || '';
|
|
670
|
+
const value = match[1];
|
|
671
|
+
|
|
672
|
+
if (/\+|`|\$\{/.test(value) || userInputPattern.test(value)) {
|
|
673
|
+
violations.push({
|
|
674
|
+
ruleId: this.ruleId,
|
|
675
|
+
file: filePath,
|
|
676
|
+
line: line,
|
|
677
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
678
|
+
message: 'Avoid inline event handlers with user data - use addEventListener instead',
|
|
679
|
+
severity: 'warning',
|
|
680
|
+
code: lineText.trim(),
|
|
681
|
+
type: 'inline_event_handler_regex',
|
|
682
|
+
confidence: 0.8,
|
|
683
|
+
suggestion: 'Use element.addEventListener() and pass data via data attributes'
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// 5. Check for JSON.stringify in script tag context without escaping
|
|
689
|
+
const jsonStringifyInScriptPattern = /<script[^>]*>.*JSON\.stringify\s*\([^)]+\).*<\/script>/gi;
|
|
690
|
+
|
|
691
|
+
while ((match = jsonStringifyInScriptPattern.exec(content)) !== null) {
|
|
692
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
693
|
+
const lineText = lines[line - 1] || '';
|
|
694
|
+
const matchText = match[0];
|
|
695
|
+
|
|
696
|
+
// Check if </script is properly escaped
|
|
697
|
+
if (!(/replace\s*\(\s*[/']<\\?\/script/i.test(matchText))) {
|
|
698
|
+
violations.push({
|
|
699
|
+
ruleId: this.ruleId,
|
|
700
|
+
file: filePath,
|
|
701
|
+
line: line,
|
|
702
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
703
|
+
message: 'JSON.stringify in script context must escape </script> sequences',
|
|
704
|
+
severity: 'warning',
|
|
705
|
+
code: lineText.trim(),
|
|
706
|
+
type: 'json_stringify_script_no_escape_regex',
|
|
707
|
+
confidence: 0.8,
|
|
708
|
+
suggestion: "Escape </script> with .replace(/<\\/script/gi, '<\\\\/script')"
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// 6. Check for JSON.parse with user input without try-catch
|
|
714
|
+
const jsonParsePattern = /JSON\.parse\s*\(\s*([^)]+)\)/g;
|
|
715
|
+
|
|
348
716
|
while ((match = jsonParsePattern.exec(content)) !== null) {
|
|
349
717
|
const line = content.substring(0, match.index).split('\n').length;
|
|
350
718
|
const lineText = lines[line - 1] || '';
|
|
351
|
-
|
|
352
|
-
// Simple check for unsafe patterns
|
|
353
719
|
const argument = match[1];
|
|
354
|
-
|
|
720
|
+
|
|
721
|
+
if (userInputPattern.test(argument)) {
|
|
722
|
+
// Check if there's a try-catch around it
|
|
723
|
+
const contextStart = Math.max(0, match.index - 200);
|
|
724
|
+
const contextBefore = content.substring(contextStart, match.index);
|
|
725
|
+
|
|
726
|
+
if (!/try\s*\{[^}]*$/.test(contextBefore)) {
|
|
727
|
+
violations.push({
|
|
728
|
+
ruleId: this.ruleId,
|
|
729
|
+
file: filePath,
|
|
730
|
+
line: line,
|
|
731
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
732
|
+
message: 'Validate JSON structure before parsing user input',
|
|
733
|
+
severity: 'warning',
|
|
734
|
+
code: lineText.trim(),
|
|
735
|
+
type: 'unsafe_json_parse_regex',
|
|
736
|
+
confidence: 0.7,
|
|
737
|
+
suggestion: 'Wrap JSON.parse in try-catch and validate the parsed structure'
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// 7. Check for document.write with dynamic content
|
|
744
|
+
const documentWritePattern = /document\.write(ln)?\s*\(\s*(`[^`]*\$\{|[^)]*\+)/g;
|
|
745
|
+
|
|
746
|
+
while ((match = documentWritePattern.exec(content)) !== null) {
|
|
747
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
748
|
+
const lineText = lines[line - 1] || '';
|
|
749
|
+
|
|
750
|
+
violations.push({
|
|
751
|
+
ruleId: this.ruleId,
|
|
752
|
+
file: filePath,
|
|
753
|
+
line: line,
|
|
754
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
755
|
+
message: 'Avoid document.write with dynamic content - use DOM methods instead',
|
|
756
|
+
severity: 'warning',
|
|
757
|
+
code: lineText.trim(),
|
|
758
|
+
type: 'unsafe_document_write_regex',
|
|
759
|
+
confidence: 0.75,
|
|
760
|
+
suggestion: 'Use createElement and textContent for safer DOM manipulation'
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// 8. Check for insertAdjacentHTML with dynamic content
|
|
765
|
+
const insertAdjacentPattern = /insertAdjacentHTML\s*\(\s*['"][^'"]+['"]\s*,\s*(`[^`]*\$\{|[^)]*\+)/g;
|
|
766
|
+
|
|
767
|
+
while ((match = insertAdjacentPattern.exec(content)) !== null) {
|
|
768
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
769
|
+
const lineText = lines[line - 1] || '';
|
|
770
|
+
|
|
771
|
+
if (!this.hasSafeEncoding(lineText)) {
|
|
355
772
|
violations.push({
|
|
356
773
|
ruleId: this.ruleId,
|
|
357
774
|
file: filePath,
|
|
358
775
|
line: line,
|
|
359
776
|
column: match.index - content.lastIndexOf('\n', match.index),
|
|
360
|
-
message: '
|
|
777
|
+
message: 'insertAdjacentHTML with dynamic content - sanitize input first',
|
|
361
778
|
severity: 'warning',
|
|
362
779
|
code: lineText.trim(),
|
|
363
|
-
type: '
|
|
364
|
-
confidence: 0.
|
|
365
|
-
suggestion: '
|
|
780
|
+
type: 'unsafe_insert_adjacent_regex',
|
|
781
|
+
confidence: 0.75,
|
|
782
|
+
suggestion: 'Use DOMPurify or escape user input before inserting HTML'
|
|
366
783
|
});
|
|
367
784
|
}
|
|
368
785
|
}
|
|
369
|
-
|
|
786
|
+
|
|
370
787
|
return violations;
|
|
371
788
|
}
|
|
372
789
|
}
|