@sun-asterisk/sunlint 1.3.26 → 1.3.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/config/rules/enhanced-rules-registry.json +101 -17
  2. package/config/rules/rules-registry-generated.json +22 -22
  3. package/origin-rules/security-en.md +351 -338
  4. package/package.json +1 -1
  5. package/rules/common/C003_no_vague_abbreviations/analyzer.js +73 -21
  6. package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +206 -2
  7. package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +553 -58
  8. package/rules/common/C029_catch_block_logging/analyzer.js +47 -12
  9. package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +35 -15
  10. package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +9 -5
  11. package/rules/security/S003_open_redirect_protection/README.md +371 -0
  12. package/rules/security/S003_open_redirect_protection/analyzer.js +135 -0
  13. package/rules/security/S003_open_redirect_protection/config.json +58 -0
  14. package/rules/security/S003_open_redirect_protection/symbol-based-analyzer.js +884 -0
  15. package/rules/security/S004_sensitive_data_logging/analyzer.js +135 -0
  16. package/rules/security/S004_sensitive_data_logging/config.json +62 -0
  17. package/rules/security/S004_sensitive_data_logging/symbol-based-analyzer.js +592 -0
  18. package/rules/security/S005_no_origin_auth/analyzer.js +97 -148
  19. package/rules/security/S005_no_origin_auth/config.json +28 -67
  20. package/rules/security/S005_no_origin_auth/symbol-based-analyzer.js +708 -0
  21. package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +170 -31
  22. package/rules/security/S010_no_insecure_encryption/analyzer.js +8 -2
  23. package/rules/security/S012_hardcoded_secrets/analyzer.js +149 -0
  24. package/rules/security/S012_hardcoded_secrets/config.json +75 -0
  25. package/rules/security/S012_hardcoded_secrets/symbol-based-analyzer.js +1204 -0
  26. package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +87 -0
  27. package/rules/security/S017_use_parameterized_queries/analyzer.js +11 -78
  28. package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +1146 -1
  29. package/rules/security/S019_smtp_injection_protection/analyzer.js +120 -0
  30. package/rules/security/S019_smtp_injection_protection/config.json +35 -0
  31. package/rules/security/S019_smtp_injection_protection/symbol-based-analyzer.js +687 -0
  32. package/rules/security/S020_no_eval_dynamic_code/analyzer.js +55 -130
  33. package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +4 -19
  34. package/rules/security/S022_escape_output_context/README.md +254 -0
  35. package/rules/security/S022_escape_output_context/analyzer.js +510 -0
  36. package/rules/security/S022_escape_output_context/config.json +229 -0
  37. package/rules/security/S023_no_json_injection/analyzer.js +15 -0
  38. package/rules/security/S023_no_json_injection/ast-analyzer.js +18 -3
  39. package/rules/security/S023_no_json_injection/config.json +133 -0
  40. package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +41 -0
  41. package/rules/security/S027_no_hardcoded_secrets/analyzer.js +67 -8
  42. package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +29 -6
  43. package/rules/security/S029_csrf_protection/config.json +127 -0
  44. package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +160 -28
  45. package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +81 -19
  46. package/rules/security/S031_secure_session_cookies/analyzer.js +20 -2
  47. package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +100 -0
  48. package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +8 -1
  49. package/rules/security/S032_httponly_session_cookies/analyzer.js +2 -2
  50. package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +115 -0
  51. package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +39 -10
  52. package/rules/security/S036_lfi_rfi_protection/analyzer.js +224 -0
  53. package/rules/security/S036_lfi_rfi_protection/config.json +20 -0
  54. package/rules/security/S040_session_fixation_protection/analyzer.js +153 -0
  55. package/rules/security/S040_session_fixation_protection/config.json +20 -0
  56. package/rules/security/S042_require_re_authentication_for_long_lived/README.md +83 -0
  57. package/rules/security/S042_require_re_authentication_for_long_lived/analyzer.js +153 -0
  58. package/rules/security/S042_require_re_authentication_for_long_lived/config.json +41 -0
  59. package/rules/security/S042_require_re_authentication_for_long_lived/symbol-based-analyzer.js +1139 -0
  60. package/rules/security/S043_password_changes_invalidate_all_sessions/README.md +107 -0
  61. package/rules/security/S043_password_changes_invalidate_all_sessions/analyzer.js +153 -0
  62. package/rules/security/S043_password_changes_invalidate_all_sessions/config.json +41 -0
  63. package/rules/security/S043_password_changes_invalidate_all_sessions/symbol-based-analyzer.js +541 -0
  64. package/docs/COMMAND-EXAMPLES.md +0 -390
  65. package/docs/FILE_LIMITS_COMPLETION_REPORT.md +0 -151
  66. package/docs/FOLDER_STRUCTURE.md +0 -59
  67. package/docs/SIMPLIFIED_USAGE_GUIDE.md +0 -208
  68. package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +0 -541
  69. package/rules/security/S020_no_eval_dynamic_code/regex-based-analyzer.js +0 -307
