@sun-asterisk/sunlint 1.3.26 → 1.3.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/rules/enhanced-rules-registry.json +101 -17
- package/config/rules/rules-registry-generated.json +22 -22
- package/origin-rules/security-en.md +351 -338
- package/package.json +1 -1
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +73 -21
- package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +206 -2
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +553 -58
- package/rules/common/C029_catch_block_logging/analyzer.js +47 -12
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +35 -15
- package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +9 -5
- package/rules/security/S003_open_redirect_protection/README.md +371 -0
- package/rules/security/S003_open_redirect_protection/analyzer.js +135 -0
- package/rules/security/S003_open_redirect_protection/config.json +58 -0
- package/rules/security/S003_open_redirect_protection/symbol-based-analyzer.js +884 -0
- package/rules/security/S004_sensitive_data_logging/analyzer.js +135 -0
- package/rules/security/S004_sensitive_data_logging/config.json +62 -0
- package/rules/security/S004_sensitive_data_logging/symbol-based-analyzer.js +592 -0
- package/rules/security/S005_no_origin_auth/analyzer.js +97 -148
- package/rules/security/S005_no_origin_auth/config.json +28 -67
- package/rules/security/S005_no_origin_auth/symbol-based-analyzer.js +708 -0
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +170 -31
- package/rules/security/S010_no_insecure_encryption/analyzer.js +8 -2
- package/rules/security/S012_hardcoded_secrets/analyzer.js +149 -0
- package/rules/security/S012_hardcoded_secrets/config.json +75 -0
- package/rules/security/S012_hardcoded_secrets/symbol-based-analyzer.js +1204 -0
- package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +87 -0
- package/rules/security/S017_use_parameterized_queries/analyzer.js +11 -78
- package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +1146 -1
- package/rules/security/S019_smtp_injection_protection/analyzer.js +120 -0
- package/rules/security/S019_smtp_injection_protection/config.json +35 -0
- package/rules/security/S019_smtp_injection_protection/symbol-based-analyzer.js +687 -0
- package/rules/security/S020_no_eval_dynamic_code/analyzer.js +55 -130
- package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +4 -19
- package/rules/security/S022_escape_output_context/README.md +254 -0
- package/rules/security/S022_escape_output_context/analyzer.js +510 -0
- package/rules/security/S022_escape_output_context/config.json +229 -0
- package/rules/security/S023_no_json_injection/analyzer.js +15 -0
- package/rules/security/S023_no_json_injection/ast-analyzer.js +18 -3
- package/rules/security/S023_no_json_injection/config.json +133 -0
- package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +41 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +67 -8
- package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +29 -6
- package/rules/security/S029_csrf_protection/config.json +127 -0
- package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +160 -28
- package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +81 -19
- package/rules/security/S031_secure_session_cookies/analyzer.js +20 -2
- package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +100 -0
- package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +8 -1
- package/rules/security/S032_httponly_session_cookies/analyzer.js +2 -2
- package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +115 -0
- package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +39 -10
- package/rules/security/S036_lfi_rfi_protection/analyzer.js +224 -0
- package/rules/security/S036_lfi_rfi_protection/config.json +20 -0
- package/rules/security/S040_session_fixation_protection/analyzer.js +153 -0
- package/rules/security/S040_session_fixation_protection/config.json +20 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/README.md +83 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/analyzer.js +153 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/config.json +41 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/symbol-based-analyzer.js +1139 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/README.md +107 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/analyzer.js +153 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/config.json +41 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/symbol-based-analyzer.js +541 -0
- package/docs/COMMAND-EXAMPLES.md +0 -390
- package/docs/FILE_LIMITS_COMPLETION_REPORT.md +0 -151
- package/docs/FOLDER_STRUCTURE.md +0 -59
- package/docs/SIMPLIFIED_USAGE_GUIDE.md +0 -208
- package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +0 -541
- package/rules/security/S020_no_eval_dynamic_code/regex-based-analyzer.js +0 -307
|
@@ -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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
123
|
-
this.
|
|
124
|
-
|
|
125
|
-
(
|
|
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
|
-
|
|
128
|
-
|
|
105
|
+
if (!sourceFile) {
|
|
106
|
+
// Create a minimal ts-morph project for this analysis
|
|
129
107
|
if (process.env.SUNLINT_DEBUG)
|
|
130
|
-
console.log(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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]
|
|
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
|
-
|
|
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]
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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]
|
|
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
|
-
//
|
|
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
|
-
//
|
|
197
|
-
|
|
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
|
+
```
|