@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.
Files changed (103) hide show
  1. package/cli.js +33 -0
  2. package/config/rules/enhanced-rules-registry.json +354 -98
  3. package/config/rules/rules-registry-generated.json +197 -171
  4. package/core/architecture-integration.js +115 -17
  5. package/core/cli-action-handler.js +101 -27
  6. package/core/cli-program.js +5 -0
  7. package/core/github-annotate-service.js +62 -0
  8. package/core/impact-integration.js +31 -16
  9. package/core/init-command.js +227 -0
  10. package/core/output-service.js +53 -5
  11. package/core/summary-report-service.js +46 -0
  12. package/core/unified-rule-registry.js +2 -1
  13. package/engines/eslint-engine.js +6 -0
  14. package/engines/impact/core/detectors/database-detector.js +1 -1
  15. package/engines/impact/core/detectors/endpoint-detector.js +1 -1
  16. package/engines/impact/core/report-generator.js +235 -73
  17. package/origin-rules/security-en.md +470 -282
  18. package/package.json +1 -1
  19. package/rules/security/S001_backend_auth_communications/dart/analyzer.js +44 -0
  20. package/rules/security/S001_backend_auth_communications/index.js +87 -0
  21. package/rules/security/S001_backend_auth_communications/typescript/analyzer.js +164 -0
  22. package/rules/security/S002_os_command_injection/dart/analyzer.js +44 -0
  23. package/rules/security/S002_os_command_injection/index.js +87 -0
  24. package/rules/security/S002_os_command_injection/typescript/analyzer.js +194 -0
  25. package/rules/security/S008_svg_content_validation/dart/analyzer.js +44 -0
  26. package/rules/security/S008_svg_content_validation/index.js +87 -0
  27. package/rules/security/S008_svg_content_validation/typescript/analyzer.js +216 -0
  28. package/rules/security/S018_no_sensitive_browser_storage/dart/analyzer.js +44 -0
  29. package/rules/security/S018_no_sensitive_browser_storage/index.js +86 -0
  30. package/rules/security/S018_no_sensitive_browser_storage/typescript/analyzer.js +193 -0
  31. package/rules/security/S021_referrer_policy/dart/analyzer.js +44 -0
  32. package/rules/security/S021_referrer_policy/index.js +86 -0
  33. package/rules/security/S021_referrer_policy/typescript/analyzer.js +183 -0
  34. package/rules/security/S023_no_json_injection/config.json +133 -44
  35. package/rules/security/S023_no_json_injection/dart/analyzer.js +7 -6
  36. package/rules/security/S023_no_json_injection/typescript/analyzer.js +402 -126
  37. package/rules/security/S023_no_json_injection/typescript/ast-analyzer.js +571 -154
  38. package/rules/security/S026_tls_all_connections/config.json +30 -0
  39. package/rules/security/S026_tls_all_connections/typescript/analyzer.js +339 -0
  40. package/rules/security/S027_mtls_certificate_validation/config.json +30 -0
  41. package/rules/security/S027_mtls_certificate_validation/typescript/analyzer.js +225 -0
  42. package/rules/security/S035_separate_app_hostnames/config.json +28 -0
  43. package/rules/security/S035_separate_app_hostnames/typescript/analyzer.js +186 -0
  44. package/rules/security/S036_lfi_rfi_protection/config.json +2 -2
  45. package/rules/security/S039_tls_certificate_validation/config.json +29 -0
  46. package/rules/security/S039_tls_certificate_validation/typescript/analyzer.js +229 -0
  47. package/rules/security/S046_jwt_algorithm_allowlist/config.json +28 -0
  48. package/rules/security/S046_jwt_algorithm_allowlist/dart/analyzer.js +44 -0
  49. package/rules/security/S046_jwt_algorithm_allowlist/index.js +87 -0
  50. package/rules/security/S046_jwt_algorithm_allowlist/typescript/analyzer.js +235 -0
  51. package/rules/security/S047_oauth_pkce_protection/config.json +31 -0
  52. package/rules/security/S047_oauth_pkce_protection/dart/analyzer.js +44 -0
  53. package/rules/security/S047_oauth_pkce_protection/index.js +86 -0
  54. package/rules/security/S047_oauth_pkce_protection/typescript/analyzer.js +78 -0
  55. package/rules/security/S048_oauth_redirect_uri_validation/config.json +30 -0
  56. package/rules/security/S048_oauth_redirect_uri_validation/typescript/analyzer.js +278 -0
  57. package/rules/security/S049_short_validity_tokens/typescript/config.json +10 -3
  58. package/rules/security/S050_reference_tokens_entropy/config.json +28 -0
  59. package/rules/security/S050_reference_tokens_entropy/dart/analyzer.js +45 -0
  60. package/rules/security/S050_reference_tokens_entropy/index.js +86 -0
  61. package/rules/security/S050_reference_tokens_entropy/typescript/analyzer.js +74 -0
  62. package/rules/security/S053_generic_error_messages/config.json +28 -0
  63. package/rules/security/S053_generic_error_messages/dart/analyzer.js +45 -0
  64. package/rules/security/S053_generic_error_messages/index.js +86 -0
  65. package/rules/security/S053_generic_error_messages/typescript/analyzer.js +80 -0
  66. package/rules/security/S055_content_type_validation/typescript/symbol-based-analyzer.js +64 -2
  67. package/rules/security/S059_disable_debug_mode/config.json +28 -0
  68. package/rules/security/S059_disable_debug_mode/dart/analyzer.js +45 -0
  69. package/rules/security/S059_disable_debug_mode/index.js +86 -0
  70. package/rules/security/S059_disable_debug_mode/typescript/analyzer.js +85 -0
  71. package/rules/security/S060_password_minimum_length/config.json +28 -0
  72. package/rules/security/S060_password_minimum_length/dart/analyzer.js +45 -0
  73. package/rules/security/S060_password_minimum_length/index.js +86 -0
  74. package/rules/security/S060_password_minimum_length/typescript/analyzer.js +78 -0
  75. package/rules/security/S026_json_schema_validation/config.json +0 -27
  76. package/rules/security/S026_json_schema_validation/typescript/analyzer.js +0 -251
  77. package/rules/security/S027_no_hardcoded_secrets/config.json +0 -29
  78. package/rules/security/S027_no_hardcoded_secrets/typescript/analyzer.js +0 -309
  79. package/rules/security/S027_no_hardcoded_secrets/typescript/categories.json +0 -153
  80. package/rules/security/S035_path_session_cookies/config.json +0 -99
  81. package/rules/security/S035_path_session_cookies/typescript/analyzer.js +0 -316
  82. package/rules/security/S035_path_session_cookies/typescript/regex-based-analyzer.js +0 -724
  83. package/rules/security/S035_path_session_cookies/typescript/symbol-based-analyzer.js +0 -373
  84. package/rules/security/S039_no_session_tokens_in_url/config.json +0 -92
  85. package/rules/security/S039_no_session_tokens_in_url/typescript/analyzer.js +0 -262
  86. package/rules/security/S039_no_session_tokens_in_url/typescript/regex-based-analyzer.js +0 -337
  87. package/rules/security/S039_no_session_tokens_in_url/typescript/symbol-based-analyzer.js +0 -443
  88. package/rules/security/S048_no_current_password_in_reset/config.json +0 -48
  89. package/rules/security/S048_no_current_password_in_reset/typescript/analyzer.js +0 -366
  90. /package/rules/security/{S026_json_schema_validation → S026_tls_all_connections}/dart/analyzer.js +0 -0
  91. /package/rules/security/{S026_json_schema_validation → S026_tls_all_connections}/index.js +0 -0
  92. /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/dart/analyzer.js +0 -0
  93. /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/index.js +0 -0
  94. /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/typescript/categorized-analyzer.js +0 -0
  95. /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/dart/analyzer.js +0 -0
  96. /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/index.js +0 -0
  97. /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/typescript/README.md +0 -0
  98. /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/dart/analyzer.js +0 -0
  99. /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/index.js +0 -0
  100. /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/typescript/README.md +0 -0
  101. /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/dart/analyzer.js +0 -0
  102. /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/index.js +0 -0
  103. /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