@@ -6,7 +6,6 @@
6
6
  */
7
7
 
8
8
  const S020SymbolBasedAnalyzer = require("./symbol-based-analyzer.js");
9
- const S020RegexBasedAnalyzer = require("./regex-based-analyzer.js");
10
9
 
11
10
  class S020Analyzer {
12
11
  constructor(options = {}) {
@@ -26,14 +25,6 @@ class S020Analyzer {
26
25
  this.semanticEngine = options.semanticEngine || null;
27
26
  this.verbose = options.verbose || false;
28
27
 
29
- this.config = {
30
- useSymbolBased: true,
31
- fallbackToRegex: true,
32
- regexBasedOnly: false,
33
- prioritizeSymbolic: true, // Prefer symbol-based when available
34
- fallbackToSymbol: true, // Allow symbol analysis even without semantic engine
35
- };
36
-
37
28
  try {
38
29
  this.symbolAnalyzer = new S020SymbolBasedAnalyzer(this.semanticEngine);
39
30
  if (process.env.SUNLINT_DEBUG)
@@ -41,14 +32,6 @@ class S020Analyzer {
41
32
  } catch (error) {
42
33
  console.error(`🔧 [S020] Error creating symbol analyzer:`, error);
43
34
  }
44
-
45
- try {
46
- this.regexAnalyzer = new S020RegexBasedAnalyzer(this.semanticEngine);
47
- if (process.env.SUNLINT_DEBUG)
48
- console.log(`🔧 [S020] Regex analyzer created successfully`);
49
- } catch (error) {
50
- console.error(`🔧 [S020] Error creating regex analyzer:`, error);
51
- }
52
35
  }
53
36
 
54
37
  async initialize(semanticEngine) {
@@ -58,9 +41,6 @@ class S020Analyzer {
58
41
 
59
42
  if (this.symbolAnalyzer)
60
43
  await this.symbolAnalyzer.initialize?.(semanticEngine);
61
- if (this.regexAnalyzer)
62
- await this.regexAnalyzer.initialize?.(semanticEngine);
63
- if (this.regexAnalyzer) this.regexAnalyzer.cleanup?.();
64
44
 
65
45
  if (process.env.SUNLINT_DEBUG)
66
46
  console.log(`🔧 [S020] Main analyzer initialized successfully`);
@@ -108,138 +88,84 @@ class S020Analyzer {
108
88
  console.log(`🔍 [S020] analyzeFile() called for: ${filePath}`);
109
89
  const violationMap = new Map();
110
90
 
111
- // Try symbol-based analysis first when semantic engine is available OR when explicitly enabled
112
- if (process.env.SUNLINT_DEBUG) {
113
- console.log(
114
- `🔧 [S020] Symbol check: useSymbolBased=${
115
- this.config.useSymbolBased
116
- }, semanticEngine=${!!this.semanticEngine}, project=${!!this
117
- .semanticEngine?.project}, initialized=${!!this.semanticEngine
118
- ?.initialized}`
119
- );
120
- }
91
+ try {
92
+ if (process.env.SUNLINT_DEBUG)
93
+ console.log(`🔧 [S020] Running symbol-based analysis...`);
121
94
 
122
- const canUseSymbol =
123
- this.config.useSymbolBased &&
124
- ((this.semanticEngine?.project && this.semanticEngine?.initialized) ||
125
- (!this.semanticEngine && this.config.fallbackToSymbol !== false));
95
+ let sourceFile = null;
96
+ if (this.semanticEngine?.project) {
97
+ sourceFile = this.semanticEngine.project.getSourceFile(filePath);
98
+ if (process.env.SUNLINT_DEBUG) {
99
+ console.log(
100
+ `🔧 [S020] Checked existing semantic engine project: sourceFile=${!!sourceFile}`
101
+ );
102
+ }
103
+ }
126
104
 
127
- if (canUseSymbol) {
128
- try {
105
+ if (!sourceFile) {
106
+ // Create a minimal ts-morph project for this analysis
129
107
  if (process.env.SUNLINT_DEBUG)
130
- console.log(`🔧 [S020] Trying symbol-based analysis...`);
131
-
132
- let sourceFile = null;
133
- if (this.semanticEngine?.project) {
134
- sourceFile = this.semanticEngine.project.getSourceFile(filePath);
135
- if (process.env.SUNLINT_DEBUG) {
136
- console.log(
137
- `🔧 [S020] Checked existing semantic engine project: sourceFile=${!!sourceFile}`
138
- );
108
+ console.log(
109
+ `🔧 [S020] Creating temporary ts-morph project for: ${filePath}`
110
+ );
111
+ try {
112
+ const fs = require("fs");
113
+ const path = require("path");
114
+ const { Project } = require("ts-morph");
115
+
116
+ // Check if file exists and read content
117
+ if (!fs.existsSync(filePath)) {
118
+ throw new Error(`File not found: ${filePath}`);
139
119
  }
140
- }
141
120
 
142
- if (!sourceFile) {
143
- // Create a minimal ts-morph project for this analysis
144
- if (process.env.SUNLINT_DEBUG)
145
- console.log(
146
- `🔧 [S020] Creating temporary ts-morph project for: ${filePath}`
147
- );
148
- try {
149
- const fs = require("fs");
150
- const path = require("path");
151
- const { Project } = require("ts-morph");
152
-
153
- // Check if file exists and read content
154
- if (!fs.existsSync(filePath)) {
155
- throw new Error(`File not found: ${filePath}`);
156
- }
157
-
158
- const fileContent = fs.readFileSync(filePath, "utf8");
159
- const fileName = path.basename(filePath);
160
-
161
- const tempProject = new Project({
162
- useInMemoryFileSystem: true,
163
- compilerOptions: {
164
- allowJs: true,
165
- allowSyntheticDefaultImports: true,
166
- },
167
- });
168
-
169
- // Add file content to in-memory project
170
- sourceFile = tempProject.createSourceFile(fileName, fileContent);
171
- if (process.env.SUNLINT_DEBUG)
172
- console.log(
173
- `🔧 [S020] Temporary project created successfully with file: ${fileName}`
174
- );
175
- } catch (projectError) {
176
- if (process.env.SUNLINT_DEBUG)
177
- console.log(
178
- `🔧 [S020] Failed to create temporary project:`,
179
- projectError.message
180
- );
181
- throw projectError;
182
- }
183
- }
121
+ const fileContent = fs.readFileSync(filePath, "utf8");
122
+ const fileName = path.basename(filePath);
184
123
 
185
- if (sourceFile) {
186
- const symbolViolations = await this.symbolAnalyzer.analyze(
187
- sourceFile,
188
- filePath
189
- );
190
- symbolViolations.forEach((v) => {
191
- const key = `${v.line}:${v.column}:${v.message}`;
192
- if (!violationMap.has(key)) violationMap.set(key, v);
124
+ const tempProject = new Project({
125
+ useInMemoryFileSystem: true,
126
+ compilerOptions: {
127
+ allowJs: true,
128
+ allowSyntheticDefaultImports: true,
129
+ },
193
130
  });
131
+
132
+ // Add file content to in-memory project
133
+ sourceFile = tempProject.createSourceFile(fileName, fileContent);
194
134
  if (process.env.SUNLINT_DEBUG)
195
135
  console.log(
196
- `🔧 [S020] Symbol analysis completed: ${symbolViolations.length} violations`
197
- );
198
-
199
- // If symbol-based found violations AND prioritizeSymbolic is true, skip regex
200
- // But still run regex if symbol-based didn't find any violations
201
- if (this.config.prioritizeSymbolic && symbolViolations.length > 0) {
202
- const finalViolations = Array.from(violationMap.values()).map(
203
- (v) => ({
204
- ...v,
205
- filePath,
206
- file: filePath,
207
- })
136
+ `🔧 [S020] Temporary project created successfully with file: ${fileName}`
208
137
  );
209
- if (process.env.SUNLINT_DEBUG)
210
- console.log(
211
- `🔧 [S020] Symbol-based analysis prioritized: ${finalViolations.length} violations`
212
- );
213
- return finalViolations;
214
- }
215
- } else {
138
+ } catch (projectError) {
216
139
  if (process.env.SUNLINT_DEBUG)
217
140
  console.log(
218
- `🔧 [S020] No source file found, skipping symbol analysis`
141
+ `🔧 [S020] Failed to create temporary project:`,
142
+ projectError.message
219
143
  );
144
+ throw projectError;
220
145
  }
221
- } catch (error) {
222
- console.warn(`⚠ [S020] Symbol analysis failed:`, error.message);
223
146
  }
224
- }
225
147
 
226
- // Fallback to regex-based analysis
227
- if (this.config.fallbackToRegex || this.config.regexBasedOnly) {
228
- try {
229
- if (process.env.SUNLINT_DEBUG)
230
- console.log(`🔧 [S020] Trying regex-based analysis...`);
231
- const regexViolations = await this.regexAnalyzer.analyze(filePath);
232
- regexViolations.forEach((v) => {
148
+ if (sourceFile) {
149
+ const symbolViolations = await this.symbolAnalyzer.analyze(
150
+ sourceFile,
151
+ filePath
152
+ );
153
+ symbolViolations.forEach((v) => {
233
154
  const key = `${v.line}:${v.column}:${v.message}`;
234
155
  if (!violationMap.has(key)) violationMap.set(key, v);
235
156
  });
236
157
  if (process.env.SUNLINT_DEBUG)
237
158
  console.log(
238
- `🔧 [S020] Regex analysis completed: ${regexViolations.length} violations`
159
+ `🔧 [S020] Symbol analysis completed: ${symbolViolations.length} violations`
160
+ );
161
+ } else {
162
+ if (process.env.SUNLINT_DEBUG)
163
+ console.log(
164
+ `🔧 [S020] No source file found, skipping symbol analysis`
239
165
  );
240
- } catch (error) {
241
- console.warn(`⚠ [S020] Regex analysis failed:`, error.message);
242
166
  }
167
+ } catch (error) {
168
+ console.warn(`⚠ [S020] Symbol analysis failed:`, error.message);
243
169
  }
244
170
 
245
171
  const finalViolations = Array.from(violationMap.values()).map((v) => ({
@@ -256,7 +182,6 @@ class S020Analyzer {
256
182
 
257
183
  cleanup() {
258
184
  if (this.symbolAnalyzer?.cleanup) this.symbolAnalyzer.cleanup();
259
- if (this.regexAnalyzer?.cleanup) this.regexAnalyzer.cleanup();
260
185
  }
261
186
  }
262
187
 
@@ -182,7 +182,8 @@ class S020SymbolBasedAnalyzer {
182
182
  if (args.length > 0) {
183
183
  const firstArg = args[0];
184
184
 
185
- // Check if first argument is a string literal
185
+ // ONLY flag if first argument is a string literal
186
+ // This is the only truly dangerous case for setTimeout/setInterval
186
187
  if (firstArg.getKind() === SyntaxKind.StringLiteral) {
187
188
  const startLine = call.getStartLineNumber();
188
189
  violations.push({
@@ -193,24 +194,8 @@ class S020SymbolBasedAnalyzer {
193
194
  column: 1,
194
195
  });
195
196
  }
196
- // Check if first argument contains dynamic code indicators
197
- else {
198
- const argText = firstArg.getText().toLowerCase();
199
- const hasDynamicIndicator = this.dynamicCodeIndicators.some(
200
- (indicator) => argText.includes(indicator)
201
- );
202
-
203
- if (hasDynamicIndicator) {
204
- const startLine = call.getStartLineNumber();
205
- violations.push({
206
- ruleId: this.ruleId,
207
- message: `Function '${functionName}()' may be executing dynamic code based on variable naming`,
208
- severity: "warning",
209
- line: startLine,
210
- column: 1,
211
- });
212
- }
213
- }
197
+ // NOTE: Removed variable name checking as it causes too many false positives
198
+ // Functions like setTimeout with arrow functions or function references are safe
214
199
  }
215
200
  }
216
201
  }
@@ -0,0 +1,254 @@
1
+ # S022 - Escape Data Properly Based on Output Context
2
+
3
+ ## Overview
4
+
5
+ **Rule ID:** S022
6
+ **Category:** Security
7
+ **Severity:** Error
8
+ **OWASP:** A03:2021 – Injection (XSS)
9
+ **CWE:** CWE-79 - Improper Neutralization of Input During Web Page Generation
10
+
11
+ ## Description
12
+
13
+ This rule ensures that all data output to different contexts (HTML, JavaScript, URL, CSS, attributes) is properly escaped or sanitized to prevent Cross-Site Scripting (XSS) attacks. Different output contexts require different escaping mechanisms.
14
+
15
+ XSS vulnerabilities occur when untrusted data is included in a web page without proper validation or escaping, allowing attackers to inject malicious scripts that execute in victims' browsers.
16
+
17
+ ## Why This Matters
18
+
19
+ ### Security Impact
20
+
21
+ - **Data Theft**: Attackers can steal sensitive information (cookies, session tokens, credentials)
22
+ - **Account Hijacking**: Session tokens can be stolen for account takeover
23
+ - **Malware Distribution**: Inject scripts that download malware
24
+ - **Defacement**: Modify page content to damage reputation
25
+ - **Phishing**: Redirect users to malicious sites
26
+
27
+ ### Context-Specific Escaping
28
+
29
+ Different contexts require different escaping methods:
30
+
31
+ 1. **HTML Context**: `<div>{data}</div>` → Escape `<`, `>`, `&`, `"`, `'`
32
+ 2. **JavaScript Context**: `<script>var x = '{data}'</script>` → JSON encoding
33
+ 3. **URL Context**: `<a href="{data}">` → URL encoding + validation
34
+ 4. **CSS Context**: `<style>{data}</style>` → CSS escaping
35
+ 5. **Attribute Context**: `<div title="{data}">` → Attribute escaping
36
+
37
+ ## What It Detects
38
+
39
+ ### 1. HTML Context Violations
40
+
41
+ ```javascript
42
+ // ❌ BAD: Unsafe innerHTML with user input
43
+ element.innerHTML = req.query.name;
44
+ element.outerHTML = userInput;
45
+ document.write(req.body.content);
46
+
47
+ // ❌ BAD: React dangerouslySetInnerHTML without sanitization
48
+ <div dangerouslySetInnerHTML={{__html: userInput}} />
49
+
50
+ // ❌ BAD: Vue v-html without sanitization
51
+ <div v-html="userInput"></div>
52
+
53
+ // ✅ GOOD: Use textContent for plain text
54
+ element.textContent = req.query.name;
55
+
56
+ // ✅ GOOD: Sanitize HTML with DOMPurify
57
+ element.innerHTML = DOMPurify.sanitize(userInput);
58
+
59
+ // ✅ GOOD: React with sanitization
60
+ <div dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(userInput)}} />
61
+
62
+ // ✅ GOOD: Vue with v-text
63
+ <div v-text="userInput"></div>
64
+ ```
65
+
66
+ ### 2. JavaScript Context Violations
67
+
68
+ ```javascript
69
+ // ❌ BAD: eval with user input
70
+ eval(req.query.code);
71
+ new Function(userInput)();
72
+ setTimeout(req.body.script, 1000);
73
+
74
+ // ❌ BAD: Even dynamic eval is dangerous
75
+ const code = getCodeFromSomewhere();
76
+ eval(code);
77
+
78
+ // ✅ GOOD: Never use eval - use JSON.parse for data
79
+ const data = JSON.parse(jsonString);
80
+
81
+ // ✅ GOOD: Use safe alternatives
82
+ const func = functionMap[safeKey];
83
+ func();
84
+ ```
85
+
86
+ ### 3. URL Context Violations
87
+
88
+ ```javascript
89
+ // ❌ BAD: Unvalidated URL redirect (Open Redirect)
90
+ window.location = req.query.redirect;
91
+ location.href = userInput;
92
+ window.open(req.query.url);
93
+
94
+ // ✅ GOOD: Validate URLs against whitelist
95
+ const allowedHosts = ['example.com', 'trusted.com'];
96
+ const url = new URL(req.query.redirect);
97
+ if (allowedHosts.includes(url.hostname)) {
98
+ window.location = url.href;
99
+ }
100
+
101
+ // ✅ GOOD: Use relative URLs only
102
+ if (redirectUrl.startsWith('/')) {
103
+ window.location = redirectUrl;
104
+ }
105
+ ```
106
+
107
+ ### 4. Event Handler Violations
108
+
109
+ ```javascript
110
+ // ❌ BAD: Dynamic event handler with user input
111
+ element.setAttribute('onclick', userInput);
112
+ element.setAttribute('onerror', req.query.handler);
113
+
114
+ // ✅ GOOD: Use addEventListener
115
+ element.addEventListener('click', function() {
116
+ // Safe handler logic
117
+ });
118
+
119
+ // ✅ GOOD: Use data attributes
120
+ element.dataset.action = userInput;
121
+ ```
122
+
123
+ ## Framework-Specific Guidance
124
+
125
+ ### React
126
+
127
+ ```javascript
128
+ // ❌ Avoid dangerouslySetInnerHTML
129
+ <div dangerouslySetInnerHTML={{__html: userInput}} />
130
+
131
+ // ✅ Use children or textContent
132
+ <div>{userInput}</div>
133
+
134
+ // ✅ If HTML is needed, sanitize it
135
+ import DOMPurify from 'dompurify';
136
+ <div dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(html)}} />
137
+ ```
138
+
139
+ ### Vue
140
+
141
+ ```javascript
142
+ // ❌ Avoid v-html with user input
143
+ <div v-html="userInput"></div>
144
+
145
+ // ✅ Use interpolation or v-text
146
+ <div>{{ userInput }}</div>
147
+ <div v-text="userInput"></div>
148
+
149
+ // ✅ If HTML is needed, sanitize it
150
+ <div v-html="$sanitize(html)"></div>
151
+ ```
152
+
153
+ ### Angular
154
+
155
+ ```javascript
156
+ // ❌ Avoid [innerHTML] with user input
157
+ <div [innerHTML]="userInput"></div>
158
+
159
+ // ✅ Use interpolation
160
+ <div>{{ userInput }}</div>
161
+
162
+ // ✅ Use DomSanitizer if HTML is needed
163
+ import { DomSanitizer } from '@angular/platform-browser';
164
+ this.safeHtml = this.sanitizer.sanitize(SecurityContext.HTML, html);
165
+ ```
166
+
167
+ ## Recommended Sanitization Libraries
168
+
169
+ 1. **DOMPurify** (Browser & Node.js)
170
+ ```javascript
171
+ import DOMPurify from 'dompurify';
172
+ const clean = DOMPurify.sanitize(dirty);
173
+ ```
174
+
175
+ 2. **xss** (Node.js)
176
+ ```javascript
177
+ const xss = require('xss');
178
+ const clean = xss(dirty);
179
+ ```
180
+
181
+ 3. **validator.js** (Node.js)
182
+ ```javascript
183
+ const validator = require('validator');
184
+ const escaped = validator.escape(input);
185
+ ```
186
+
187
+ ## Safe Alternatives
188
+
189
+ | Unsafe Method | Safe Alternative |
190
+ |--------------|------------------|
191
+ | `innerHTML` | `textContent` or `innerText` |
192
+ | `outerHTML` | `textContent` |
193
+ | `document.write()` | DOM manipulation methods |
194
+ | `eval()` | `JSON.parse()` |
195
+ | `setTimeout(string)` | `setTimeout(function)` |
196
+ | `dangerouslySetInnerHTML` | React children or DOMPurify |
197
+ | `v-html` | `v-text` or sanitization |
198
+
199
+ ## How to Fix
200
+
201
+ 1. **Identify the Context**: Determine where the data will be output (HTML, JS, URL, etc.)
202
+ 2. **Choose Appropriate Method**:
203
+ - HTML: Use `textContent` or sanitize with DOMPurify
204
+ - JavaScript: Avoid dynamic code execution
205
+ - URL: Validate and whitelist
206
+ 3. **Apply Escaping/Sanitization**: Use context-appropriate escaping
207
+ 4. **Test**: Verify XSS payloads don't execute
208
+
209
+ ## Testing
210
+
211
+ Create test files with common XSS payloads:
212
+
213
+ ```javascript
214
+ const testPayloads = [
215
+ '<script>alert("XSS")</script>',
216
+ '<img src=x onerror=alert("XSS")>',
217
+ 'javascript:alert("XSS")',
218
+ '<svg onload=alert("XSS")>',
219
+ '"><script>alert("XSS")</script>',
220
+ ];
221
+
222
+ // Test that these are properly escaped/sanitized
223
+ testPayloads.forEach(payload => {
224
+ const result = sanitize(payload);
225
+ assert(!result.includes('<script'));
226
+ });
227
+ ```
228
+
229
+ ## References
230
+
231
+ - [OWASP XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)
232
+ - [OWASP DOM XSS Prevention](https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html)
233
+ - [DOMPurify Documentation](https://github.com/cure53/DOMPurify)
234
+ - [CWE-79: Improper Neutralization of Input](https://cwe.mitre.org/data/definitions/79.html)
235
+
236
+ ## Configuration
237
+
238
+ This rule can be configured in `.sunlint.json`:
239
+
240
+ ```json
241
+ {
242
+ "rules": {
243
+ "S022": {
244
+ "enabled": true,
245
+ "severity": "error",
246
+ "contexts": {
247
+ "html": { "severity": "error" },
248
+ "javascript": { "severity": "error" },
249
+ "url": { "severity": "warning" }
250
+ }
251
+ }
252
+ }
253
+ }
254
+ ```