@sun-asterisk/sunlint 1.3.5 → 1.3.7
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 +67 -0
- package/config/rule-analysis-strategies.js +5 -0
- package/config/rules/enhanced-rules-registry.json +65 -10
- package/core/analysis-orchestrator.js +9 -5
- package/core/performance-optimizer.js +8 -2
- package/package.json +1 -1
- package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +78 -44
- package/rules/common/C070_no_real_time_tests/analyzer.js +320 -0
- package/rules/common/C070_no_real_time_tests/config.json +78 -0
- package/rules/common/C070_no_real_time_tests/regex-analyzer.js +424 -0
- package/rules/common/C073_validate_required_config_on_startup/README.md +110 -0
- package/rules/common/C073_validate_required_config_on_startup/analyzer.js +770 -0
- package/rules/common/C073_validate_required_config_on_startup/config.json +46 -0
- package/rules/common/C073_validate_required_config_on_startup/symbol-based-analyzer.js +370 -0
- package/rules/security/S057_utc_logging/README.md +152 -0
- package/rules/security/S057_utc_logging/analyzer.js +457 -0
- package/rules/security/S057_utc_logging/config.json +105 -0
- package/rules/security/S058_no_ssrf/README.md +180 -0
- package/rules/security/S058_no_ssrf/analyzer.js +403 -0
- package/rules/security/S058_no_ssrf/config.json +125 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Rule S058: No SSRF (Server-Side Request Forgery)
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
Rule S058 prevents Server-Side Request Forgery (SSRF) attacks by detecting and flagging HTTP requests that use user-controlled URLs without proper validation.
|
|
5
|
+
|
|
6
|
+
## What is SSRF?
|
|
7
|
+
SSRF allows attackers to make HTTP requests from the server to arbitrary destinations, potentially accessing:
|
|
8
|
+
- Internal services (databases, admin panels)
|
|
9
|
+
- Cloud metadata endpoints (AWS, GCP, Azure)
|
|
10
|
+
- Local files via file:// protocol
|
|
11
|
+
- Internal network resources
|
|
12
|
+
|
|
13
|
+
## Detection Strategy
|
|
14
|
+
|
|
15
|
+
### 1. HTTP Client Detection
|
|
16
|
+
Detects calls to HTTP libraries:
|
|
17
|
+
```typescript
|
|
18
|
+
// Detected patterns
|
|
19
|
+
fetch(url)
|
|
20
|
+
axios.get(url)
|
|
21
|
+
http.request(url)
|
|
22
|
+
httpClient.post(url)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 2. URL Source Tracing
|
|
26
|
+
Traces URL variables back to their source:
|
|
27
|
+
```typescript
|
|
28
|
+
// ❌ User-controlled (DANGEROUS)
|
|
29
|
+
const url = req.query.targetUrl;
|
|
30
|
+
fetch(url);
|
|
31
|
+
|
|
32
|
+
// ✅ Hardcoded (SAFE)
|
|
33
|
+
const url = "https://api.trusted-service.com";
|
|
34
|
+
fetch(url);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 3. Validation Check
|
|
38
|
+
Verifies if URL validation exists:
|
|
39
|
+
```typescript
|
|
40
|
+
// ❌ No validation
|
|
41
|
+
const url = req.body.webhookUrl;
|
|
42
|
+
fetch(url);
|
|
43
|
+
|
|
44
|
+
// ✅ With validation
|
|
45
|
+
const url = req.body.webhookUrl;
|
|
46
|
+
validateUrlAllowList(url);
|
|
47
|
+
fetch(url);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Examples
|
|
51
|
+
|
|
52
|
+
### ❌ Violations
|
|
53
|
+
|
|
54
|
+
#### 1. User-controlled URL without validation
|
|
55
|
+
```typescript
|
|
56
|
+
app.post('/webhook', (req, res) => {
|
|
57
|
+
const webhookUrl = req.body.url;
|
|
58
|
+
// VIOLATION: User input directly used in HTTP request
|
|
59
|
+
fetch(webhookUrl);
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### 2. Dangerous hardcoded URLs
|
|
64
|
+
```typescript
|
|
65
|
+
// VIOLATION: Internal metadata endpoint
|
|
66
|
+
fetch('http://169.254.169.254/latest/meta-data/');
|
|
67
|
+
|
|
68
|
+
// VIOLATION: Local file access
|
|
69
|
+
fetch('file:///etc/passwd');
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### 3. Query parameter injection
|
|
73
|
+
```typescript
|
|
74
|
+
app.get('/proxy', (req, res) => {
|
|
75
|
+
const targetUrl = req.query.url;
|
|
76
|
+
// VIOLATION: No validation of target URL
|
|
77
|
+
axios.get(targetUrl);
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### ✅ Safe Patterns
|
|
82
|
+
|
|
83
|
+
#### 1. Allow-list validation
|
|
84
|
+
```typescript
|
|
85
|
+
const ALLOWED_DOMAINS = ['api.trusted.com', 'webhook.company.com'];
|
|
86
|
+
|
|
87
|
+
function validateUrlAllowList(url) {
|
|
88
|
+
const parsed = new URL(url);
|
|
89
|
+
if (!ALLOWED_DOMAINS.includes(parsed.hostname)) {
|
|
90
|
+
throw new Error('Domain not allowed');
|
|
91
|
+
}
|
|
92
|
+
if (parsed.protocol !== 'https:') {
|
|
93
|
+
throw new Error('Only HTTPS allowed');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
app.post('/webhook', (req, res) => {
|
|
98
|
+
const webhookUrl = req.body.url;
|
|
99
|
+
validateUrlAllowList(webhookUrl); // ✅ Validated
|
|
100
|
+
fetch(webhookUrl);
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### 2. Hardcoded trusted URLs
|
|
105
|
+
```typescript
|
|
106
|
+
// ✅ Safe: Hardcoded trusted domain
|
|
107
|
+
const API_BASE = 'https://api.trusted-service.com';
|
|
108
|
+
fetch(\`\${API_BASE}/users\`);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### 3. Configuration-based URLs
|
|
112
|
+
```typescript
|
|
113
|
+
// ✅ Safe: From config, not user input
|
|
114
|
+
const externalApiUrl = process.env.EXTERNAL_API_URL;
|
|
115
|
+
fetch(externalApiUrl);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Configuration
|
|
119
|
+
|
|
120
|
+
### Blocked Elements
|
|
121
|
+
- **Protocols**: `file://`, `ftp://`, `ldap://`, etc.
|
|
122
|
+
- **IPs**: `127.0.0.1`, `169.254.169.254`, private ranges
|
|
123
|
+
- **Ports**: `22`, `3306`, `6379`, `5432`, etc.
|
|
124
|
+
|
|
125
|
+
### Detection Patterns
|
|
126
|
+
- **HTTP Clients**: `fetch`, `axios`, `http`, `request`, etc.
|
|
127
|
+
- **User Input**: `req.body`, `req.query`, `ctx.request`, etc.
|
|
128
|
+
- **Validation Functions**: `validateUrl`, `isAllowedUrl`, etc.
|
|
129
|
+
|
|
130
|
+
## Best Practices
|
|
131
|
+
|
|
132
|
+
### 1. Use Allow-lists
|
|
133
|
+
```typescript
|
|
134
|
+
const ALLOWED_HOSTS = [
|
|
135
|
+
'api.company.com',
|
|
136
|
+
'webhook.trusted-partner.com'
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
function isAllowedUrl(url) {
|
|
140
|
+
return ALLOWED_HOSTS.some(host => url.includes(host));
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 2. Protocol Restriction
|
|
145
|
+
```typescript
|
|
146
|
+
function validateUrl(url) {
|
|
147
|
+
const parsed = new URL(url);
|
|
148
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
149
|
+
throw new Error('Invalid protocol');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 3. Timeout & Redirect Limits
|
|
155
|
+
```typescript
|
|
156
|
+
const options = {
|
|
157
|
+
timeout: 5000,
|
|
158
|
+
maxRedirects: 0, // Prevent redirect attacks
|
|
159
|
+
headers: { 'User-Agent': 'MyApp/1.0' }
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
fetch(validatedUrl, options);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Testing
|
|
166
|
+
|
|
167
|
+
Run S058 on your codebase:
|
|
168
|
+
```bash
|
|
169
|
+
# Test single file
|
|
170
|
+
node cli.js --input=src/webhook.ts --rule=S058 --engine=heuristic
|
|
171
|
+
|
|
172
|
+
# Test entire project
|
|
173
|
+
node cli.js --input=src --rule=S058 --engine=heuristic
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Related Security Rules
|
|
177
|
+
- **S001**: SQL Injection Prevention
|
|
178
|
+
- **S002**: XSS Prevention
|
|
179
|
+
- **S057**: Input Validation
|
|
180
|
+
- **S059**: Path Traversal Prevention
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
// rules/security/S058_no_ssrf/analyzer.js
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { CommentDetector } = require('../../utils/rule-helpers');
|
|
5
|
+
|
|
6
|
+
class S058SSRFAnalyzer {
|
|
7
|
+
constructor(semanticEngine = null) {
|
|
8
|
+
this.ruleId = 'S058';
|
|
9
|
+
this.ruleName = 'No SSRF (Server-Side Request Forgery)';
|
|
10
|
+
this.description = 'S058 - Prevent SSRF attacks by validating URLs from user input before making HTTP requests';
|
|
11
|
+
this.semanticEngine = semanticEngine;
|
|
12
|
+
this.verbose = false;
|
|
13
|
+
|
|
14
|
+
// Load config from config.json
|
|
15
|
+
this.loadConfig();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
loadConfig() {
|
|
19
|
+
try {
|
|
20
|
+
const configPath = path.join(__dirname, 'config.json');
|
|
21
|
+
const configData = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
22
|
+
this.options = configData.options || {};
|
|
23
|
+
this.httpClientPatterns = this.options.httpClientPatterns || [];
|
|
24
|
+
this.userInputSources = this.options.userInputSources || [];
|
|
25
|
+
this.dangerousProtocols = this.options.dangerousProtocols || [];
|
|
26
|
+
this.blockedIPs = this.options.blockedIPs || [];
|
|
27
|
+
this.blockedPorts = this.options.blockedPorts || [];
|
|
28
|
+
this.allowedDomains = this.options.allowedDomains || [];
|
|
29
|
+
this.validationFunctions = this.options.validationFunctions || [];
|
|
30
|
+
this.policy = this.options.policy || {};
|
|
31
|
+
this.thresholds = this.options.thresholds || {};
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.warn(`[S058] Could not load config: ${error.message}`);
|
|
34
|
+
this.options = {};
|
|
35
|
+
this.httpClientPatterns = [];
|
|
36
|
+
this.userInputSources = [];
|
|
37
|
+
this.dangerousProtocols = [];
|
|
38
|
+
this.blockedIPs = [];
|
|
39
|
+
this.blockedPorts = [];
|
|
40
|
+
this.allowedDomains = [];
|
|
41
|
+
this.validationFunctions = [];
|
|
42
|
+
this.policy = {};
|
|
43
|
+
this.thresholds = {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async initialize(semanticEngine = null) {
|
|
48
|
+
if (semanticEngine) {
|
|
49
|
+
this.semanticEngine = semanticEngine;
|
|
50
|
+
}
|
|
51
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Main analyze method required by heuristic engine
|
|
55
|
+
async analyze(files, language, options = {}) {
|
|
56
|
+
const violations = [];
|
|
57
|
+
|
|
58
|
+
if (this.verbose) {
|
|
59
|
+
console.log(`[DEBUG] 🎯 S058: Analyzing ${files.length} files for SSRF vulnerabilities`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const filePath of files) {
|
|
63
|
+
if (this.verbose) {
|
|
64
|
+
console.log(`[DEBUG] 🎯 S058: Analyzing ${filePath.split('/').pop()}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
69
|
+
const fileExtension = path.extname(filePath);
|
|
70
|
+
const fileViolations = this.analyzeFile(filePath, content, fileExtension);
|
|
71
|
+
violations.push(...fileViolations);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.warn(`[S058] Error analyzing ${filePath}: ${error.message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (this.verbose) {
|
|
78
|
+
console.log(`[DEBUG] 🎯 S058: Found ${violations.length} SSRF violations`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return violations;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
analyzeFile(filePath, content, fileExtension) {
|
|
85
|
+
const violations = [];
|
|
86
|
+
const detectedLanguage = this.detectLanguage(fileExtension);
|
|
87
|
+
|
|
88
|
+
if (!detectedLanguage) {
|
|
89
|
+
return violations;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Use semantic engine (AST) if available, fallback to heuristic
|
|
93
|
+
if (this.semanticEngine && typeof this.semanticEngine.parseCode === 'function') {
|
|
94
|
+
return this.analyzeWithAST(filePath, content, detectedLanguage);
|
|
95
|
+
} else {
|
|
96
|
+
return this.analyzeWithHeuristic(filePath, content, detectedLanguage);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
detectLanguage(fileExtension) {
|
|
101
|
+
const extensions = {
|
|
102
|
+
'.ts': 'typescript',
|
|
103
|
+
'.tsx': 'typescript',
|
|
104
|
+
'.js': 'javascript',
|
|
105
|
+
'.jsx': 'javascript',
|
|
106
|
+
'.mjs': 'javascript'
|
|
107
|
+
};
|
|
108
|
+
return extensions[fileExtension] || null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
analyzeWithHeuristic(filePath, content, language) {
|
|
112
|
+
const violations = [];
|
|
113
|
+
const lines = content.split('\n');
|
|
114
|
+
|
|
115
|
+
// Detect HTTP client calls and trace URLs
|
|
116
|
+
const httpCalls = this.detectHttpCalls(content, lines);
|
|
117
|
+
|
|
118
|
+
for (const call of httpCalls) {
|
|
119
|
+
const urlSource = this.traceUrlSource(call, content, lines);
|
|
120
|
+
|
|
121
|
+
if (urlSource.isUserControlled) {
|
|
122
|
+
// Check if URL is validated
|
|
123
|
+
const hasValidation = this.checkUrlValidation(call, content, lines);
|
|
124
|
+
|
|
125
|
+
if (!hasValidation) {
|
|
126
|
+
violations.push({
|
|
127
|
+
ruleId: this.ruleId,
|
|
128
|
+
message: `Potential SSRF vulnerability: HTTP request with user-controlled URL '${call.urlVariable}' without validation`,
|
|
129
|
+
severity: 'error',
|
|
130
|
+
line: call.line,
|
|
131
|
+
column: call.column,
|
|
132
|
+
filePath: filePath,
|
|
133
|
+
details: {
|
|
134
|
+
httpMethod: call.method,
|
|
135
|
+
urlVariable: call.urlVariable,
|
|
136
|
+
userInputSource: urlSource.source,
|
|
137
|
+
suggestion: `Add URL validation using: ${this.validationFunctions[0] || 'validateUrlAllowList'}(${call.urlVariable})`
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check for hardcoded dangerous URLs
|
|
144
|
+
if (call.isHardcoded) {
|
|
145
|
+
const dangerousUrl = this.checkDangerousUrl(call.url);
|
|
146
|
+
if (dangerousUrl.isDangerous) {
|
|
147
|
+
violations.push({
|
|
148
|
+
ruleId: this.ruleId,
|
|
149
|
+
message: `Dangerous URL detected: ${dangerousUrl.reason}`,
|
|
150
|
+
severity: 'error',
|
|
151
|
+
line: call.line,
|
|
152
|
+
column: call.column,
|
|
153
|
+
filePath: filePath,
|
|
154
|
+
details: {
|
|
155
|
+
url: call.url,
|
|
156
|
+
reason: dangerousUrl.reason,
|
|
157
|
+
suggestion: 'Remove dangerous URL or add to allow-list if legitimate'
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return violations;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
detectHttpCalls(content, lines) {
|
|
168
|
+
const calls = [];
|
|
169
|
+
|
|
170
|
+
if (this.verbose) {
|
|
171
|
+
console.log(`[DEBUG] 🔍 S058: Detecting HTTP calls in ${lines.length} lines`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const pattern of this.httpClientPatterns) {
|
|
175
|
+
const regex = new RegExp(pattern, 'gi');
|
|
176
|
+
let match;
|
|
177
|
+
|
|
178
|
+
while ((match = regex.exec(content)) !== null) {
|
|
179
|
+
const lineNumber = content.substring(0, match.index).split('\n').length;
|
|
180
|
+
const line = lines[lineNumber - 1];
|
|
181
|
+
const columnPosition = match.index - content.lastIndexOf('\n', match.index - 1) - 1;
|
|
182
|
+
|
|
183
|
+
// ✅ CHECK: Skip if this code is in comments
|
|
184
|
+
const isInComment = CommentDetector.isLineInBlockComment(lines, lineNumber - 1) ||
|
|
185
|
+
CommentDetector.isPositionInComment(line, columnPosition);
|
|
186
|
+
|
|
187
|
+
if (isInComment) {
|
|
188
|
+
if (this.verbose) {
|
|
189
|
+
console.log(`[DEBUG] 🔍 S058: SKIPPING comment at line ${lineNumber}: ${line.trim()}`);
|
|
190
|
+
}
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (this.verbose) {
|
|
195
|
+
console.log(`[DEBUG] 🔍 S058: Found HTTP call pattern "${pattern}" at line ${lineNumber}: ${line.trim()}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Extract URL parameter
|
|
199
|
+
const urlMatch = this.extractUrlFromCall(line, match[0]);
|
|
200
|
+
|
|
201
|
+
if (urlMatch) {
|
|
202
|
+
calls.push({
|
|
203
|
+
method: match[0],
|
|
204
|
+
line: lineNumber,
|
|
205
|
+
column: columnPosition,
|
|
206
|
+
urlVariable: urlMatch.variable,
|
|
207
|
+
url: urlMatch.value,
|
|
208
|
+
isHardcoded: urlMatch.isHardcoded,
|
|
209
|
+
fullCall: line.trim()
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
if (this.verbose) {
|
|
213
|
+
console.log(`[DEBUG] 🔍 S058: Extracted URL: ${urlMatch.variable} (hardcoded: ${urlMatch.isHardcoded})`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (this.verbose) {
|
|
220
|
+
console.log(`[DEBUG] 🔍 S058: Found ${calls.length} HTTP calls total (after comment filtering)`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return calls;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
extractUrlFromCall(line, methodCall) {
|
|
227
|
+
// Extract URL from HTTP call - simplified regex approach
|
|
228
|
+
const patterns = [
|
|
229
|
+
// fetch(url), axios.get(url)
|
|
230
|
+
/(?:fetch|\.(?:get|post|put|delete|patch|request))\s*\(\s*([^,\)]+)/,
|
|
231
|
+
// More complex patterns can be added
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
for (const pattern of patterns) {
|
|
235
|
+
const match = line.match(pattern);
|
|
236
|
+
if (match) {
|
|
237
|
+
const urlParam = match[1].trim();
|
|
238
|
+
|
|
239
|
+
// Check if it's a string literal
|
|
240
|
+
if (urlParam.startsWith('"') || urlParam.startsWith("'") || urlParam.startsWith('`')) {
|
|
241
|
+
return {
|
|
242
|
+
variable: urlParam,
|
|
243
|
+
value: urlParam.slice(1, -1), // Remove quotes
|
|
244
|
+
isHardcoded: true
|
|
245
|
+
};
|
|
246
|
+
} else {
|
|
247
|
+
return {
|
|
248
|
+
variable: urlParam,
|
|
249
|
+
value: null,
|
|
250
|
+
isHardcoded: false
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
traceUrlSource(call, content, lines) {
|
|
260
|
+
if (call.isHardcoded) {
|
|
261
|
+
return { isUserControlled: false, source: 'hardcoded' };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Simple variable tracing
|
|
265
|
+
const variable = this.escapeRegex(call.urlVariable);
|
|
266
|
+
|
|
267
|
+
// Check if variable comes from user input
|
|
268
|
+
for (const inputPattern of this.userInputSources) {
|
|
269
|
+
// userInputSources patterns are already regex-escaped in config.json
|
|
270
|
+
const escapedPattern = inputPattern;
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
// Check direct assignment: const url = req.body.url
|
|
274
|
+
const assignmentRegex = new RegExp(`const\\s+${variable}\\s*=\\s*${escapedPattern}`, 'i');
|
|
275
|
+
if (assignmentRegex.test(content)) {
|
|
276
|
+
return {
|
|
277
|
+
isUserControlled: true,
|
|
278
|
+
source: inputPattern
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Check let assignment: let url = req.query.endpoint
|
|
283
|
+
const letAssignmentRegex = new RegExp(`let\\s+${variable}\\s*=\\s*${escapedPattern}`, 'i');
|
|
284
|
+
if (letAssignmentRegex.test(content)) {
|
|
285
|
+
return {
|
|
286
|
+
isUserControlled: true,
|
|
287
|
+
source: inputPattern
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Check var assignment: var url = req.params.id
|
|
292
|
+
const varAssignmentRegex = new RegExp(`var\\s+${variable}\\s*=\\s*${escapedPattern}`, 'i');
|
|
293
|
+
if (varAssignmentRegex.test(content)) {
|
|
294
|
+
return {
|
|
295
|
+
isUserControlled: true,
|
|
296
|
+
source: inputPattern
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Check property access: url = req.body.path
|
|
301
|
+
const propertyRegex = new RegExp(`${variable}\\s*=\\s*${escapedPattern}`, 'i');
|
|
302
|
+
if (propertyRegex.test(content)) {
|
|
303
|
+
return {
|
|
304
|
+
isUserControlled: true,
|
|
305
|
+
source: inputPattern
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Also check reverse assignment patterns
|
|
310
|
+
const reverseRegex = new RegExp(`${escapedPattern}.*${variable}`, 'i');
|
|
311
|
+
if (reverseRegex.test(content)) {
|
|
312
|
+
return {
|
|
313
|
+
isUserControlled: true,
|
|
314
|
+
source: inputPattern
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
} catch (e) {
|
|
318
|
+
// Skip invalid regex patterns
|
|
319
|
+
if (this.verbose) {
|
|
320
|
+
console.log(`[DEBUG] S058: Skipping invalid regex pattern for ${inputPattern}: ${e.message}`);
|
|
321
|
+
}
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return { isUserControlled: false, source: 'unknown' };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
escapeRegex(string) {
|
|
330
|
+
// Escape special regex characters
|
|
331
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
checkUrlValidation(call, content, lines) {
|
|
335
|
+
// Check if URL validation function is called before HTTP request
|
|
336
|
+
const beforeCallContent = content.substring(0, content.indexOf(call.fullCall));
|
|
337
|
+
|
|
338
|
+
for (const validationFn of this.validationFunctions) {
|
|
339
|
+
try {
|
|
340
|
+
const escapedVariable = this.escapeRegex(call.urlVariable);
|
|
341
|
+
const escapedFunction = this.escapeRegex(validationFn);
|
|
342
|
+
const regex = new RegExp(`${escapedFunction}\\s*\\(.*${escapedVariable}`, 'i');
|
|
343
|
+
if (regex.test(beforeCallContent)) {
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
} catch (e) {
|
|
347
|
+
// Skip invalid regex patterns
|
|
348
|
+
if (this.verbose) {
|
|
349
|
+
console.log(`[DEBUG] S058: Skipping invalid validation regex for ${validationFn}: ${e.message}`);
|
|
350
|
+
}
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
checkDangerousUrl(url) {
|
|
359
|
+
if (!url) return { isDangerous: false };
|
|
360
|
+
|
|
361
|
+
// Check dangerous protocols
|
|
362
|
+
for (const protocol of this.dangerousProtocols) {
|
|
363
|
+
if (url.toLowerCase().includes(protocol)) {
|
|
364
|
+
return {
|
|
365
|
+
isDangerous: true,
|
|
366
|
+
reason: `Dangerous protocol: ${protocol}`
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Check blocked IPs
|
|
372
|
+
for (const ipPattern of this.blockedIPs) {
|
|
373
|
+
const regex = new RegExp(ipPattern, 'i');
|
|
374
|
+
if (regex.test(url)) {
|
|
375
|
+
return {
|
|
376
|
+
isDangerous: true,
|
|
377
|
+
reason: `Blocked IP range: ${ipPattern}`
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Check blocked ports
|
|
383
|
+
for (const port of this.blockedPorts) {
|
|
384
|
+
const portRegex = new RegExp(`:${port}(?:/|$)`, 'i');
|
|
385
|
+
if (portRegex.test(url)) {
|
|
386
|
+
return {
|
|
387
|
+
isDangerous: true,
|
|
388
|
+
reason: `Blocked port: ${port}`
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return { isDangerous: false };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
analyzeWithAST(filePath, content, language) {
|
|
397
|
+
// Enhanced AST-based analysis for more precise detection
|
|
398
|
+
// This would use the semantic engine for deeper analysis
|
|
399
|
+
return this.analyzeWithHeuristic(filePath, content, language);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
module.exports = S058SSRFAnalyzer;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ruleId": "S058",
|
|
3
|
+
"name": "No SSRF (Server-Side Request Forgery)",
|
|
4
|
+
"description": "Prevent SSRF attacks by validating URLs from user input before making HTTP requests",
|
|
5
|
+
"category": "security",
|
|
6
|
+
"severity": "error",
|
|
7
|
+
"options": {
|
|
8
|
+
"httpClientPatterns": [
|
|
9
|
+
"fetch\\s*\\(",
|
|
10
|
+
"axios\\.(?:get|post|put|delete|patch|request)\\s*\\(",
|
|
11
|
+
"http\\.(?:get|post|put|delete|patch|request)\\s*\\(",
|
|
12
|
+
"https\\.(?:get|post|put|delete|patch|request)\\s*\\(",
|
|
13
|
+
"(?:^|\\s|=|\\()request\\s*\\(",
|
|
14
|
+
"got\\s*\\(",
|
|
15
|
+
"superagent\\.",
|
|
16
|
+
"needle\\.",
|
|
17
|
+
"bent\\(",
|
|
18
|
+
"node-fetch\\s*\\(",
|
|
19
|
+
"isomorphic-fetch\\s*\\(",
|
|
20
|
+
"ky\\s*\\(",
|
|
21
|
+
"httpClient\\.",
|
|
22
|
+
"\\.httpClient\\."
|
|
23
|
+
],
|
|
24
|
+
"userInputSources": [
|
|
25
|
+
"req\\.body",
|
|
26
|
+
"req\\.query",
|
|
27
|
+
"req\\.params",
|
|
28
|
+
"request\\.body",
|
|
29
|
+
"request\\.query",
|
|
30
|
+
"request\\.params",
|
|
31
|
+
"ctx\\.request\\.body",
|
|
32
|
+
"ctx\\.query",
|
|
33
|
+
"ctx\\.params",
|
|
34
|
+
"event\\.body",
|
|
35
|
+
"event\\.queryStringParameters",
|
|
36
|
+
"event\\.pathParameters",
|
|
37
|
+
"\\.query\\.",
|
|
38
|
+
"\\.body\\.",
|
|
39
|
+
"\\.params\\.",
|
|
40
|
+
"process\\.argv",
|
|
41
|
+
"process\\.env\\.",
|
|
42
|
+
"from.*request",
|
|
43
|
+
"from.*input",
|
|
44
|
+
"user.*input",
|
|
45
|
+
"client.*data",
|
|
46
|
+
"external.*data"
|
|
47
|
+
],
|
|
48
|
+
"dangerousProtocols": [
|
|
49
|
+
"file://",
|
|
50
|
+
"ftp://",
|
|
51
|
+
"sftp://",
|
|
52
|
+
"ldap://",
|
|
53
|
+
"ldaps://",
|
|
54
|
+
"dict://",
|
|
55
|
+
"gopher://",
|
|
56
|
+
"jar://",
|
|
57
|
+
"netdoc://",
|
|
58
|
+
"mailto:",
|
|
59
|
+
"news:",
|
|
60
|
+
"imap://",
|
|
61
|
+
"pop3://",
|
|
62
|
+
"smb://",
|
|
63
|
+
"afp://",
|
|
64
|
+
"telnet://",
|
|
65
|
+
"ssh://"
|
|
66
|
+
],
|
|
67
|
+
"blockedIPs": [
|
|
68
|
+
"127\\.0\\.0\\.1",
|
|
69
|
+
"::1",
|
|
70
|
+
"localhost",
|
|
71
|
+
"169\\.254\\.169\\.254",
|
|
72
|
+
"metadata\\.google\\.internal",
|
|
73
|
+
"169\\.254\\.",
|
|
74
|
+
"10\\.",
|
|
75
|
+
"172\\.(1[6-9]|2[0-9]|3[01])\\.",
|
|
76
|
+
"192\\.168\\."
|
|
77
|
+
],
|
|
78
|
+
"blockedPorts": [
|
|
79
|
+
"22",
|
|
80
|
+
"23",
|
|
81
|
+
"25",
|
|
82
|
+
"53",
|
|
83
|
+
"135",
|
|
84
|
+
"139",
|
|
85
|
+
"445",
|
|
86
|
+
"1433",
|
|
87
|
+
"1521",
|
|
88
|
+
"3306",
|
|
89
|
+
"3389",
|
|
90
|
+
"5432",
|
|
91
|
+
"5984",
|
|
92
|
+
"6379",
|
|
93
|
+
"8080",
|
|
94
|
+
"9200",
|
|
95
|
+
"11211",
|
|
96
|
+
"27017",
|
|
97
|
+
"50070"
|
|
98
|
+
],
|
|
99
|
+
"allowedDomains": [
|
|
100
|
+
"api\\.trusted-service\\.com",
|
|
101
|
+
"service\\.company\\.com"
|
|
102
|
+
],
|
|
103
|
+
"validationFunctions": [
|
|
104
|
+
"validateUrl",
|
|
105
|
+
"validateUrlAllowList",
|
|
106
|
+
"checkAllowedUrl",
|
|
107
|
+
"isAllowedUrl",
|
|
108
|
+
"sanitizeUrl",
|
|
109
|
+
"verifyUrl",
|
|
110
|
+
"urlValidator"
|
|
111
|
+
],
|
|
112
|
+
"policy": {
|
|
113
|
+
"requireExplicitValidation": true,
|
|
114
|
+
"enforceAllowList": true,
|
|
115
|
+
"blockPrivateIPs": true,
|
|
116
|
+
"checkProtocols": true,
|
|
117
|
+
"requireHttpsOnly": false,
|
|
118
|
+
"maxRedirects": 0
|
|
119
|
+
},
|
|
120
|
+
"thresholds": {
|
|
121
|
+
"maxSuspiciousUrls": 3,
|
|
122
|
+
"maxUnvalidatedRequests": 1
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|