@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
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
{
|
|
2
|
+
"rule": {
|
|
3
|
+
"id": "S022",
|
|
4
|
+
"name": "Escape data properly based on output context",
|
|
5
|
+
"description": "Ensure data is properly escaped or sanitized based on the output context (HTML, JavaScript, CSS, URL) to prevent Cross-Site Scripting (XSS) attacks. Different contexts require different escaping methods.",
|
|
6
|
+
"category": "security",
|
|
7
|
+
"severity": "error",
|
|
8
|
+
"languages": ["typescript", "javascript"],
|
|
9
|
+
"frameworks": ["express", "nestjs", "react", "vue", "angular", "node"],
|
|
10
|
+
"version": "1.0.0",
|
|
11
|
+
"status": "stable",
|
|
12
|
+
"tags": ["security", "xss", "escaping", "sanitization", "owasp", "injection"],
|
|
13
|
+
"references": [
|
|
14
|
+
"https://owasp.org/www-community/attacks/xss/",
|
|
15
|
+
"https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html",
|
|
16
|
+
"https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html",
|
|
17
|
+
"https://portswigger.net/web-security/cross-site-scripting",
|
|
18
|
+
"https://cwe.mitre.org/data/definitions/79.html"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"configuration": {
|
|
22
|
+
"contexts": {
|
|
23
|
+
"html": {
|
|
24
|
+
"dangerousMethods": [
|
|
25
|
+
"innerHTML",
|
|
26
|
+
"outerHTML",
|
|
27
|
+
"insertAdjacentHTML",
|
|
28
|
+
"document.write",
|
|
29
|
+
"document.writeln",
|
|
30
|
+
"v-html",
|
|
31
|
+
"dangerouslySetInnerHTML"
|
|
32
|
+
],
|
|
33
|
+
"requireEscaping": true,
|
|
34
|
+
"severity": "error"
|
|
35
|
+
},
|
|
36
|
+
"javascript": {
|
|
37
|
+
"dangerousMethods": [
|
|
38
|
+
"eval",
|
|
39
|
+
"Function",
|
|
40
|
+
"setTimeout",
|
|
41
|
+
"setInterval",
|
|
42
|
+
"execScript"
|
|
43
|
+
],
|
|
44
|
+
"requireEscaping": true,
|
|
45
|
+
"severity": "error"
|
|
46
|
+
},
|
|
47
|
+
"url": {
|
|
48
|
+
"dangerousMethods": [
|
|
49
|
+
"location.href",
|
|
50
|
+
"window.location",
|
|
51
|
+
"location.assign",
|
|
52
|
+
"location.replace",
|
|
53
|
+
"window.open"
|
|
54
|
+
],
|
|
55
|
+
"requireValidation": true,
|
|
56
|
+
"severity": "warning"
|
|
57
|
+
},
|
|
58
|
+
"attribute": {
|
|
59
|
+
"dangerousAttributes": [
|
|
60
|
+
"onclick",
|
|
61
|
+
"onload",
|
|
62
|
+
"onerror",
|
|
63
|
+
"onmouseover",
|
|
64
|
+
"href",
|
|
65
|
+
"src"
|
|
66
|
+
],
|
|
67
|
+
"requireEscaping": true,
|
|
68
|
+
"severity": "error"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"userInputSources": [
|
|
72
|
+
"req.body",
|
|
73
|
+
"req.query",
|
|
74
|
+
"req.params",
|
|
75
|
+
"request.body",
|
|
76
|
+
"request.query",
|
|
77
|
+
"request.params",
|
|
78
|
+
"localStorage",
|
|
79
|
+
"sessionStorage",
|
|
80
|
+
"window.location",
|
|
81
|
+
"location.search",
|
|
82
|
+
"location.hash",
|
|
83
|
+
"URLSearchParams",
|
|
84
|
+
"document.cookie",
|
|
85
|
+
"window.name",
|
|
86
|
+
"postMessage"
|
|
87
|
+
],
|
|
88
|
+
"safeEscapingFunctions": [
|
|
89
|
+
"escape",
|
|
90
|
+
"escapeHtml",
|
|
91
|
+
"sanitize",
|
|
92
|
+
"DOMPurify.sanitize",
|
|
93
|
+
"textContent",
|
|
94
|
+
"innerText",
|
|
95
|
+
"setAttribute",
|
|
96
|
+
"encodeURIComponent",
|
|
97
|
+
"encodeURI",
|
|
98
|
+
"validator.escape",
|
|
99
|
+
"xss.filterXSS"
|
|
100
|
+
],
|
|
101
|
+
"safeFrameworkMethods": {
|
|
102
|
+
"react": [
|
|
103
|
+
"textContent",
|
|
104
|
+
"children",
|
|
105
|
+
"{variable}"
|
|
106
|
+
],
|
|
107
|
+
"vue": [
|
|
108
|
+
"{{ variable }}",
|
|
109
|
+
"v-text"
|
|
110
|
+
],
|
|
111
|
+
"angular": [
|
|
112
|
+
"{{ variable }}",
|
|
113
|
+
"[textContent]"
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"examples": {
|
|
118
|
+
"violations": [
|
|
119
|
+
{
|
|
120
|
+
"description": "Unescaped user input in innerHTML",
|
|
121
|
+
"code": "element.innerHTML = userInput;",
|
|
122
|
+
"context": "html"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"description": "User input in eval without validation",
|
|
126
|
+
"code": "eval(req.query.code);",
|
|
127
|
+
"context": "javascript"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"description": "Unvalidated URL from user input",
|
|
131
|
+
"code": "window.location = req.query.redirect;",
|
|
132
|
+
"context": "url"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"description": "React dangerouslySetInnerHTML without sanitization",
|
|
136
|
+
"code": "<div dangerouslySetInnerHTML={{__html: userInput}} />",
|
|
137
|
+
"context": "html"
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"description": "Vue v-html directive with user input",
|
|
141
|
+
"code": "<div v-html=\"userInput\"></div>",
|
|
142
|
+
"context": "html"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"description": "Dynamic event handler with user input",
|
|
146
|
+
"code": "element.setAttribute('onclick', userInput);",
|
|
147
|
+
"context": "attribute"
|
|
148
|
+
}
|
|
149
|
+
],
|
|
150
|
+
"fixes": [
|
|
151
|
+
{
|
|
152
|
+
"description": "Use textContent instead of innerHTML",
|
|
153
|
+
"code": "element.textContent = userInput;",
|
|
154
|
+
"context": "html"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"description": "Sanitize HTML with DOMPurify",
|
|
158
|
+
"code": "element.innerHTML = DOMPurify.sanitize(userInput);",
|
|
159
|
+
"context": "html"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"description": "Never use eval with user input",
|
|
163
|
+
"code": "// Avoid eval entirely, use JSON.parse or safe alternatives",
|
|
164
|
+
"context": "javascript"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"description": "Validate and whitelist URLs",
|
|
168
|
+
"code": "const allowedHosts = ['example.com'];\nconst url = new URL(req.query.redirect);\nif (allowedHosts.includes(url.hostname)) {\n window.location = url.href;\n}",
|
|
169
|
+
"context": "url"
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"description": "React with proper sanitization",
|
|
173
|
+
"code": "<div dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(userInput)}} />",
|
|
174
|
+
"context": "html"
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"description": "Vue using v-text for safe output",
|
|
178
|
+
"code": "<div v-text=\"userInput\"></div>",
|
|
179
|
+
"context": "html"
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
},
|
|
183
|
+
"testing": {
|
|
184
|
+
"testCases": [
|
|
185
|
+
{
|
|
186
|
+
"name": "innerHTML_with_user_input",
|
|
187
|
+
"type": "violation",
|
|
188
|
+
"description": "Using innerHTML with unsanitized user input"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"name": "eval_with_user_input",
|
|
192
|
+
"type": "violation",
|
|
193
|
+
"description": "Using eval with user input"
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"name": "location_with_user_input",
|
|
197
|
+
"type": "violation",
|
|
198
|
+
"description": "Setting location with unvalidated user input"
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"name": "textContent_safe_output",
|
|
202
|
+
"type": "clean",
|
|
203
|
+
"description": "Using textContent for safe output"
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"name": "dompurify_sanitization",
|
|
207
|
+
"type": "clean",
|
|
208
|
+
"description": "Using DOMPurify to sanitize HTML"
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"name": "url_validation",
|
|
212
|
+
"type": "clean",
|
|
213
|
+
"description": "Validating URLs before redirect"
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
},
|
|
217
|
+
"performance": {
|
|
218
|
+
"complexity": "O(n)",
|
|
219
|
+
"description": "Linear complexity based on number of DOM manipulation and output operations in the source code"
|
|
220
|
+
},
|
|
221
|
+
"owaspMapping": {
|
|
222
|
+
"category": "A03:2021 – Injection",
|
|
223
|
+
"subcategories": [
|
|
224
|
+
"A07:2021 – Identification and Authentication Failures"
|
|
225
|
+
],
|
|
226
|
+
"cweId": "CWE-79",
|
|
227
|
+
"description": "Validates that all output is properly escaped or sanitized based on context to prevent XSS attacks"
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -82,6 +82,21 @@ class S023Analyzer {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
async analyzeFile(filePath, language, options = {}) {
|
|
85
|
+
// Skip test files - they often use JSON.parse for testing purposes
|
|
86
|
+
const skipPatterns = [
|
|
87
|
+
/\.spec\.(ts|tsx|js|jsx)$/,
|
|
88
|
+
/\.test\.(ts|tsx|js|jsx)$/,
|
|
89
|
+
/__tests__\//, // Test directories
|
|
90
|
+
/__mocks__\//, // Mock directories
|
|
91
|
+
/\/tests?\//, // Test directories
|
|
92
|
+
/\/fixtures?\//, // Fixture directories
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const shouldSkip = skipPatterns.some((pattern) => pattern.test(filePath));
|
|
96
|
+
if (shouldSkip) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
|
|
85
100
|
switch (language) {
|
|
86
101
|
case 'typescript':
|
|
87
102
|
case 'javascript':
|
|
@@ -10,12 +10,27 @@ class S023ASTAnalyzer {
|
|
|
10
10
|
|
|
11
11
|
async analyze(files, language, options = {}) {
|
|
12
12
|
const violations = [];
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
// Skip patterns for test files
|
|
15
|
+
const skipPatterns = [
|
|
16
|
+
/\.spec\.(ts|tsx|js|jsx)$/,
|
|
17
|
+
/\.test\.(ts|tsx|js|jsx)$/,
|
|
18
|
+
/__tests__\//,
|
|
19
|
+
/__mocks__\//,
|
|
20
|
+
/\/tests?\//,
|
|
21
|
+
/\/fixtures?\//,
|
|
22
|
+
];
|
|
23
|
+
|
|
14
24
|
for (const filePath of files) {
|
|
25
|
+
// Skip test files
|
|
26
|
+
if (skipPatterns.some(pattern => pattern.test(filePath))) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
15
30
|
if (options.verbose) {
|
|
16
31
|
console.log(`🎯 Running S023 AST analysis on ${path.basename(filePath)}`);
|
|
17
32
|
}
|
|
18
|
-
|
|
33
|
+
|
|
19
34
|
try {
|
|
20
35
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
21
36
|
const fileViolations = await this.analyzeFile(filePath, content, language, options);
|
|
@@ -24,7 +39,7 @@ class S023ASTAnalyzer {
|
|
|
24
39
|
console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
|
|
25
40
|
}
|
|
26
41
|
}
|
|
27
|
-
|
|
42
|
+
|
|
28
43
|
return violations;
|
|
29
44
|
}
|
|
30
45
|
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
{
|
|
2
|
+
"rule": {
|
|
3
|
+
"id": "S023",
|
|
4
|
+
"name": "Prevent JSON Injection and JSON eval attacks",
|
|
5
|
+
"description": "Prevent JSON injection attacks and unsafe JSON handling. Detects unsafe JSON.parse(), eval() with JSON, JSON.stringify in HTML context, and JSON handling without proper validation.",
|
|
6
|
+
"category": "security",
|
|
7
|
+
"severity": "error",
|
|
8
|
+
"languages": ["typescript", "javascript"],
|
|
9
|
+
"frameworks": ["express", "nestjs", "node", "react", "vue", "angular"],
|
|
10
|
+
"version": "1.0.0",
|
|
11
|
+
"status": "stable",
|
|
12
|
+
"tags": ["security", "json", "injection", "xss", "owasp", "eval"],
|
|
13
|
+
"references": [
|
|
14
|
+
"https://owasp.org/www-community/vulnerabilities/JSON_Injection",
|
|
15
|
+
"https://cheatsheetseries.owasp.org/cheatsheets/Injection_Prevention_Cheat_Sheet.html",
|
|
16
|
+
"https://portswigger.net/web-security/dom-based/json-injection",
|
|
17
|
+
"https://cwe.mitre.org/data/definitions/94.html"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"configuration": {
|
|
21
|
+
"checkJsonParse": true,
|
|
22
|
+
"checkEvalWithJson": true,
|
|
23
|
+
"checkJsonStringifyInHtml": true,
|
|
24
|
+
"userInputSources": [
|
|
25
|
+
"localStorage",
|
|
26
|
+
"sessionStorage",
|
|
27
|
+
"window.location",
|
|
28
|
+
"location.search",
|
|
29
|
+
"location.hash",
|
|
30
|
+
"URLSearchParams",
|
|
31
|
+
"req.body",
|
|
32
|
+
"req.query",
|
|
33
|
+
"req.params",
|
|
34
|
+
"request.body",
|
|
35
|
+
"request.query",
|
|
36
|
+
"document.cookie",
|
|
37
|
+
"window.name",
|
|
38
|
+
"postMessage",
|
|
39
|
+
"fetch",
|
|
40
|
+
"axios"
|
|
41
|
+
],
|
|
42
|
+
"validationPatterns": [
|
|
43
|
+
"try",
|
|
44
|
+
"catch",
|
|
45
|
+
"typeof",
|
|
46
|
+
"instanceof",
|
|
47
|
+
"validate",
|
|
48
|
+
"check",
|
|
49
|
+
"isValid",
|
|
50
|
+
"sanitize",
|
|
51
|
+
"escape",
|
|
52
|
+
"filter"
|
|
53
|
+
],
|
|
54
|
+
"htmlContextPatterns": [
|
|
55
|
+
"innerHTML",
|
|
56
|
+
"outerHTML",
|
|
57
|
+
"insertAdjacentHTML",
|
|
58
|
+
"document.write",
|
|
59
|
+
".html(",
|
|
60
|
+
"<script",
|
|
61
|
+
"</script>"
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
"examples": {
|
|
65
|
+
"violations": [
|
|
66
|
+
{
|
|
67
|
+
"description": "Unsafe JSON.parse with user input",
|
|
68
|
+
"code": "const data = JSON.parse(localStorage.getItem('userData'));"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"description": "Using eval() to parse JSON",
|
|
72
|
+
"code": "const obj = eval('(' + jsonString + ')');"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"description": "JSON.stringify in HTML context without escaping",
|
|
76
|
+
"code": "element.innerHTML = JSON.stringify(userInput);"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"description": "Parsing URL parameters without validation",
|
|
80
|
+
"code": "const params = JSON.parse(new URLSearchParams(window.location.search).get('data'));"
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
"fixes": [
|
|
84
|
+
{
|
|
85
|
+
"description": "Validate input before parsing JSON",
|
|
86
|
+
"code": "try {\n const data = JSON.parse(localStorage.getItem('userData'));\n if (typeof data === 'object' && data !== null) {\n // Use data\n }\n} catch (e) {\n // Handle error\n}"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"description": "Always use JSON.parse instead of eval",
|
|
90
|
+
"code": "const obj = JSON.parse(jsonString);"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"description": "Escape JSON output when used in HTML",
|
|
94
|
+
"code": "element.textContent = JSON.stringify(userInput);\n// or\nelement.innerHTML = escapeHtml(JSON.stringify(userInput));"
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
"testing": {
|
|
99
|
+
"testCases": [
|
|
100
|
+
{
|
|
101
|
+
"name": "unsafe_json_parse_localstorage",
|
|
102
|
+
"type": "violation",
|
|
103
|
+
"description": "JSON.parse with localStorage without validation"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"name": "eval_with_json",
|
|
107
|
+
"type": "violation",
|
|
108
|
+
"description": "Using eval() to parse JSON data"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"name": "json_stringify_html_context",
|
|
112
|
+
"type": "violation",
|
|
113
|
+
"description": "JSON.stringify output in innerHTML"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"name": "safe_json_parse_with_trycatch",
|
|
117
|
+
"type": "clean",
|
|
118
|
+
"description": "JSON.parse with proper try-catch validation"
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
"performance": {
|
|
123
|
+
"complexity": "O(n)",
|
|
124
|
+
"description": "Linear complexity based on number of JSON operations in the source code"
|
|
125
|
+
},
|
|
126
|
+
"owaspMapping": {
|
|
127
|
+
"category": "A03:2021 – Injection",
|
|
128
|
+
"subcategories": [
|
|
129
|
+
"A05:2021 – Security Misconfiguration"
|
|
130
|
+
],
|
|
131
|
+
"description": "Validates that JSON parsing and handling is done safely to prevent injection attacks and XSS vulnerabilities"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -113,6 +113,12 @@ class S024RegexBasedAnalyzer {
|
|
|
113
113
|
// Remove comments to avoid false positives
|
|
114
114
|
const cleanContent = this.removeComments(content);
|
|
115
115
|
|
|
116
|
+
// PRE-FILTER: Skip files that don't use XPath or XML at all
|
|
117
|
+
// This prevents false positives on regular JavaScript code
|
|
118
|
+
if (!this.hasXPathOrXMLUsage(cleanContent)) {
|
|
119
|
+
return []; // Early return - no XPath/XML usage detected
|
|
120
|
+
}
|
|
121
|
+
|
|
116
122
|
try {
|
|
117
123
|
// Pattern 1: XPath Injection vulnerabilities
|
|
118
124
|
violations.push(
|
|
@@ -146,6 +152,41 @@ class S024RegexBasedAnalyzer {
|
|
|
146
152
|
return violations;
|
|
147
153
|
}
|
|
148
154
|
|
|
155
|
+
hasXPathOrXMLUsage(content) {
|
|
156
|
+
// Check if file imports or uses XPath/XML libraries
|
|
157
|
+
const xpathXmlIndicators = [
|
|
158
|
+
// Library imports
|
|
159
|
+
/require\s*\(\s*['"]xpath['"]\s*\)/i,
|
|
160
|
+
/require\s*\(\s*['"]xmldom['"]\s*\)/i,
|
|
161
|
+
/require\s*\(\s*['"]libxmljs['"]\s*\)/i,
|
|
162
|
+
/require\s*\(\s*['"]xml2js['"]\s*\)/i,
|
|
163
|
+
/require\s*\(\s*['"]fast-xml-parser['"]\s*\)/i,
|
|
164
|
+
/from\s+['"]xpath['"]/i,
|
|
165
|
+
/from\s+['"]xmldom['"]/i,
|
|
166
|
+
/from\s+['"]libxmljs['"]/i,
|
|
167
|
+
/from\s+['"]xml2js['"]/i,
|
|
168
|
+
|
|
169
|
+
// XPath method calls
|
|
170
|
+
/xpath\s*\.\s*(?:evaluate|select|selectText|selectValue|selectNodes)/i,
|
|
171
|
+
/\.evaluate\s*\([^)]*\s*,\s*(?:XPathResult|doc|document)/i,
|
|
172
|
+
|
|
173
|
+
// XML parsing methods
|
|
174
|
+
/DOMParser\s*\(\s*\)/,
|
|
175
|
+
/parseFromString/i,
|
|
176
|
+
/parseXml/i,
|
|
177
|
+
/parseString\s*\(/,
|
|
178
|
+
/XSLTProcessor/,
|
|
179
|
+
/SAXParser/,
|
|
180
|
+
|
|
181
|
+
// XML document indicators
|
|
182
|
+
/<\?xml/i,
|
|
183
|
+
/xmlDoc/i,
|
|
184
|
+
/xmlDocument/i,
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
return xpathXmlIndicators.some(pattern => pattern.test(content));
|
|
188
|
+
}
|
|
189
|
+
|
|
149
190
|
analyzeXPathInjection(content, lines, filePath) {
|
|
150
191
|
const violations = [];
|
|
151
192
|
|
|
@@ -74,7 +74,24 @@ class S027CategorizedAnalyzer {
|
|
|
74
74
|
'coverage/', '.next/', '.cache/', 'tmp/',
|
|
75
75
|
'.lock', '.log', '.min.js', '.bundle.js'
|
|
76
76
|
];
|
|
77
|
-
|
|
77
|
+
|
|
78
|
+
// Skip test files completely - they often contain mock/fake credentials
|
|
79
|
+
const testPatterns = [
|
|
80
|
+
/\.spec\.(ts|tsx|js|jsx)$/,
|
|
81
|
+
/\.test\.(ts|tsx|js|jsx)$/,
|
|
82
|
+
/__tests__\//,
|
|
83
|
+
/__mocks__\//,
|
|
84
|
+
/\/tests?\//,
|
|
85
|
+
/\/fixtures?\//,
|
|
86
|
+
/\/factories\//, // Test factories
|
|
87
|
+
/setupTests\./,
|
|
88
|
+
/testSetup\./,
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
if (testPatterns.some(pattern => pattern.test(filePath))) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
78
95
|
return skipPatterns.some(pattern => filePath.includes(pattern));
|
|
79
96
|
}
|
|
80
97
|
|
|
@@ -95,11 +112,16 @@ class S027CategorizedAnalyzer {
|
|
|
95
112
|
return;
|
|
96
113
|
}
|
|
97
114
|
|
|
115
|
+
// Skip NEXT_PUBLIC_ environment variables - these are public by design
|
|
116
|
+
if (this.isPublicEnvironmentVariable(line)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
98
120
|
// Check global exclude patterns first
|
|
99
121
|
if (this.matchesGlobalExcludes(line)) {
|
|
100
122
|
return;
|
|
101
123
|
}
|
|
102
|
-
|
|
124
|
+
|
|
103
125
|
// Check each category
|
|
104
126
|
this.categories.forEach(category => {
|
|
105
127
|
const categoryViolations = this.checkCategory(
|
|
@@ -136,7 +158,39 @@ class S027CategorizedAnalyzer {
|
|
|
136
158
|
matchesGlobalExcludes(line) {
|
|
137
159
|
return this.globalExcludePatterns.some(pattern => pattern.test(line));
|
|
138
160
|
}
|
|
139
|
-
|
|
161
|
+
|
|
162
|
+
isPublicEnvironmentVariable(line) {
|
|
163
|
+
// NEXT_PUBLIC_, REACT_APP_, VITE_, PUBLIC_ prefixed env vars are public by design
|
|
164
|
+
// These are intentionally exposed to the client-side and are not secrets
|
|
165
|
+
const publicEnvPatterns = [
|
|
166
|
+
/NEXT_PUBLIC_[A-Z0-9_]+\s*[:=]/i, // Next.js public env vars
|
|
167
|
+
/REACT_APP_[A-Z0-9_]+\s*[:=]/i, // Create React App public env vars
|
|
168
|
+
/VITE_[A-Z0-9_]+\s*[:=]/i, // Vite public env vars
|
|
169
|
+
/PUBLIC_[A-Z0-9_]+\s*[:=]/i, // Generic public prefix
|
|
170
|
+
/EXPO_PUBLIC_[A-Z0-9_]+\s*[:=]/i, // Expo public env vars
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
return publicEnvPatterns.some(pattern => pattern.test(line));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
isEnvironmentVariableUsage(line) {
|
|
177
|
+
// Patterns that indicate environment variable or config service usage - these are SAFE
|
|
178
|
+
const safePatterns = [
|
|
179
|
+
/process\.env\./i, // process.env.DB_PASSWORD
|
|
180
|
+
/configService\.get/i, // configService.get('DB_PASSWORD')
|
|
181
|
+
/config\.get/i, // config.get('API_KEY')
|
|
182
|
+
/getConfig\(/i, // getConfig('secret')
|
|
183
|
+
/env\(['"]([^'"]+)['"]\)/i, // env('SECRET_KEY')
|
|
184
|
+
/process\.env\[['"]([^'"]+)['"]\]/i, // process.env['API_KEY']
|
|
185
|
+
/import\.meta\.env\./i, // import.meta.env.VITE_API_KEY
|
|
186
|
+
/Deno\.env\.get/i, // Deno.env.get()
|
|
187
|
+
/os\.getenv/i, // Python os.getenv()
|
|
188
|
+
/System\.getenv/i, // Java System.getenv()
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
return safePatterns.some(pattern => pattern.test(line));
|
|
192
|
+
}
|
|
193
|
+
|
|
140
194
|
checkCategory(category, line, lineNumber, filePath, isTestFile) {
|
|
141
195
|
const violations = [];
|
|
142
196
|
|
|
@@ -156,14 +210,19 @@ class S027CategorizedAnalyzer {
|
|
|
156
210
|
}
|
|
157
211
|
|
|
158
212
|
// Check category-specific excludes
|
|
159
|
-
if (category.compiledExcludePatterns &&
|
|
213
|
+
if (category.compiledExcludePatterns &&
|
|
160
214
|
category.compiledExcludePatterns.some(pattern => pattern.test(matchedText))) {
|
|
161
215
|
continue;
|
|
162
216
|
}
|
|
163
|
-
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
217
|
+
|
|
218
|
+
// Check if this uses environment variables or config services - SAFE patterns
|
|
219
|
+
if (this.isEnvironmentVariableUsage(line)) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Be more lenient in test files - skip all but critical severity
|
|
224
|
+
// Test files commonly use hardcoded values for testing purposes
|
|
225
|
+
if (isTestFile && (category.severity === 'low' || category.severity === 'medium' || category.severity === 'high')) {
|
|
167
226
|
continue;
|
|
168
227
|
}
|
|
169
228
|
|
|
@@ -136,7 +136,25 @@ class S027CategorizedAnalyzer {
|
|
|
136
136
|
matchesGlobalExcludes(line) {
|
|
137
137
|
return this.globalExcludePatterns.some(pattern => pattern.test(line));
|
|
138
138
|
}
|
|
139
|
-
|
|
139
|
+
|
|
140
|
+
isEnvironmentVariableUsage(line) {
|
|
141
|
+
// Patterns that indicate environment variable or config service usage - these are SAFE
|
|
142
|
+
const safePatterns = [
|
|
143
|
+
/process\.env\./i, // process.env.DB_PASSWORD
|
|
144
|
+
/configService\.get/i, // configService.get('DB_PASSWORD')
|
|
145
|
+
/config\.get/i, // config.get('API_KEY')
|
|
146
|
+
/getConfig\(/i, // getConfig('secret')
|
|
147
|
+
/env\(['"]([^'"]+)['"]\)/i, // env('SECRET_KEY')
|
|
148
|
+
/process\.env\[['"]([^'"]+)['"]\]/i, // process.env['API_KEY']
|
|
149
|
+
/import\.meta\.env\./i, // import.meta.env.VITE_API_KEY
|
|
150
|
+
/Deno\.env\.get/i, // Deno.env.get()
|
|
151
|
+
/os\.getenv/i, // Python os.getenv()
|
|
152
|
+
/System\.getenv/i, // Java System.getenv()
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
return safePatterns.some(pattern => pattern.test(line));
|
|
156
|
+
}
|
|
157
|
+
|
|
140
158
|
checkCategory(category, line, lineNumber, filePath, isTestFile) {
|
|
141
159
|
const violations = [];
|
|
142
160
|
|
|
@@ -156,14 +174,19 @@ class S027CategorizedAnalyzer {
|
|
|
156
174
|
}
|
|
157
175
|
|
|
158
176
|
// Check category-specific excludes
|
|
159
|
-
if (category.compiledExcludePatterns &&
|
|
177
|
+
if (category.compiledExcludePatterns &&
|
|
160
178
|
category.compiledExcludePatterns.some(pattern => pattern.test(matchedText))) {
|
|
161
179
|
continue;
|
|
162
180
|
}
|
|
163
|
-
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
181
|
+
|
|
182
|
+
// Check if this uses environment variables or config services - SAFE patterns
|
|
183
|
+
if (this.isEnvironmentVariableUsage(line)) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Be more lenient in test files - skip all but critical severity
|
|
188
|
+
// Test files commonly use hardcoded values for testing purposes
|
|
189
|
+
if (isTestFile && (category.severity === 'low' || category.severity === 'medium' || category.severity === 'high')) {
|
|
167
190
|
continue;
|
|
168
191
|
}
|
|
169
192
|
|