openpen 0.2.0
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/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/checks/auth-bypass.d.ts +12 -0
- package/dist/checks/auth-bypass.js +93 -0
- package/dist/checks/bac.d.ts +12 -0
- package/dist/checks/bac.js +107 -0
- package/dist/checks/base.d.ts +22 -0
- package/dist/checks/base.js +13 -0
- package/dist/checks/index.d.ts +7 -0
- package/dist/checks/index.js +40 -0
- package/dist/checks/llm-leak.d.ts +23 -0
- package/dist/checks/llm-leak.js +251 -0
- package/dist/checks/mass-assignment.d.ts +12 -0
- package/dist/checks/mass-assignment.js +169 -0
- package/dist/checks/prompt-injection.d.ts +23 -0
- package/dist/checks/prompt-injection.js +262 -0
- package/dist/checks/security-headers.d.ts +12 -0
- package/dist/checks/security-headers.js +133 -0
- package/dist/checks/sensitive-data.d.ts +12 -0
- package/dist/checks/sensitive-data.js +122 -0
- package/dist/checks/sqli.d.ts +12 -0
- package/dist/checks/sqli.js +178 -0
- package/dist/checks/ssrf.d.ts +12 -0
- package/dist/checks/ssrf.js +126 -0
- package/dist/checks/xss.d.ts +12 -0
- package/dist/checks/xss.js +79 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +300 -0
- package/dist/fuzzer/engine.d.ts +27 -0
- package/dist/fuzzer/engine.js +126 -0
- package/dist/fuzzer/mutator.d.ts +8 -0
- package/dist/fuzzer/mutator.js +54 -0
- package/dist/fuzzer/payloads.d.ts +13 -0
- package/dist/fuzzer/payloads.js +167 -0
- package/dist/reporter/index.d.ts +5 -0
- package/dist/reporter/index.js +5 -0
- package/dist/reporter/json.d.ts +5 -0
- package/dist/reporter/json.js +14 -0
- package/dist/reporter/terminal.d.ts +5 -0
- package/dist/reporter/terminal.js +59 -0
- package/dist/spec/openapi.d.ts +5 -0
- package/dist/spec/openapi.js +119 -0
- package/dist/spec/parser.d.ts +11 -0
- package/dist/spec/parser.js +45 -0
- package/dist/types.d.ts +145 -0
- package/dist/types.js +4 -0
- package/dist/utils/http.d.ts +37 -0
- package/dist/utils/http.js +92 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.js +20 -0
- package/dist/ws/checks.d.ts +18 -0
- package/dist/ws/checks.js +558 -0
- package/dist/ws/engine.d.ts +47 -0
- package/dist/ws/engine.js +139 -0
- package/package.json +41 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sensitive Data Exposure Check (A02:2021 - Cryptographic Failures)
|
|
3
|
+
*/
|
|
4
|
+
import { BaseCheck } from './base.js';
|
|
5
|
+
import { sendRequest } from '../utils/http.js';
|
|
6
|
+
const SENSITIVE_PATTERNS = [
|
|
7
|
+
{ name: 'email', pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, severity: 'low' },
|
|
8
|
+
{ name: 'jwt', pattern: /eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, severity: 'high' },
|
|
9
|
+
{ name: 'api_key', pattern: /(?:api[_-]?key|apikey|api_secret)['":\s]*['"]?([a-zA-Z0-9]{20,})/gi, severity: 'high' },
|
|
10
|
+
{ name: 'aws_key', pattern: /AKIA[0-9A-Z]{16}/g, severity: 'high' },
|
|
11
|
+
{ name: 'private_key', pattern: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/g, severity: 'high' },
|
|
12
|
+
{ name: 'password_field', pattern: /"password"\s*:\s*"[^"]+"/gi, severity: 'high' },
|
|
13
|
+
{ name: 'ssn', pattern: /\b\d{3}-\d{2}-\d{4}\b/g, severity: 'high' },
|
|
14
|
+
{ name: 'credit_card', pattern: /\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13})\b/g, severity: 'high' },
|
|
15
|
+
{ name: 'internal_ip', pattern: /\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})\b/g, severity: 'medium' },
|
|
16
|
+
{ name: 'stack_trace', pattern: /(?:at\s+[\w.$]+\s*\(.*:\d+:\d+\)|Traceback \(most recent|Exception in thread)/g, severity: 'medium' },
|
|
17
|
+
{ name: 'debug_info', pattern: /(?:DEBUG|TRACE|stack_trace|backtrace).*[:=]/gi, severity: 'low' },
|
|
18
|
+
];
|
|
19
|
+
const COMMON_LEAK_PATHS = [
|
|
20
|
+
'/.env',
|
|
21
|
+
'/config.json',
|
|
22
|
+
'/config.yaml',
|
|
23
|
+
'/.git/config',
|
|
24
|
+
'/debug',
|
|
25
|
+
'/actuator/env',
|
|
26
|
+
'/api/debug',
|
|
27
|
+
'/graphql',
|
|
28
|
+
'/__debug__',
|
|
29
|
+
'/server-status',
|
|
30
|
+
'/phpinfo.php',
|
|
31
|
+
'/swagger.json',
|
|
32
|
+
'/api-docs',
|
|
33
|
+
'/.well-known/security.txt',
|
|
34
|
+
];
|
|
35
|
+
export class SensitiveDataCheck extends BaseCheck {
|
|
36
|
+
id = 'sensitive-data';
|
|
37
|
+
name = 'Sensitive Data Exposure';
|
|
38
|
+
description = 'Check for leaked secrets, PII, and debug information in responses';
|
|
39
|
+
owaspCategory = 'A02:2021 Crypto Failures';
|
|
40
|
+
async run(target, config) {
|
|
41
|
+
const findings = [];
|
|
42
|
+
let requestCount = 0;
|
|
43
|
+
// Check endpoint responses for sensitive data
|
|
44
|
+
for (const ep of target.endpoints) {
|
|
45
|
+
const url = target.baseUrl + ep.path;
|
|
46
|
+
try {
|
|
47
|
+
const res = await sendRequest({
|
|
48
|
+
url,
|
|
49
|
+
method: ep.method,
|
|
50
|
+
headers: target.globalHeaders,
|
|
51
|
+
timeout: config.timeout,
|
|
52
|
+
});
|
|
53
|
+
requestCount++;
|
|
54
|
+
for (const sp of SENSITIVE_PATTERNS) {
|
|
55
|
+
const matches = res.response.bodySnippet.match(sp.pattern);
|
|
56
|
+
if (matches && matches.length > 0) {
|
|
57
|
+
// Don't flag emails in obvious contexts (like user profile endpoints)
|
|
58
|
+
if (sp.name === 'email' && matches.length < 3)
|
|
59
|
+
continue;
|
|
60
|
+
findings.push({
|
|
61
|
+
id: `sensitive-${sp.name}-${ep.method}-${ep.path}`,
|
|
62
|
+
checkId: this.id,
|
|
63
|
+
checkName: this.name,
|
|
64
|
+
severity: sp.severity,
|
|
65
|
+
endpoint: url,
|
|
66
|
+
method: ep.method,
|
|
67
|
+
evidence: `Found ${matches.length} ${sp.name} pattern(s): ${matches[0].slice(0, 50)}...`,
|
|
68
|
+
description: `Response from ${ep.method} ${ep.path} contains ${sp.name} data that may be sensitive.`,
|
|
69
|
+
remediation: 'Review response content. Mask or remove sensitive data. Use proper access controls.',
|
|
70
|
+
owaspCategory: this.owaspCategory,
|
|
71
|
+
request: res.request,
|
|
72
|
+
response: res.response,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// skip
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Probe common leak paths
|
|
82
|
+
const leakPaths = config.depth === 'shallow'
|
|
83
|
+
? COMMON_LEAK_PATHS.slice(0, 5)
|
|
84
|
+
: COMMON_LEAK_PATHS;
|
|
85
|
+
for (const path of leakPaths) {
|
|
86
|
+
const url = target.baseUrl + path;
|
|
87
|
+
try {
|
|
88
|
+
const res = await sendRequest({
|
|
89
|
+
url,
|
|
90
|
+
method: 'GET',
|
|
91
|
+
headers: target.globalHeaders,
|
|
92
|
+
timeout: config.timeout,
|
|
93
|
+
});
|
|
94
|
+
requestCount++;
|
|
95
|
+
if (res.response.statusCode === 200 && res.response.bodySnippet.length > 10) {
|
|
96
|
+
let severity = 'medium';
|
|
97
|
+
if (path.includes('.env') || path.includes('.git') || path.includes('config')) {
|
|
98
|
+
severity = 'high';
|
|
99
|
+
}
|
|
100
|
+
findings.push({
|
|
101
|
+
id: `sensitive-leak-path-${path}`,
|
|
102
|
+
checkId: this.id,
|
|
103
|
+
checkName: this.name,
|
|
104
|
+
severity,
|
|
105
|
+
endpoint: url,
|
|
106
|
+
method: 'GET',
|
|
107
|
+
evidence: `Path ${path} returned 200 with ${res.response.bodySnippet.length} bytes`,
|
|
108
|
+
description: `Sensitive path ${path} is accessible and returns content. This may expose configuration, debug info, or secrets.`,
|
|
109
|
+
remediation: `Block access to ${path}. Ensure sensitive files are not served by the web server.`,
|
|
110
|
+
owaspCategory: this.owaspCategory,
|
|
111
|
+
request: res.request,
|
|
112
|
+
response: res.response,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// skip
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { findings, requestCount };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL Injection Check (A03:2021 - Injection)
|
|
3
|
+
*/
|
|
4
|
+
import { BaseCheck, type CheckResult } from './base.js';
|
|
5
|
+
import type { ScanTarget, ScanConfig } from '../types.js';
|
|
6
|
+
export declare class SqlInjectionCheck extends BaseCheck {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
owaspCategory: string;
|
|
11
|
+
run(target: ScanTarget, config: ScanConfig): Promise<CheckResult>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL Injection Check (A03:2021 - Injection)
|
|
3
|
+
*/
|
|
4
|
+
import { BaseCheck } from './base.js';
|
|
5
|
+
import { sendRequest, Semaphore, RateLimiter } from '../utils/http.js';
|
|
6
|
+
import { SQLI_PAYLOADS } from '../fuzzer/payloads.js';
|
|
7
|
+
const SQL_ERROR_PATTERNS = [
|
|
8
|
+
/you have an error in your sql syntax/i,
|
|
9
|
+
/warning:.*mysql/i,
|
|
10
|
+
/unclosed quotation mark/i,
|
|
11
|
+
/quoted string not properly terminated/i,
|
|
12
|
+
/pg_query\(\)/i,
|
|
13
|
+
/postgresql/i,
|
|
14
|
+
/sqlite3?\.OperationalError/i,
|
|
15
|
+
/SQLite\/JDBCDriver/i,
|
|
16
|
+
/ORA-\d{5}/i,
|
|
17
|
+
/Microsoft OLE DB Provider for SQL Server/i,
|
|
18
|
+
/ODBC SQL Server Driver/i,
|
|
19
|
+
/SQL Server.*Driver/i,
|
|
20
|
+
/Warning.*mssql_/i,
|
|
21
|
+
/com\.mysql\.jdbc/i,
|
|
22
|
+
/org\.postgresql\.util\.PSQLException/i,
|
|
23
|
+
/Syntax error.*in query expression/i,
|
|
24
|
+
];
|
|
25
|
+
export class SqlInjectionCheck extends BaseCheck {
|
|
26
|
+
id = 'sqli';
|
|
27
|
+
name = 'SQL Injection';
|
|
28
|
+
description = 'Test for SQL injection vulnerabilities in parameters and request bodies';
|
|
29
|
+
owaspCategory = 'A03:2021 Injection';
|
|
30
|
+
async run(target, config) {
|
|
31
|
+
const findings = [];
|
|
32
|
+
let requestCount = 0;
|
|
33
|
+
const sem = new Semaphore(config.concurrency);
|
|
34
|
+
const rl = new RateLimiter(config.rateLimit);
|
|
35
|
+
const payloads = config.depth === 'shallow'
|
|
36
|
+
? SQLI_PAYLOADS.slice(0, 5)
|
|
37
|
+
: config.depth === 'deep'
|
|
38
|
+
? SQLI_PAYLOADS
|
|
39
|
+
: SQLI_PAYLOADS.slice(0, 12);
|
|
40
|
+
const tasks = [];
|
|
41
|
+
for (const ep of target.endpoints) {
|
|
42
|
+
const url = target.baseUrl + ep.path;
|
|
43
|
+
// Test query parameters
|
|
44
|
+
for (const param of ep.parameters.filter(p => p.in === 'query')) {
|
|
45
|
+
for (const payload of payloads) {
|
|
46
|
+
tasks.push(testPayload(url, ep.method, param.name, payload));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Test body fields
|
|
50
|
+
if (ep.requestBody?.fields) {
|
|
51
|
+
for (const [fieldName] of Object.entries(ep.requestBody.fields)) {
|
|
52
|
+
for (const payload of payloads) {
|
|
53
|
+
tasks.push(testBodyField(url, ep.method, fieldName, payload, ep.requestBody.fields));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Always test with a query param even if none defined
|
|
58
|
+
if (ep.parameters.filter(p => p.in === 'query').length === 0) {
|
|
59
|
+
for (const payload of payloads.slice(0, 3)) {
|
|
60
|
+
tasks.push(testPayload(url, ep.method, 'id', payload));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function testPayload(baseUrl, method, param, payload) {
|
|
65
|
+
await sem.acquire();
|
|
66
|
+
try {
|
|
67
|
+
await rl.wait();
|
|
68
|
+
const testUrl = `${baseUrl}?${encodeURIComponent(param)}=${encodeURIComponent(payload)}`;
|
|
69
|
+
const res = await sendRequest({
|
|
70
|
+
url: testUrl,
|
|
71
|
+
method: 'GET',
|
|
72
|
+
headers: target.globalHeaders,
|
|
73
|
+
timeout: config.timeout,
|
|
74
|
+
});
|
|
75
|
+
requestCount++;
|
|
76
|
+
const match = checkForSqlError(res.response.bodySnippet);
|
|
77
|
+
if (match) {
|
|
78
|
+
findings.push({
|
|
79
|
+
id: `sqli-${param}-${findings.length}`,
|
|
80
|
+
checkId: 'sqli',
|
|
81
|
+
checkName: 'SQL Injection',
|
|
82
|
+
severity: 'critical',
|
|
83
|
+
endpoint: baseUrl,
|
|
84
|
+
method: method,
|
|
85
|
+
parameter: param,
|
|
86
|
+
payload,
|
|
87
|
+
evidence: `SQL error detected: "${match}"`,
|
|
88
|
+
description: `Parameter "${param}" may be vulnerable to SQL injection. The server returned a database error in response to a SQL payload.`,
|
|
89
|
+
remediation: 'Use parameterized queries or prepared statements. Never concatenate user input into SQL.',
|
|
90
|
+
owaspCategory: 'A03:2021 Injection',
|
|
91
|
+
request: res.request,
|
|
92
|
+
response: res.response,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// Time-based detection
|
|
96
|
+
if (payload.includes('SLEEP') || payload.includes('WAITFOR')) {
|
|
97
|
+
if (res.response.responseTime > 4500) {
|
|
98
|
+
findings.push({
|
|
99
|
+
id: `sqli-time-${param}-${findings.length}`,
|
|
100
|
+
checkId: 'sqli',
|
|
101
|
+
checkName: 'SQL Injection (Time-based)',
|
|
102
|
+
severity: 'critical',
|
|
103
|
+
endpoint: baseUrl,
|
|
104
|
+
method: method,
|
|
105
|
+
parameter: param,
|
|
106
|
+
payload,
|
|
107
|
+
evidence: `Response took ${res.response.responseTime}ms (expected delay from payload)`,
|
|
108
|
+
description: `Parameter "${param}" appears vulnerable to time-based blind SQL injection.`,
|
|
109
|
+
remediation: 'Use parameterized queries or prepared statements.',
|
|
110
|
+
owaspCategory: 'A03:2021 Injection',
|
|
111
|
+
request: res.request,
|
|
112
|
+
response: res.response,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Request failed, skip
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
sem.release();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function testBodyField(baseUrl, method, field, payload, fields) {
|
|
125
|
+
await sem.acquire();
|
|
126
|
+
try {
|
|
127
|
+
await rl.wait();
|
|
128
|
+
const body = {};
|
|
129
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
130
|
+
body[k] = k === field ? payload : (v.example || 'test');
|
|
131
|
+
}
|
|
132
|
+
const res = await sendRequest({
|
|
133
|
+
url: baseUrl,
|
|
134
|
+
method,
|
|
135
|
+
headers: { ...target.globalHeaders, 'Content-Type': 'application/json' },
|
|
136
|
+
body: JSON.stringify(body),
|
|
137
|
+
timeout: config.timeout,
|
|
138
|
+
});
|
|
139
|
+
requestCount++;
|
|
140
|
+
const match = checkForSqlError(res.response.bodySnippet);
|
|
141
|
+
if (match) {
|
|
142
|
+
findings.push({
|
|
143
|
+
id: `sqli-body-${field}-${findings.length}`,
|
|
144
|
+
checkId: 'sqli',
|
|
145
|
+
checkName: 'SQL Injection',
|
|
146
|
+
severity: 'critical',
|
|
147
|
+
endpoint: baseUrl,
|
|
148
|
+
method: method,
|
|
149
|
+
parameter: field,
|
|
150
|
+
payload,
|
|
151
|
+
evidence: `SQL error in response body: "${match}"`,
|
|
152
|
+
description: `Body field "${field}" may be vulnerable to SQL injection.`,
|
|
153
|
+
remediation: 'Use parameterized queries or prepared statements.',
|
|
154
|
+
owaspCategory: 'A03:2021 Injection',
|
|
155
|
+
request: res.request,
|
|
156
|
+
response: res.response,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// skip
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
sem.release();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
await Promise.all(tasks);
|
|
168
|
+
return { findings, requestCount };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function checkForSqlError(body) {
|
|
172
|
+
for (const pattern of SQL_ERROR_PATTERNS) {
|
|
173
|
+
const match = body.match(pattern);
|
|
174
|
+
if (match)
|
|
175
|
+
return match[0];
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-Side Request Forgery Check (A10:2021)
|
|
3
|
+
*/
|
|
4
|
+
import { BaseCheck, type CheckResult } from './base.js';
|
|
5
|
+
import type { ScanTarget, ScanConfig } from '../types.js';
|
|
6
|
+
export declare class SsrfCheck extends BaseCheck {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
owaspCategory: string;
|
|
11
|
+
run(target: ScanTarget, config: ScanConfig): Promise<CheckResult>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-Side Request Forgery Check (A10:2021)
|
|
3
|
+
*/
|
|
4
|
+
import { BaseCheck } from './base.js';
|
|
5
|
+
import { sendRequest, Semaphore, RateLimiter } from '../utils/http.js';
|
|
6
|
+
import { SSRF_PAYLOADS } from '../fuzzer/payloads.js';
|
|
7
|
+
const SSRF_INDICATORS = [
|
|
8
|
+
/root:.*:0:0/i, // /etc/passwd content
|
|
9
|
+
/ami-id/i, // AWS metadata
|
|
10
|
+
/instance-id/i, // Cloud metadata
|
|
11
|
+
/compute\.internal/i, // GCP metadata
|
|
12
|
+
/\b10\.\d+\.\d+\.\d+/, // Internal IP leaked
|
|
13
|
+
/\b172\.(1[6-9]|2\d|3[01])\.\d+\.\d+/, // Internal IP
|
|
14
|
+
/\b192\.168\.\d+\.\d+/, // Internal IP
|
|
15
|
+
];
|
|
16
|
+
export class SsrfCheck extends BaseCheck {
|
|
17
|
+
id = 'ssrf';
|
|
18
|
+
name = 'Server-Side Request Forgery';
|
|
19
|
+
description = 'Test for SSRF vulnerabilities in URL parameters';
|
|
20
|
+
owaspCategory = 'A10:2021 SSRF';
|
|
21
|
+
async run(target, config) {
|
|
22
|
+
const findings = [];
|
|
23
|
+
let requestCount = 0;
|
|
24
|
+
const sem = new Semaphore(config.concurrency);
|
|
25
|
+
const rl = new RateLimiter(config.rateLimit);
|
|
26
|
+
const payloads = config.depth === 'shallow'
|
|
27
|
+
? SSRF_PAYLOADS.slice(0, 4)
|
|
28
|
+
: config.depth === 'deep'
|
|
29
|
+
? SSRF_PAYLOADS
|
|
30
|
+
: SSRF_PAYLOADS.slice(0, 8);
|
|
31
|
+
// Find parameters that look like URLs
|
|
32
|
+
const urlParamNames = ['url', 'uri', 'href', 'link', 'redirect', 'callback', 'next', 'return', 'dest', 'target', 'path', 'file', 'page', 'fetch', 'load'];
|
|
33
|
+
const tasks = [];
|
|
34
|
+
for (const ep of target.endpoints) {
|
|
35
|
+
const url = target.baseUrl + ep.path;
|
|
36
|
+
const queryParams = ep.parameters.filter(p => p.in === 'query');
|
|
37
|
+
// Test named URL-like params, or common param names if none defined
|
|
38
|
+
const testParams = queryParams.length > 0
|
|
39
|
+
? queryParams.filter(p => urlParamNames.some(n => p.name.toLowerCase().includes(n)))
|
|
40
|
+
: urlParamNames.slice(0, 3).map(n => ({ name: n }));
|
|
41
|
+
for (const param of testParams) {
|
|
42
|
+
for (const payload of payloads) {
|
|
43
|
+
tasks.push((async () => {
|
|
44
|
+
await sem.acquire();
|
|
45
|
+
try {
|
|
46
|
+
await rl.wait();
|
|
47
|
+
const testUrl = `${url}?${encodeURIComponent(param.name)}=${encodeURIComponent(payload)}`;
|
|
48
|
+
const res = await sendRequest({
|
|
49
|
+
url: testUrl,
|
|
50
|
+
method: ep.method,
|
|
51
|
+
headers: target.globalHeaders,
|
|
52
|
+
timeout: config.timeout,
|
|
53
|
+
});
|
|
54
|
+
requestCount++;
|
|
55
|
+
// Check for SSRF indicators in response
|
|
56
|
+
for (const indicator of SSRF_INDICATORS) {
|
|
57
|
+
if (indicator.test(res.response.bodySnippet)) {
|
|
58
|
+
findings.push({
|
|
59
|
+
id: `ssrf-${param.name}-${findings.length}`,
|
|
60
|
+
checkId: this.id,
|
|
61
|
+
checkName: this.name,
|
|
62
|
+
severity: 'critical',
|
|
63
|
+
endpoint: url,
|
|
64
|
+
method: ep.method,
|
|
65
|
+
parameter: param.name,
|
|
66
|
+
payload,
|
|
67
|
+
evidence: `SSRF indicator in response: ${res.response.bodySnippet.slice(0, 200)}`,
|
|
68
|
+
description: `Parameter "${param.name}" may be vulnerable to SSRF. The server appears to fetch attacker-controlled URLs.`,
|
|
69
|
+
remediation: 'Validate and sanitize all URL inputs. Use allowlists for permitted domains. Block requests to internal/metadata IPs.',
|
|
70
|
+
owaspCategory: this.owaspCategory,
|
|
71
|
+
request: res.request,
|
|
72
|
+
response: res.response,
|
|
73
|
+
});
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Check for different behavior with internal URLs
|
|
78
|
+
if (res.response.statusCode === 200 && payload.includes('127.0.0.1')) {
|
|
79
|
+
// Compare with a non-internal URL
|
|
80
|
+
const extUrl = `${url}?${encodeURIComponent(param.name)}=${encodeURIComponent('http://example.com')}`;
|
|
81
|
+
try {
|
|
82
|
+
const extRes = await sendRequest({
|
|
83
|
+
url: extUrl,
|
|
84
|
+
method: ep.method,
|
|
85
|
+
headers: target.globalHeaders,
|
|
86
|
+
timeout: config.timeout,
|
|
87
|
+
});
|
|
88
|
+
requestCount++;
|
|
89
|
+
if (res.response.bodySnippet !== extRes.response.bodySnippet) {
|
|
90
|
+
findings.push({
|
|
91
|
+
id: `ssrf-diff-${param.name}-${findings.length}`,
|
|
92
|
+
checkId: this.id,
|
|
93
|
+
checkName: this.name,
|
|
94
|
+
severity: 'high',
|
|
95
|
+
endpoint: url,
|
|
96
|
+
method: ep.method,
|
|
97
|
+
parameter: param.name,
|
|
98
|
+
payload,
|
|
99
|
+
evidence: `Different responses for internal (${payload}) vs external URL`,
|
|
100
|
+
description: `Parameter "${param.name}" produces different responses for internal vs external URLs, suggesting server-side URL fetching.`,
|
|
101
|
+
remediation: 'Block requests to internal IPs and cloud metadata endpoints. Use URL allowlists.',
|
|
102
|
+
owaspCategory: this.owaspCategory,
|
|
103
|
+
request: res.request,
|
|
104
|
+
response: res.response,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// skip comparison
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// skip
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
sem.release();
|
|
118
|
+
}
|
|
119
|
+
})());
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
await Promise.all(tasks);
|
|
124
|
+
return { findings, requestCount };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-Site Scripting Check (A03:2021 - Injection)
|
|
3
|
+
*/
|
|
4
|
+
import { BaseCheck, type CheckResult } from './base.js';
|
|
5
|
+
import type { ScanTarget, ScanConfig } from '../types.js';
|
|
6
|
+
export declare class XssCheck extends BaseCheck {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
owaspCategory: string;
|
|
11
|
+
run(target: ScanTarget, config: ScanConfig): Promise<CheckResult>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-Site Scripting Check (A03:2021 - Injection)
|
|
3
|
+
*/
|
|
4
|
+
import { BaseCheck } from './base.js';
|
|
5
|
+
import { sendRequest, Semaphore, RateLimiter } from '../utils/http.js';
|
|
6
|
+
import { XSS_PAYLOADS } from '../fuzzer/payloads.js';
|
|
7
|
+
export class XssCheck extends BaseCheck {
|
|
8
|
+
id = 'xss';
|
|
9
|
+
name = 'Cross-Site Scripting';
|
|
10
|
+
description = 'Test for reflected XSS vulnerabilities';
|
|
11
|
+
owaspCategory = 'A03:2021 Injection';
|
|
12
|
+
async run(target, config) {
|
|
13
|
+
const findings = [];
|
|
14
|
+
let requestCount = 0;
|
|
15
|
+
const sem = new Semaphore(config.concurrency);
|
|
16
|
+
const rl = new RateLimiter(config.rateLimit);
|
|
17
|
+
const payloads = config.depth === 'shallow'
|
|
18
|
+
? XSS_PAYLOADS.slice(0, 4)
|
|
19
|
+
: config.depth === 'deep'
|
|
20
|
+
? XSS_PAYLOADS
|
|
21
|
+
: XSS_PAYLOADS.slice(0, 8);
|
|
22
|
+
const tasks = [];
|
|
23
|
+
for (const ep of target.endpoints) {
|
|
24
|
+
if (ep.method !== 'GET')
|
|
25
|
+
continue; // reflected XSS mainly via GET
|
|
26
|
+
const url = target.baseUrl + ep.path;
|
|
27
|
+
const params = ep.parameters.filter(p => p.in === 'query');
|
|
28
|
+
// Also test a generic param if none defined
|
|
29
|
+
const testParams = params.length > 0 ? params.map(p => p.name) : ['q', 'search', 'input'];
|
|
30
|
+
for (const paramName of testParams) {
|
|
31
|
+
for (const payload of payloads) {
|
|
32
|
+
tasks.push((async () => {
|
|
33
|
+
await sem.acquire();
|
|
34
|
+
try {
|
|
35
|
+
await rl.wait();
|
|
36
|
+
const testUrl = `${url}?${encodeURIComponent(paramName)}=${encodeURIComponent(payload)}`;
|
|
37
|
+
const res = await sendRequest({
|
|
38
|
+
url: testUrl,
|
|
39
|
+
method: 'GET',
|
|
40
|
+
headers: target.globalHeaders,
|
|
41
|
+
timeout: config.timeout,
|
|
42
|
+
});
|
|
43
|
+
requestCount++;
|
|
44
|
+
// Check if payload is reflected unescaped
|
|
45
|
+
if (res.response.bodySnippet.includes(payload)) {
|
|
46
|
+
const contentType = res.response.headers['content-type'] || '';
|
|
47
|
+
const isHtml = contentType.includes('html');
|
|
48
|
+
findings.push({
|
|
49
|
+
id: `xss-${paramName}-${findings.length}`,
|
|
50
|
+
checkId: this.id,
|
|
51
|
+
checkName: this.name,
|
|
52
|
+
severity: isHtml ? 'high' : 'medium',
|
|
53
|
+
endpoint: url,
|
|
54
|
+
method: 'GET',
|
|
55
|
+
parameter: paramName,
|
|
56
|
+
payload,
|
|
57
|
+
evidence: `Payload reflected in response${isHtml ? ' (HTML context)' : ''}`,
|
|
58
|
+
description: `Parameter "${paramName}" reflects input without escaping. ${isHtml ? 'Response is HTML, making XSS likely exploitable.' : 'Response is not HTML but input is reflected.'}`,
|
|
59
|
+
remediation: 'Escape all user input before rendering. Use Content-Type headers correctly. Implement CSP.',
|
|
60
|
+
owaspCategory: this.owaspCategory,
|
|
61
|
+
request: res.request,
|
|
62
|
+
response: res.response,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// skip
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
sem.release();
|
|
71
|
+
}
|
|
72
|
+
})());
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
await Promise.all(tasks);
|
|
77
|
+
return { findings, requestCount };
|
|
78
|
+
}
|
|
79
|
+
}
|