@sun-asterisk/sunlint 1.3.3 → 1.3.5
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/CHANGELOG.md +69 -2
- package/config/presets/all.json +49 -48
- package/config/presets/beginner.json +7 -18
- package/config/presets/ci.json +63 -27
- package/config/presets/maintainability.json +6 -4
- package/config/presets/performance.json +4 -3
- package/config/presets/quality.json +11 -50
- package/config/presets/recommended.json +83 -10
- package/config/presets/security.json +20 -19
- package/config/presets/strict.json +6 -13
- package/config/rules/enhanced-rules-registry.json +64 -7
- package/core/analysis-orchestrator.js +6 -0
- package/core/cli-action-handler.js +15 -1
- package/core/config-preset-resolver.js +7 -2
- package/engines/engine-factory.js +7 -0
- package/package.json +1 -1
- package/rules/common/C067_no_hardcoded_config/analyzer.js +95 -0
- package/rules/common/C067_no_hardcoded_config/config.json +81 -0
- package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +1000 -0
- package/rules/security/S024_xpath_xxe_protection/analyzer.js +242 -0
- package/rules/security/S024_xpath_xxe_protection/config.json +152 -0
- package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +338 -0
- package/rules/security/S024_xpath_xxe_protection/symbol-based-analyzer.js +474 -0
- package/rules/security/S025_server_side_validation/README.md +179 -0
- package/rules/security/S025_server_side_validation/analyzer.js +242 -0
- package/rules/security/S025_server_side_validation/config.json +111 -0
- package/rules/security/S025_server_side_validation/regex-based-analyzer.js +388 -0
- package/rules/security/S025_server_side_validation/symbol-based-analyzer.js +523 -0
- package/scripts/README.md +83 -0
- package/scripts/analyze-core-rules.js +151 -0
- package/scripts/generate-presets.js +202 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S024 Main Analyzer - Protect against XPath Injection and XML External Entity (XXE)
|
|
3
|
+
* Primary: Symbol-based analysis (when available)
|
|
4
|
+
* Fallback: Regex-based for all other cases
|
|
5
|
+
* Command: node cli.js --rule=S024 --input=examples/rule-test-fixtures/rules/S024_xpath_xxe_protection --engine=heuristic
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const S024SymbolBasedAnalyzer = require("./symbol-based-analyzer.js");
|
|
9
|
+
const S024RegexBasedAnalyzer = require("./regex-based-analyzer.js");
|
|
10
|
+
|
|
11
|
+
class S024Analyzer {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
14
|
+
console.log(`🔧 [S024] Constructor called with options:`, !!options);
|
|
15
|
+
console.log(
|
|
16
|
+
`🔧 [S024] Options type:`,
|
|
17
|
+
typeof options,
|
|
18
|
+
Object.keys(options || {})
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.ruleId = "S024";
|
|
23
|
+
this.ruleName = "Protect against XPath Injection and XML External Entity (XXE)";
|
|
24
|
+
this.description =
|
|
25
|
+
"Protect against XPath Injection and XML External Entity (XXE) attacks. XPath injection occurs when user input is used to construct XPath queries without proper sanitization. XXE attacks exploit XML parsers that process external entities, potentially leading to data disclosure, server-side request forgery, or denial of service.";
|
|
26
|
+
this.semanticEngine = options.semanticEngine || null;
|
|
27
|
+
this.verbose = options.verbose || false;
|
|
28
|
+
|
|
29
|
+
// Configuration
|
|
30
|
+
this.config = {
|
|
31
|
+
useSymbolBased: true, // Primary approach
|
|
32
|
+
fallbackToRegex: true, // Secondary approach
|
|
33
|
+
regexBasedOnly: false, // Can be set to true for pure mode
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Initialize analyzers
|
|
37
|
+
try {
|
|
38
|
+
this.symbolAnalyzer = new S024SymbolBasedAnalyzer(this.semanticEngine);
|
|
39
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
40
|
+
console.log(`🔧 [S024] Symbol analyzer created successfully`);
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error(`🔧 [S024] Error creating symbol analyzer:`, error);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
this.regexAnalyzer = new S024RegexBasedAnalyzer(this.semanticEngine);
|
|
48
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
49
|
+
console.log(`🔧 [S024] Regex analyzer created successfully`);
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error(`🔧 [S024] Error creating regex analyzer:`, error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Initialize analyzer with semantic engine
|
|
58
|
+
*/
|
|
59
|
+
async initialize(semanticEngine) {
|
|
60
|
+
this.semanticEngine = semanticEngine;
|
|
61
|
+
|
|
62
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
63
|
+
console.log(`🔧 [S024] Main analyzer initializing...`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Initialize both analyzers
|
|
67
|
+
if (this.symbolAnalyzer) {
|
|
68
|
+
await this.symbolAnalyzer.initialize?.(semanticEngine);
|
|
69
|
+
}
|
|
70
|
+
if (this.regexAnalyzer) {
|
|
71
|
+
await this.regexAnalyzer.initialize?.(semanticEngine);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Clean up if needed
|
|
75
|
+
if (this.regexAnalyzer) {
|
|
76
|
+
this.regexAnalyzer.cleanup?.();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
80
|
+
console.log(`🔧 [S024] Main analyzer initialized successfully`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Single file analysis method for testing
|
|
86
|
+
*/
|
|
87
|
+
analyzeSingle(filePath, options = {}) {
|
|
88
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
89
|
+
console.log(`📊 [S024] analyzeSingle() called for: ${filePath}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Return result using same format as analyze method
|
|
93
|
+
return this.analyze([filePath], "typescript", options);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async analyze(files, language, options = {}) {
|
|
97
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
98
|
+
console.log(
|
|
99
|
+
`🔧 [S024] analyze() method called with ${files.length} files, language: ${language}`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const violations = [];
|
|
104
|
+
|
|
105
|
+
for (const filePath of files) {
|
|
106
|
+
try {
|
|
107
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
108
|
+
console.log(`🔧 [S024] Processing file: ${filePath}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const fileViolations = await this.analyzeFile(filePath, options);
|
|
112
|
+
violations.push(...fileViolations);
|
|
113
|
+
|
|
114
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
115
|
+
console.log(
|
|
116
|
+
`🔧 [S024] File ${filePath}: Found ${fileViolations.length} violations`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.warn(
|
|
121
|
+
`⚠ [S024] Analysis failed for ${filePath}:`,
|
|
122
|
+
error.message
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
128
|
+
console.log(`🔧 [S024] Total violations found: ${violations.length}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return violations;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async analyzeFile(filePath, options = {}) {
|
|
135
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
136
|
+
console.log(`🔍 [S024] analyzeFile() called for: ${filePath}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Create a Map to track unique violations and prevent duplicates
|
|
140
|
+
const violationMap = new Map();
|
|
141
|
+
|
|
142
|
+
// 1. Try Symbol-based analysis first (primary)
|
|
143
|
+
if (
|
|
144
|
+
this.config.useSymbolBased &&
|
|
145
|
+
this.semanticEngine?.project &&
|
|
146
|
+
this.semanticEngine?.initialized
|
|
147
|
+
) {
|
|
148
|
+
try {
|
|
149
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
150
|
+
console.log(`🔧 [S024] Trying symbol-based analysis...`);
|
|
151
|
+
}
|
|
152
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
153
|
+
if (sourceFile) {
|
|
154
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
155
|
+
console.log(`🔧 [S024] Source file found, analyzing...`);
|
|
156
|
+
}
|
|
157
|
+
const symbolViolations = await this.symbolAnalyzer.analyze(
|
|
158
|
+
sourceFile,
|
|
159
|
+
filePath
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Add to violation map with deduplication
|
|
163
|
+
symbolViolations.forEach((violation) => {
|
|
164
|
+
const key = `${violation.line}:${violation.column}:${violation.message}`;
|
|
165
|
+
if (!violationMap.has(key)) {
|
|
166
|
+
violationMap.set(key, violation);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
171
|
+
console.log(
|
|
172
|
+
`🔧 [S024] Symbol analysis completed: ${symbolViolations.length} violations`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
177
|
+
console.log(`🔧 [S024] Source file not found, falling back...`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.warn(`⚠ [S024] Symbol analysis failed:`, error.message);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 2. Try Regex-based analysis (fallback or additional)
|
|
186
|
+
if (this.config.fallbackToRegex || this.config.regexBasedOnly) {
|
|
187
|
+
try {
|
|
188
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
189
|
+
console.log(`🔧 [S024] Trying regex-based analysis...`);
|
|
190
|
+
}
|
|
191
|
+
const regexViolations = await this.regexAnalyzer.analyze(filePath);
|
|
192
|
+
|
|
193
|
+
// Add to violation map with deduplication
|
|
194
|
+
regexViolations.forEach((violation) => {
|
|
195
|
+
const key = `${violation.line}:${violation.column}:${violation.message}`;
|
|
196
|
+
if (!violationMap.has(key)) {
|
|
197
|
+
violationMap.set(key, violation);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
202
|
+
console.log(
|
|
203
|
+
`🔧 [S024] Regex analysis completed: ${regexViolations.length} violations`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.warn(`⚠ [S024] Regex analysis failed:`, error.message);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Convert Map values to array and add filePath to each violation
|
|
212
|
+
const finalViolations = Array.from(violationMap.values()).map(
|
|
213
|
+
(violation) => ({
|
|
214
|
+
...violation,
|
|
215
|
+
filePath: filePath,
|
|
216
|
+
file: filePath, // Also add 'file' for compatibility
|
|
217
|
+
})
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
221
|
+
console.log(
|
|
222
|
+
`🔧 [S024] File analysis completed: ${finalViolations.length} unique violations`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return finalViolations;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Clean up resources
|
|
231
|
+
*/
|
|
232
|
+
cleanup() {
|
|
233
|
+
if (this.symbolAnalyzer?.cleanup) {
|
|
234
|
+
this.symbolAnalyzer.cleanup();
|
|
235
|
+
}
|
|
236
|
+
if (this.regexAnalyzer?.cleanup) {
|
|
237
|
+
this.regexAnalyzer.cleanup();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = S024Analyzer;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
{
|
|
2
|
+
"rule": {
|
|
3
|
+
"id": "S024",
|
|
4
|
+
"name": "Protect against XPath Injection and XML External Entity (XXE)",
|
|
5
|
+
"description": "Protect against XPath Injection and XML External Entity (XXE) attacks. XPath injection occurs when user input is used to construct XPath queries without proper sanitization. XXE attacks exploit XML parsers that process external entities, potentially leading to data disclosure, server-side request forgery, or denial of service.",
|
|
6
|
+
"category": "security",
|
|
7
|
+
"severity": "error",
|
|
8
|
+
"languages": ["typescript", "javascript"],
|
|
9
|
+
"frameworks": ["express", "nestjs", "node"],
|
|
10
|
+
"version": "1.0.0",
|
|
11
|
+
"status": "stable",
|
|
12
|
+
"tags": ["security", "xpath", "xxe", "xml", "injection", "owasp"],
|
|
13
|
+
"references": [
|
|
14
|
+
"https://owasp.org/www-community/attacks/XPATH_Injection",
|
|
15
|
+
"https://owasp.org/www-community/vulnerabilities/XML_External_Entity_(XXE)_Processing",
|
|
16
|
+
"https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html",
|
|
17
|
+
"https://portswigger.net/web-security/xpath-injection",
|
|
18
|
+
"https://portswigger.net/web-security/xxe"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"configuration": {
|
|
22
|
+
"enableXPathInjectionDetection": true,
|
|
23
|
+
"enableXXEDetection": true,
|
|
24
|
+
"checkUserInputSources": [
|
|
25
|
+
"req",
|
|
26
|
+
"request",
|
|
27
|
+
"params",
|
|
28
|
+
"query",
|
|
29
|
+
"body",
|
|
30
|
+
"headers",
|
|
31
|
+
"cookies"
|
|
32
|
+
],
|
|
33
|
+
"vulnerableXPathMethods": [
|
|
34
|
+
"evaluate",
|
|
35
|
+
"select",
|
|
36
|
+
"selectText",
|
|
37
|
+
"selectValue",
|
|
38
|
+
"selectNodes",
|
|
39
|
+
"selectSingleNode"
|
|
40
|
+
],
|
|
41
|
+
"vulnerableXMLMethods": [
|
|
42
|
+
"parseString",
|
|
43
|
+
"parseXml",
|
|
44
|
+
"parseFromString",
|
|
45
|
+
"parse",
|
|
46
|
+
"parseXmlString",
|
|
47
|
+
"transform"
|
|
48
|
+
],
|
|
49
|
+
"vulnerableXMLConstructors": [
|
|
50
|
+
"DOMParser",
|
|
51
|
+
"XSLTProcessor",
|
|
52
|
+
"SAXParser"
|
|
53
|
+
],
|
|
54
|
+
"vulnerableXMLLibraries": [
|
|
55
|
+
"xml2js",
|
|
56
|
+
"libxmljs",
|
|
57
|
+
"xmldom",
|
|
58
|
+
"fast-xml-parser",
|
|
59
|
+
"node-xml2js",
|
|
60
|
+
"xml-parser",
|
|
61
|
+
"xmldoc",
|
|
62
|
+
"xpath"
|
|
63
|
+
],
|
|
64
|
+
"xxeProtectionPatterns": [
|
|
65
|
+
"resolveExternalEntities\\s*:\\s*false",
|
|
66
|
+
"setFeature.*disallow-doctype-decl.*true",
|
|
67
|
+
"setFeature.*external-general-entities.*false",
|
|
68
|
+
"setFeature.*external-parameter-entities.*false",
|
|
69
|
+
"explicitChildren\\s*:\\s*false",
|
|
70
|
+
"ignoreAttrs\\s*:\\s*true",
|
|
71
|
+
"parseDoctype\\s*:\\s*false"
|
|
72
|
+
],
|
|
73
|
+
"secureXPathPatterns": [
|
|
74
|
+
"parameterized",
|
|
75
|
+
"escaped",
|
|
76
|
+
"sanitized",
|
|
77
|
+
"validate"
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
"examples": {
|
|
81
|
+
"violations": [
|
|
82
|
+
{
|
|
83
|
+
"description": "XPath injection via direct user input",
|
|
84
|
+
"code": "const result = xpath.evaluate(req.query.expression, doc);"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"description": "XPath injection via string concatenation",
|
|
88
|
+
"code": "const query = \"//user[@name='\" + req.body.username + \"']\";\nconst result = xpath.select(query, doc);"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"description": "XXE vulnerability in XML parsing",
|
|
92
|
+
"code": "const parser = new DOMParser();\nconst doc = parser.parseFromString(req.body.xml, 'text/xml');"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"description": "XXE vulnerability with xml2js",
|
|
96
|
+
"code": "xml2js.parseString(req.body.xml, (err, result) => { /* ... */ });"
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
"fixes": [
|
|
100
|
+
{
|
|
101
|
+
"description": "Use parameterized XPath with validation",
|
|
102
|
+
"code": "const sanitizedInput = sanitize(req.query.expression);\nconst result = xpath.evaluate(sanitizedInput, doc);"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"description": "Disable external entities in XML parsing",
|
|
106
|
+
"code": "const parser = new DOMParser();\nparser.setFeature('http://apache.org/xml/features/disallow-doctype-decl', true);\nparser.setFeature('http://xml.org/sax/features/external-general-entities', false);\nparser.setFeature('http://xml.org/sax/features/external-parameter-entities', false);"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"description": "Secure xml2js configuration",
|
|
110
|
+
"code": "xml2js.parseString(req.body.xml, {\n explicitChildren: false,\n ignoreAttrs: true\n}, (err, result) => { /* ... */ });"
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
"testing": {
|
|
115
|
+
"testCases": [
|
|
116
|
+
{
|
|
117
|
+
"name": "xpath_injection_direct_input",
|
|
118
|
+
"type": "violation",
|
|
119
|
+
"description": "Direct user input in XPath query"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"name": "xpath_injection_concatenation",
|
|
123
|
+
"type": "violation",
|
|
124
|
+
"description": "String concatenation with user input"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"name": "xxe_domparser",
|
|
128
|
+
"type": "violation",
|
|
129
|
+
"description": "DOMParser without XXE protection"
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"name": "xxe_xml2js",
|
|
133
|
+
"type": "violation",
|
|
134
|
+
"description": "xml2js without XXE protection"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"name": "secure_xpath_parameterized",
|
|
138
|
+
"type": "clean",
|
|
139
|
+
"description": "Parameterized XPath query"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"name": "secure_xml_xxe_protected",
|
|
143
|
+
"type": "clean",
|
|
144
|
+
"description": "XML parsing with XXE protection"
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
},
|
|
148
|
+
"performance": {
|
|
149
|
+
"complexity": "O(n)",
|
|
150
|
+
"description": "Linear complexity based on number of function calls and expressions in the source code"
|
|
151
|
+
}
|
|
152
|
+
}
|