- * Heuristic analyzer for: S023 No JSON Injection
3
- * Purpose: Prevent JSON injection attacks and unsafe JSON handling
4
- * Detects: unsafe JSON.parse(), eval() with JSON, JSON.stringify in HTML context
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 = 'S023';
10
- this.ruleName = 'No JSON Injection';
11
- this.description = 'Prevent JSON injection attacks and unsafe JSON handling';
12
-
13
- // Patterns that indicate user input sources
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
- /xhr\./
38
+ /getElementById/,
39
+ /querySelector/,
40
+ /formData/i,
41
+ /event\.data/,
42
+ /\.value\b/,
28
43
  ];
29
-
30
- // Patterns that indicate validation/safety measures
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
- // JSON patterns for eval detection
58
- this.jsonPatterns = [
59
- /json/i,
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(filePath, language, options);
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(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
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 - they often use JSON.parse for testing purposes
118
+ // Skip test files
86
119
  const skipPatterns = [
87
120
  /\.spec\.(ts|tsx|js|jsx)$/,
88
121
  /\.test\.(ts|tsx|js|jsx)$/,
89
- /__tests__\//, // Test directories
90
- /__mocks__\//, // Mock directories
91
- /\/tests?\//, // Test directories
92
- /\/fixtures?\//, // Fixture directories
122
+ /__tests__\//,
123
+ /__mocks__\//,
124
+ /\/tests?\//,
125
+ /\/fixtures?\//,
93
126
  ];
94
127
 
95
- const shouldSkip = skipPatterns.some((pattern) => pattern.test(filePath));
96
- if (shouldSkip) {
128
+ if (skipPatterns.some((pattern) => pattern.test(filePath))) {
97
129
  return [];
98
130
  }
99
131
 
100
132
  switch (language) {
101
- case 'typescript':
102
- case 'javascript':
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('./ast-analyzer.js');
113
- const astViolations = await astAnalyzer.analyze([filePath], 'javascript', options);
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(`⚠️ AST analysis failed for ${filePath}, falling back to regex`);
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('fs');
129
- const path = require('path');
130
-
166
+ const fs = require("fs");
167
+
131
168
  try {
132
- const content = fs.readFileSync(filePath, 'utf8');
169
+ const content = fs.readFileSync(filePath, "utf8");
133
170
  const violations = [];
134
- const lines = content.split('\n');
135
-
136
- // 1. Check JSON.parse() calls
137
- const jsonParseViolations = this.checkJsonParseCalls(content, lines, filePath);
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
- // 3. Check JSON.stringify in HTML context
145
- const htmlViolations = this.checkJsonStringifyInHtml(content, lines, filePath);
146
- violations.push(...htmlViolations);
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(`⚠️ Failed to read file ${filePath}: ${error.message}`);
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
- checkJsonParseCalls(content, lines, filePath) {
236
+ checkEvalCalls(content, lines, filePath) {
158
237
  const violations = [];
159
- const jsonParsePattern = /JSON\.parse\s*\(\s*([^)]+)\)/g;
238
+ const evalPattern = /\beval\s*\(\s*([^)]+)\)/g;
160
239
  let match;
161
-
162
- while ((match = jsonParsePattern.exec(content)) !== null) {
163
- const lineNumber = content.substring(0, match.index).split('\n').length;
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 is from user input
168
- if (this.isUserInputArgument(argument)) {
169
- // Check if there's validation around this call
170
- if (!this.hasValidationContext(content, match.index, lineNumber, lines)) {
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('\n', match.index),
176
- message: 'Unsafe JSON parsing - validate input before parsing',
177
- severity: 'warning',
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: 'unsafe_json_parse',
180
- confidence: 0.8,
181
- suggestion: 'Validate input before parsing JSON or use try-catch block'
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
- checkEvalWithJson(content, lines, filePath) {
342
+ checkTemplateLiterals(content, lines, filePath) {
191
343
  const violations = [];
192
- const evalPattern = /eval\s*\(\s*([^)]+)\)/g;
344
+
345
+ // Pattern: template literals with ${} in HTML/JS context
346
+ const templatePattern = /`[^`]*\$\{[^}]+\}[^`]*`/g;
193
347
  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
- // Check if eval contains JSON patterns
201
- if (this.containsJsonPattern(argument)) {
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('\n', match.index),
207
- message: 'Never use eval() to process JSON data - use JSON.parse() instead',
208
- severity: 'error',
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: 'eval_json',
211
- confidence: 0.9,
212
- suggestion: 'Use JSON.parse() instead of eval() for parsing JSON'
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
- checkJsonStringifyInHtml(content, lines, filePath) {
388
+ checkInlineEventHandlers(content, lines, filePath) {
221
389
  const violations = [];
222
- const jsonStringifyPattern = /JSON\.stringify\s*\([^)]+\)/g;
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 = jsonStringifyPattern.exec(content)) !== null) {
226
- const lineNumber = content.substring(0, match.index).split('\n').length;
227
- const lineText = lines[lineNumber - 1] || '';
228
-
229
- // Check if JSON.stringify is used in HTML context
230
- if (this.isInHtmlContext(content, match.index)) {
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('\n', match.index),
236
- message: 'JSON.stringify output should be escaped when used in HTML context',
237
- severity: 'warning',
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: 'json_stringify_html',
240
- confidence: 0.7,
241
- suggestion: 'Escape JSON.stringify output when inserting into HTML'
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(Math.max(0, matchIndex - 200), matchIndex);
267
- const afterText = content.substring(matchIndex, Math.min(content.length, matchIndex + 100));
268
-
269
- return /try\s*\{[^}]*$/.test(beforeText) || /catch\s*\(/.test(afterText);
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
- containsJsonPattern(text) {
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 = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'];
288
- const path = require('path');
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
  }