@sun-asterisk/sunlint 1.3.36 → 1.3.37
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 +33 -0
- package/config/rules/enhanced-rules-registry.json +354 -98
- package/config/rules/rules-registry-generated.json +197 -171
- package/core/architecture-integration.js +115 -17
- package/core/cli-action-handler.js +101 -27
- package/core/cli-program.js +5 -0
- package/core/github-annotate-service.js +62 -0
- package/core/impact-integration.js +31 -16
- package/core/init-command.js +227 -0
- package/core/output-service.js +53 -5
- package/core/summary-report-service.js +46 -0
- package/core/unified-rule-registry.js +2 -1
- package/engines/eslint-engine.js +6 -0
- 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/security-en.md +470 -282
- package/package.json +1 -1
- 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,16 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* S023 – Use output encoding when building dynamic JavaScript/JSON
|
|
3
|
+
*
|
|
4
|
+
* Purpose: Prevent JavaScript and JSON injection by applying proper output encoding
|
|
5
|
+
* when dynamically building JavaScript content or JSON data.
|
|
6
|
+
*
|
|
7
|
+
* Detects:
|
|
8
|
+
* - eval() with user data
|
|
9
|
+
* - new Function() with user data
|
|
10
|
+
* - String concatenation to build JavaScript code
|
|
11
|
+
* - Template literals with unescaped user input in JS context
|
|
12
|
+
* - Inline event handlers with user data
|
|
13
|
+
* - JSON.stringify in HTML context without proper escaping
|
|
5
14
|
*/
|
|
15
|
+
// Command: node cli.js --rule=S023 --input=examples/rule-test-fixtures/rules/S023_no_json_injection --engine=heuristic
|
|
6
16
|
|
|
7
17
|
class S023Analyzer {
|
|
8
18
|
constructor() {
|
|
9
|
-
this.ruleId =
|
|
10
|
-
this.ruleName =
|
|
11
|
-
this.description =
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
this.ruleId = "S023";
|
|
20
|
+
this.ruleName = "Use output encoding when building dynamic JavaScript/JSON";
|
|
21
|
+
this.description =
|
|
22
|
+
"Prevent JavaScript and JSON injection by applying proper output encoding";
|
|
23
|
+
|
|
24
|
+
// User input source patterns
|
|
14
25
|
this.userInputPatterns = [
|
|
15
26
|
/localStorage\.getItem/,
|
|
16
27
|
/sessionStorage\.getItem/,
|
|
@@ -24,10 +35,27 @@ class S023Analyzer {
|
|
|
24
35
|
/postMessage/,
|
|
25
36
|
/fetch\(/,
|
|
26
37
|
/axios\./,
|
|
27
|
-
/
|
|
38
|
+
/getElementById/,
|
|
39
|
+
/querySelector/,
|
|
40
|
+
/formData/i,
|
|
41
|
+
/event\.data/,
|
|
42
|
+
/\.value\b/,
|
|
28
43
|
];
|
|
29
|
-
|
|
30
|
-
//
|
|
44
|
+
|
|
45
|
+
// Safe encoding functions that indicate proper handling
|
|
46
|
+
this.safeEncodingPatterns = [
|
|
47
|
+
/encodeURIComponent/,
|
|
48
|
+
/encodeURI/,
|
|
49
|
+
/escapeHtml/i,
|
|
50
|
+
/sanitize/i,
|
|
51
|
+
/DOMPurify/,
|
|
52
|
+
/textContent/,
|
|
53
|
+
/htmlEncode/i,
|
|
54
|
+
/jsEncode/i,
|
|
55
|
+
/xss\(/i,
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
// Validation patterns indicating proper error handling
|
|
31
59
|
this.validationPatterns = [
|
|
32
60
|
/try\s*\{/,
|
|
33
61
|
/catch\s*\(/,
|
|
@@ -40,66 +68,70 @@ class S023Analyzer {
|
|
|
40
68
|
/isValid/i,
|
|
41
69
|
/sanitize/i,
|
|
42
70
|
/escape/i,
|
|
43
|
-
/filter/i
|
|
71
|
+
/filter/i,
|
|
44
72
|
];
|
|
45
|
-
|
|
73
|
+
|
|
46
74
|
// HTML context patterns
|
|
47
75
|
this.htmlContextPatterns = [
|
|
48
76
|
/innerHTML/,
|
|
49
77
|
/outerHTML/,
|
|
50
78
|
/insertAdjacentHTML/,
|
|
51
79
|
/document\.write/,
|
|
80
|
+
/document\.writeln/,
|
|
52
81
|
/\.html\(/,
|
|
53
82
|
/<script/i,
|
|
54
|
-
/<\/script>/i
|
|
83
|
+
/<\/script>/i,
|
|
84
|
+
/dangerouslySetInnerHTML/,
|
|
55
85
|
];
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
this.
|
|
59
|
-
/
|
|
60
|
-
|
|
61
|
-
/\[.*\]/,
|
|
62
|
-
/parse/i,
|
|
63
|
-
/stringify/i
|
|
86
|
+
|
|
87
|
+
// Inline event handler patterns
|
|
88
|
+
this.inlineEventPatterns = [
|
|
89
|
+
/on\w+\s*=\s*["']/,
|
|
90
|
+
/setAttribute\s*\(\s*["']on\w+/,
|
|
64
91
|
];
|
|
65
92
|
}
|
|
66
93
|
|
|
67
94
|
async analyze(files, language, options = {}) {
|
|
68
95
|
const violations = [];
|
|
69
|
-
|
|
96
|
+
|
|
70
97
|
for (const filePath of files) {
|
|
71
98
|
try {
|
|
72
|
-
const fileViolations = await this.analyzeFile(
|
|
99
|
+
const fileViolations = await this.analyzeFile(
|
|
100
|
+
filePath,
|
|
101
|
+
language,
|
|
102
|
+
options,
|
|
103
|
+
);
|
|
73
104
|
violations.push(...fileViolations);
|
|
74
105
|
} catch (error) {
|
|
75
106
|
if (options.verbose) {
|
|
76
|
-
console.warn(
|
|
107
|
+
console.warn(
|
|
108
|
+
`⚠️ S023: Failed to analyze ${filePath}: ${error.message}`,
|
|
109
|
+
);
|
|
77
110
|
}
|
|
78
111
|
}
|
|
79
112
|
}
|
|
80
|
-
|
|
113
|
+
|
|
81
114
|
return violations;
|
|
82
115
|
}
|
|
83
116
|
|
|
84
117
|
async analyzeFile(filePath, language, options = {}) {
|
|
85
|
-
// Skip test files
|
|
118
|
+
// Skip test files
|
|
86
119
|
const skipPatterns = [
|
|
87
120
|
/\.spec\.(ts|tsx|js|jsx)$/,
|
|
88
121
|
/\.test\.(ts|tsx|js|jsx)$/,
|
|
89
|
-
/__tests__\//,
|
|
90
|
-
/__mocks__\//,
|
|
91
|
-
/\/tests?\//,
|
|
92
|
-
/\/fixtures?\//,
|
|
122
|
+
/__tests__\//,
|
|
123
|
+
/__mocks__\//,
|
|
124
|
+
/\/tests?\//,
|
|
125
|
+
/\/fixtures?\//,
|
|
93
126
|
];
|
|
94
127
|
|
|
95
|
-
|
|
96
|
-
if (shouldSkip) {
|
|
128
|
+
if (skipPatterns.some((pattern) => pattern.test(filePath))) {
|
|
97
129
|
return [];
|
|
98
130
|
}
|
|
99
131
|
|
|
100
132
|
switch (language) {
|
|
101
|
-
case
|
|
102
|
-
case
|
|
133
|
+
case "typescript":
|
|
134
|
+
case "javascript":
|
|
103
135
|
return this.analyzeJavaScript(filePath, options);
|
|
104
136
|
default:
|
|
105
137
|
return [];
|
|
@@ -109,183 +141,427 @@ class S023Analyzer {
|
|
|
109
141
|
async analyzeJavaScript(filePath, options = {}) {
|
|
110
142
|
try {
|
|
111
143
|
// Try AST analysis first (preferred method)
|
|
112
|
-
const astAnalyzer = require(
|
|
113
|
-
const astViolations = await astAnalyzer.analyze(
|
|
144
|
+
const astAnalyzer = require("./ast-analyzer.js");
|
|
145
|
+
const astViolations = await astAnalyzer.analyze(
|
|
146
|
+
[filePath],
|
|
147
|
+
"javascript",
|
|
148
|
+
options,
|
|
149
|
+
);
|
|
114
150
|
if (astViolations.length > 0) {
|
|
115
151
|
return astViolations;
|
|
116
152
|
}
|
|
117
153
|
} catch (astError) {
|
|
118
154
|
if (options.verbose) {
|
|
119
|
-
console.log(
|
|
155
|
+
console.log(
|
|
156
|
+
`⚠️ S023: AST analysis failed for ${filePath}, falling back to regex`,
|
|
157
|
+
);
|
|
120
158
|
}
|
|
121
159
|
}
|
|
122
|
-
|
|
160
|
+
|
|
123
161
|
// Fallback to regex analysis
|
|
124
162
|
return this.analyzeWithRegex(filePath, options);
|
|
125
163
|
}
|
|
126
164
|
|
|
127
165
|
async analyzeWithRegex(filePath, options = {}) {
|
|
128
|
-
const fs = require(
|
|
129
|
-
|
|
130
|
-
|
|
166
|
+
const fs = require("fs");
|
|
167
|
+
|
|
131
168
|
try {
|
|
132
|
-
const content = fs.readFileSync(filePath,
|
|
169
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
133
170
|
const violations = [];
|
|
134
|
-
const lines = content.split(
|
|
135
|
-
|
|
136
|
-
// 1. Check
|
|
137
|
-
const
|
|
138
|
-
violations.push(...jsonParseViolations);
|
|
139
|
-
|
|
140
|
-
// 2. Check eval() with JSON patterns
|
|
141
|
-
const evalViolations = this.checkEvalWithJson(content, lines, filePath);
|
|
171
|
+
const lines = content.split("\n");
|
|
172
|
+
|
|
173
|
+
// 1. Check eval() calls with user data
|
|
174
|
+
const evalViolations = this.checkEvalCalls(content, lines, filePath);
|
|
142
175
|
violations.push(...evalViolations);
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
176
|
+
|
|
177
|
+
// 2. Check new Function() calls with user data
|
|
178
|
+
const newFunctionViolations = this.checkNewFunctionCalls(
|
|
179
|
+
content,
|
|
180
|
+
lines,
|
|
181
|
+
filePath,
|
|
182
|
+
);
|
|
183
|
+
violations.push(...newFunctionViolations);
|
|
184
|
+
|
|
185
|
+
// 3. Check string concatenation for JS building
|
|
186
|
+
const concatViolations = this.checkStringConcatenationJS(
|
|
187
|
+
content,
|
|
188
|
+
lines,
|
|
189
|
+
filePath,
|
|
190
|
+
);
|
|
191
|
+
violations.push(...concatViolations);
|
|
192
|
+
|
|
193
|
+
// 4. Check template literals with user input in JS/HTML context
|
|
194
|
+
const templateViolations = this.checkTemplateLiterals(
|
|
195
|
+
content,
|
|
196
|
+
lines,
|
|
197
|
+
filePath,
|
|
198
|
+
);
|
|
199
|
+
violations.push(...templateViolations);
|
|
200
|
+
|
|
201
|
+
// 5. Check inline event handlers with user data
|
|
202
|
+
const eventViolations = this.checkInlineEventHandlers(
|
|
203
|
+
content,
|
|
204
|
+
lines,
|
|
205
|
+
filePath,
|
|
206
|
+
);
|
|
207
|
+
violations.push(...eventViolations);
|
|
208
|
+
|
|
209
|
+
// 6. Check JSON.stringify in HTML context
|
|
210
|
+
const jsonHtmlViolations = this.checkJsonStringifyInHtml(
|
|
211
|
+
content,
|
|
212
|
+
lines,
|
|
213
|
+
filePath,
|
|
214
|
+
);
|
|
215
|
+
violations.push(...jsonHtmlViolations);
|
|
216
|
+
|
|
217
|
+
// 7. Check JSON.parse with user input (existing check)
|
|
218
|
+
const jsonParseViolations = this.checkJsonParseCalls(
|
|
219
|
+
content,
|
|
220
|
+
lines,
|
|
221
|
+
filePath,
|
|
222
|
+
);
|
|
223
|
+
violations.push(...jsonParseViolations);
|
|
224
|
+
|
|
148
225
|
return violations;
|
|
149
226
|
} catch (error) {
|
|
150
227
|
if (options.verbose) {
|
|
151
|
-
console.warn(
|
|
228
|
+
console.warn(
|
|
229
|
+
`⚠️ S023: Failed to read file ${filePath}: ${error.message}`,
|
|
230
|
+
);
|
|
152
231
|
}
|
|
153
232
|
return [];
|
|
154
233
|
}
|
|
155
234
|
}
|
|
156
235
|
|
|
157
|
-
|
|
236
|
+
checkEvalCalls(content, lines, filePath) {
|
|
158
237
|
const violations = [];
|
|
159
|
-
const
|
|
238
|
+
const evalPattern = /\beval\s*\(\s*([^)]+)\)/g;
|
|
160
239
|
let match;
|
|
161
|
-
|
|
162
|
-
while ((match =
|
|
163
|
-
const lineNumber = content.substring(0, match.index).split(
|
|
164
|
-
const lineText = lines[lineNumber - 1] ||
|
|
240
|
+
|
|
241
|
+
while ((match = evalPattern.exec(content)) !== null) {
|
|
242
|
+
const lineNumber = content.substring(0, match.index).split("\n").length;
|
|
243
|
+
const lineText = lines[lineNumber - 1] || "";
|
|
165
244
|
const argument = match[1].trim();
|
|
166
|
-
|
|
167
|
-
// Check if argument
|
|
168
|
-
if (this.
|
|
169
|
-
|
|
170
|
-
|
|
245
|
+
|
|
246
|
+
// Check if argument contains user input or dynamic data
|
|
247
|
+
if (this.containsUserInput(argument) || this.isDynamicData(argument)) {
|
|
248
|
+
violations.push({
|
|
249
|
+
ruleId: this.ruleId,
|
|
250
|
+
file: filePath,
|
|
251
|
+
line: lineNumber,
|
|
252
|
+
column: match.index - content.lastIndexOf("\n", match.index),
|
|
253
|
+
message:
|
|
254
|
+
"Never use eval() with user data - use JSON.parse() for JSON or safer alternatives",
|
|
255
|
+
severity: "error",
|
|
256
|
+
code: lineText.trim(),
|
|
257
|
+
type: "eval_with_user_data",
|
|
258
|
+
confidence: 0.95,
|
|
259
|
+
suggestion:
|
|
260
|
+
"Use JSON.parse() for JSON data or refactor to avoid dynamic code execution",
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return violations;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
checkNewFunctionCalls(content, lines, filePath) {
|
|
269
|
+
const violations = [];
|
|
270
|
+
const newFunctionPattern = /new\s+Function\s*\(\s*([^)]*)\)/g;
|
|
271
|
+
let match;
|
|
272
|
+
|
|
273
|
+
while ((match = newFunctionPattern.exec(content)) !== null) {
|
|
274
|
+
const lineNumber = content.substring(0, match.index).split("\n").length;
|
|
275
|
+
const lineText = lines[lineNumber - 1] || "";
|
|
276
|
+
const argument = match[1].trim();
|
|
277
|
+
|
|
278
|
+
// Check if argument contains user input or dynamic data
|
|
279
|
+
if (this.containsUserInput(argument) || this.isDynamicData(argument)) {
|
|
280
|
+
violations.push({
|
|
281
|
+
ruleId: this.ruleId,
|
|
282
|
+
file: filePath,
|
|
283
|
+
line: lineNumber,
|
|
284
|
+
column: match.index - content.lastIndexOf("\n", match.index),
|
|
285
|
+
message:
|
|
286
|
+
"Never use new Function() with user data - this is equivalent to eval()",
|
|
287
|
+
severity: "error",
|
|
288
|
+
code: lineText.trim(),
|
|
289
|
+
type: "new_function_with_user_data",
|
|
290
|
+
confidence: 0.95,
|
|
291
|
+
suggestion:
|
|
292
|
+
"Refactor to avoid dynamic function creation with user input",
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return violations;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
checkStringConcatenationJS(content, lines, filePath) {
|
|
301
|
+
const violations = [];
|
|
302
|
+
|
|
303
|
+
// Pattern: building JS code via string concatenation
|
|
304
|
+
// e.g., var code = 'var x = "' + userInput + '"';
|
|
305
|
+
const jsBuildPatterns = [
|
|
306
|
+
/(var|let|const)\s+\w+\s*=\s*['"](?:var|let|const|function|return)\s+.*['"]\s*\+/g,
|
|
307
|
+
/['"](?:var|let|const|function|return)\s+.*\+.*['"]/g,
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
for (const pattern of jsBuildPatterns) {
|
|
311
|
+
let match;
|
|
312
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
313
|
+
const lineNumber = content.substring(0, match.index).split("\n").length;
|
|
314
|
+
const lineText = lines[lineNumber - 1] || "";
|
|
315
|
+
|
|
316
|
+
// Check if this is in HTML context or involves user input
|
|
317
|
+
if (
|
|
318
|
+
this.isInHtmlContext(content, match.index) ||
|
|
319
|
+
this.lineContainsUserInput(lineText)
|
|
320
|
+
) {
|
|
171
321
|
violations.push({
|
|
172
322
|
ruleId: this.ruleId,
|
|
173
323
|
file: filePath,
|
|
174
324
|
line: lineNumber,
|
|
175
|
-
column: match.index - content.lastIndexOf(
|
|
176
|
-
message:
|
|
177
|
-
|
|
325
|
+
column: match.index - content.lastIndexOf("\n", match.index),
|
|
326
|
+
message:
|
|
327
|
+
"Avoid string concatenation to build JavaScript code - encode data properly",
|
|
328
|
+
severity: "warning",
|
|
178
329
|
code: lineText.trim(),
|
|
179
|
-
type:
|
|
180
|
-
confidence: 0.
|
|
181
|
-
suggestion:
|
|
330
|
+
type: "string_concatenation_js",
|
|
331
|
+
confidence: 0.7,
|
|
332
|
+
suggestion:
|
|
333
|
+
"Use proper encoding or templating that auto-escapes user input",
|
|
182
334
|
});
|
|
183
335
|
}
|
|
184
336
|
}
|
|
185
337
|
}
|
|
186
|
-
|
|
338
|
+
|
|
187
339
|
return violations;
|
|
188
340
|
}
|
|
189
341
|
|
|
190
|
-
|
|
342
|
+
checkTemplateLiterals(content, lines, filePath) {
|
|
191
343
|
const violations = [];
|
|
192
|
-
|
|
344
|
+
|
|
345
|
+
// Pattern: template literals with ${} in HTML/JS context
|
|
346
|
+
const templatePattern = /`[^`]*\$\{[^}]+\}[^`]*`/g;
|
|
193
347
|
let match;
|
|
194
|
-
|
|
195
|
-
while ((match =
|
|
196
|
-
const lineNumber = content.substring(0, match.index).split(
|
|
197
|
-
const lineText = lines[lineNumber - 1] ||
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
// Check if
|
|
201
|
-
|
|
348
|
+
|
|
349
|
+
while ((match = templatePattern.exec(content)) !== null) {
|
|
350
|
+
const lineNumber = content.substring(0, match.index).split("\n").length;
|
|
351
|
+
const lineText = lines[lineNumber - 1] || "";
|
|
352
|
+
const templateContent = match[0];
|
|
353
|
+
|
|
354
|
+
// Check if template is used in dangerous context (innerHTML, onclick, etc.)
|
|
355
|
+
const contextBefore = content.substring(
|
|
356
|
+
Math.max(0, match.index - 50),
|
|
357
|
+
match.index,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const isDangerousContext =
|
|
361
|
+
/innerHTML\s*=/.test(contextBefore) ||
|
|
362
|
+
/outerHTML\s*=/.test(contextBefore) ||
|
|
363
|
+
/insertAdjacentHTML/.test(contextBefore) ||
|
|
364
|
+
/on\w+\s*=/.test(templateContent) ||
|
|
365
|
+
/javascript:/i.test(templateContent);
|
|
366
|
+
|
|
367
|
+
if (isDangerousContext && !this.hasEncodingFunction(lineText)) {
|
|
202
368
|
violations.push({
|
|
203
369
|
ruleId: this.ruleId,
|
|
204
370
|
file: filePath,
|
|
205
371
|
line: lineNumber,
|
|
206
|
-
column: match.index - content.lastIndexOf(
|
|
207
|
-
message:
|
|
208
|
-
|
|
372
|
+
column: match.index - content.lastIndexOf("\n", match.index),
|
|
373
|
+
message:
|
|
374
|
+
"Template literal with user input in dangerous context - escape data before insertion",
|
|
375
|
+
severity: "warning",
|
|
209
376
|
code: lineText.trim(),
|
|
210
|
-
type:
|
|
211
|
-
confidence: 0.
|
|
212
|
-
suggestion:
|
|
377
|
+
type: "unsafe_template_literal",
|
|
378
|
+
confidence: 0.75,
|
|
379
|
+
suggestion:
|
|
380
|
+
"Use textContent for text, or properly escape data before inserting into HTML/JS context",
|
|
213
381
|
});
|
|
214
382
|
}
|
|
215
383
|
}
|
|
216
|
-
|
|
384
|
+
|
|
217
385
|
return violations;
|
|
218
386
|
}
|
|
219
387
|
|
|
220
|
-
|
|
388
|
+
checkInlineEventHandlers(content, lines, filePath) {
|
|
221
389
|
const violations = [];
|
|
222
|
-
|
|
390
|
+
|
|
391
|
+
// Pattern: setAttribute('onclick', ...) with dynamic data
|
|
392
|
+
const setAttributePattern =
|
|
393
|
+
/setAttribute\s*\(\s*['"]on\w+['"]\s*,\s*([^)]+)\)/g;
|
|
223
394
|
let match;
|
|
224
|
-
|
|
225
|
-
while ((match =
|
|
226
|
-
const lineNumber = content.substring(0, match.index).split(
|
|
227
|
-
const lineText = lines[lineNumber - 1] ||
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if
|
|
395
|
+
|
|
396
|
+
while ((match = setAttributePattern.exec(content)) !== null) {
|
|
397
|
+
const lineNumber = content.substring(0, match.index).split("\n").length;
|
|
398
|
+
const lineText = lines[lineNumber - 1] || "";
|
|
399
|
+
const argument = match[1].trim();
|
|
400
|
+
|
|
401
|
+
// Check if the value contains user input or concatenation
|
|
402
|
+
if (
|
|
403
|
+
this.containsUserInput(argument) ||
|
|
404
|
+
/\+/.test(argument) ||
|
|
405
|
+
/\$\{/.test(argument)
|
|
406
|
+
) {
|
|
231
407
|
violations.push({
|
|
232
408
|
ruleId: this.ruleId,
|
|
233
409
|
file: filePath,
|
|
234
410
|
line: lineNumber,
|
|
235
|
-
column: match.index - content.lastIndexOf(
|
|
236
|
-
message:
|
|
237
|
-
|
|
411
|
+
column: match.index - content.lastIndexOf("\n", match.index),
|
|
412
|
+
message:
|
|
413
|
+
"Avoid inline event handlers with user data - use addEventListener instead",
|
|
414
|
+
severity: "warning",
|
|
238
415
|
code: lineText.trim(),
|
|
239
|
-
type:
|
|
240
|
-
confidence: 0.
|
|
241
|
-
suggestion:
|
|
416
|
+
type: "inline_event_handler_user_data",
|
|
417
|
+
confidence: 0.8,
|
|
418
|
+
suggestion:
|
|
419
|
+
"Use element.addEventListener() and pass data via data attributes or closures",
|
|
242
420
|
});
|
|
243
421
|
}
|
|
244
422
|
}
|
|
245
|
-
|
|
423
|
+
|
|
424
|
+
return violations;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
checkJsonStringifyInHtml(content, lines, filePath) {
|
|
428
|
+
const violations = [];
|
|
429
|
+
const jsonStringifyPattern = /JSON\.stringify\s*\([^)]+\)/g;
|
|
430
|
+
let match;
|
|
431
|
+
|
|
432
|
+
while ((match = jsonStringifyPattern.exec(content)) !== null) {
|
|
433
|
+
const lineNumber = content.substring(0, match.index).split("\n").length;
|
|
434
|
+
const lineText = lines[lineNumber - 1] || "";
|
|
435
|
+
|
|
436
|
+
// Check if JSON.stringify is used in HTML context
|
|
437
|
+
if (this.isInHtmlContext(content, match.index)) {
|
|
438
|
+
// Check if </script> is properly escaped
|
|
439
|
+
const surroundingContext = content.substring(
|
|
440
|
+
Math.max(0, match.index - 100),
|
|
441
|
+
Math.min(content.length, match.index + match[0].length + 100),
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
const hasScriptTag = /<script|<\/script>/i.test(surroundingContext);
|
|
445
|
+
const hasEscaping = /replace\s*\(\s*[/']<\\?\/script/i.test(
|
|
446
|
+
surroundingContext,
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
if (hasScriptTag && !hasEscaping) {
|
|
450
|
+
violations.push({
|
|
451
|
+
ruleId: this.ruleId,
|
|
452
|
+
file: filePath,
|
|
453
|
+
line: lineNumber,
|
|
454
|
+
column: match.index - content.lastIndexOf("\n", match.index),
|
|
455
|
+
message:
|
|
456
|
+
"JSON.stringify in HTML/script context must escape </script> sequences",
|
|
457
|
+
severity: "warning",
|
|
458
|
+
code: lineText.trim(),
|
|
459
|
+
type: "json_stringify_html_no_escape",
|
|
460
|
+
confidence: 0.8,
|
|
461
|
+
suggestion:
|
|
462
|
+
"Escape </script> with .replace(/<\\/script/gi, '<\\\\/script')",
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return violations;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
checkJsonParseCalls(content, lines, filePath) {
|
|
472
|
+
const violations = [];
|
|
473
|
+
const jsonParsePattern = /JSON\.parse\s*\(\s*([^)]+)\)/g;
|
|
474
|
+
let match;
|
|
475
|
+
|
|
476
|
+
while ((match = jsonParsePattern.exec(content)) !== null) {
|
|
477
|
+
const lineNumber = content.substring(0, match.index).split("\n").length;
|
|
478
|
+
const lineText = lines[lineNumber - 1] || "";
|
|
479
|
+
const argument = match[1].trim();
|
|
480
|
+
|
|
481
|
+
// Check if argument is from user input
|
|
482
|
+
if (this.isUserInputArgument(argument)) {
|
|
483
|
+
// Check if there's validation around this call
|
|
484
|
+
if (
|
|
485
|
+
!this.hasValidationContext(content, match.index, lineNumber, lines)
|
|
486
|
+
) {
|
|
487
|
+
violations.push({
|
|
488
|
+
ruleId: this.ruleId,
|
|
489
|
+
file: filePath,
|
|
490
|
+
line: lineNumber,
|
|
491
|
+
column: match.index - content.lastIndexOf("\n", match.index),
|
|
492
|
+
message: "Validate JSON structure before parsing user input",
|
|
493
|
+
severity: "warning",
|
|
494
|
+
code: lineText.trim(),
|
|
495
|
+
type: "unsafe_json_parse",
|
|
496
|
+
confidence: 0.8,
|
|
497
|
+
suggestion:
|
|
498
|
+
"Wrap JSON.parse in try-catch and validate the parsed structure",
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
246
504
|
return violations;
|
|
247
505
|
}
|
|
248
506
|
|
|
507
|
+
// Helper methods
|
|
508
|
+
containsUserInput(text) {
|
|
509
|
+
return this.userInputPatterns.some((pattern) => pattern.test(text));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
isDynamicData(text) {
|
|
513
|
+
// Check for concatenation or template literals
|
|
514
|
+
return /\+/.test(text) || /\$\{/.test(text) || /`/.test(text);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
lineContainsUserInput(line) {
|
|
518
|
+
return this.containsUserInput(line);
|
|
519
|
+
}
|
|
520
|
+
|
|
249
521
|
isUserInputArgument(argument) {
|
|
250
|
-
return this.userInputPatterns.some(pattern => pattern.test(argument));
|
|
522
|
+
return this.userInputPatterns.some((pattern) => pattern.test(argument));
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
hasEncodingFunction(text) {
|
|
526
|
+
return this.safeEncodingPatterns.some((pattern) => pattern.test(text));
|
|
251
527
|
}
|
|
252
528
|
|
|
253
529
|
hasValidationContext(content, matchIndex, lineNumber, lines) {
|
|
254
530
|
// Check surrounding lines for validation patterns
|
|
255
531
|
const startLine = Math.max(0, lineNumber - 3);
|
|
256
532
|
const endLine = Math.min(lines.length, lineNumber + 2);
|
|
257
|
-
|
|
533
|
+
|
|
258
534
|
for (let i = startLine; i < endLine; i++) {
|
|
259
|
-
const line = lines[i] ||
|
|
260
|
-
if (this.validationPatterns.some(pattern => pattern.test(line))) {
|
|
535
|
+
const line = lines[i] || "";
|
|
536
|
+
if (this.validationPatterns.some((pattern) => pattern.test(line))) {
|
|
261
537
|
return true;
|
|
262
538
|
}
|
|
263
539
|
}
|
|
264
|
-
|
|
540
|
+
|
|
265
541
|
// Check if the call is inside a try block
|
|
266
|
-
const beforeText = content.substring(
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
542
|
+
const beforeText = content.substring(
|
|
543
|
+
Math.max(0, matchIndex - 200),
|
|
544
|
+
matchIndex,
|
|
545
|
+
);
|
|
546
|
+
const afterText = content.substring(
|
|
547
|
+
matchIndex,
|
|
548
|
+
Math.min(content.length, matchIndex + 100),
|
|
549
|
+
);
|
|
271
550
|
|
|
272
|
-
|
|
273
|
-
return this.jsonPatterns.some(pattern => pattern.test(text));
|
|
551
|
+
return /try\s*\{[^}]*$/.test(beforeText) || /catch\s*\(/.test(afterText);
|
|
274
552
|
}
|
|
275
553
|
|
|
276
554
|
isInHtmlContext(content, matchIndex) {
|
|
277
|
-
// Check surrounding context for HTML patterns
|
|
278
555
|
const contextStart = Math.max(0, matchIndex - 100);
|
|
279
556
|
const contextEnd = Math.min(content.length, matchIndex + 100);
|
|
280
557
|
const context = content.substring(contextStart, contextEnd);
|
|
281
|
-
|
|
282
|
-
return this.htmlContextPatterns.some(pattern => pattern.test(context));
|
|
558
|
+
|
|
559
|
+
return this.htmlContextPatterns.some((pattern) => pattern.test(context));
|
|
283
560
|
}
|
|
284
561
|
|
|
285
|
-
// Utility method for file extension checking
|
|
286
562
|
isSupportedFile(filePath) {
|
|
287
|
-
const supportedExtensions = [
|
|
288
|
-
const path = require(
|
|
563
|
+
const supportedExtensions = [".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs"];
|
|
564
|
+
const path = require("path");
|
|
289
565
|
return supportedExtensions.includes(path.extname(filePath));
|
|
290
566
|
}
|
|
291
567
|
}
|