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
package/dist/cli.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* openpen - Open source API fuzzing and penetration testing CLI
|
|
4
|
+
*/
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { setVerbose, info, error } from './utils/logger.js';
|
|
7
|
+
import { loadTarget } from './spec/parser.js';
|
|
8
|
+
import { FuzzEngine } from './fuzzer/engine.js';
|
|
9
|
+
import { getAllChecks, getChecksByIds, listChecks } from './checks/index.js';
|
|
10
|
+
import { getAllWsChecks, getWsChecksByIds, listWsChecks } from './ws/checks.js';
|
|
11
|
+
import { reportTerminal, reportJson } from './reporter/index.js';
|
|
12
|
+
const program = new Command();
|
|
13
|
+
program
|
|
14
|
+
.name('openpen')
|
|
15
|
+
.description('Open source CLI for API fuzzing and penetration testing')
|
|
16
|
+
.version('0.1.0');
|
|
17
|
+
// scan command
|
|
18
|
+
program
|
|
19
|
+
.command('scan')
|
|
20
|
+
.description('Run OWASP security checks against an API')
|
|
21
|
+
.argument('<target>', 'Base URL of the API')
|
|
22
|
+
.option('--spec <path>', 'Path or URL to OpenAPI/Swagger spec')
|
|
23
|
+
.option('--checks <list>', 'Comma-separated check IDs to run')
|
|
24
|
+
.option('--exclude-checks <list>', 'Checks to skip')
|
|
25
|
+
.option('--auth-header <header>', 'Auth header (e.g. "Authorization: Bearer tok")', collect, [])
|
|
26
|
+
.option('--auth-token <token>', 'Bearer token shorthand')
|
|
27
|
+
.option('--method <methods>', 'HTTP methods to test (comma-separated)')
|
|
28
|
+
.option('--paths <paths>', 'Comma-separated paths to test (manual mode)')
|
|
29
|
+
.option('--depth <level>', 'Payload volume: shallow, normal, deep', 'normal')
|
|
30
|
+
.option('--output <file>', 'Write report to file')
|
|
31
|
+
.option('--format <fmt>', 'Output format: terminal, json', 'terminal')
|
|
32
|
+
.option('--timeout <ms>', 'Per-request timeout in ms', '10000')
|
|
33
|
+
.option('--concurrency <n>', 'Max concurrent requests', '10')
|
|
34
|
+
.option('--rate-limit <n>', 'Max requests per second', '50')
|
|
35
|
+
.option('--verbose', 'Show individual request details')
|
|
36
|
+
.option('--no-color', 'Disable colored output')
|
|
37
|
+
.action(async (targetUrl, opts) => {
|
|
38
|
+
setVerbose(opts.verbose || false);
|
|
39
|
+
const auth = parseAuth(opts.authHeader, opts.authToken);
|
|
40
|
+
const target = await loadTarget(targetUrl, {
|
|
41
|
+
specPath: opts.spec,
|
|
42
|
+
methods: opts.method?.split(','),
|
|
43
|
+
paths: opts.paths?.split(','),
|
|
44
|
+
auth,
|
|
45
|
+
});
|
|
46
|
+
if (target.endpoints.length === 0) {
|
|
47
|
+
error('No endpoints found. Provide --spec or --paths.');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
info(`\n OPENPEN v0.1.0 -- API Security Scanner\n`);
|
|
51
|
+
info(` Target: ${targetUrl}`);
|
|
52
|
+
info(` Endpoints: ${target.endpoints.length}`);
|
|
53
|
+
info(` Depth: ${opts.depth}`);
|
|
54
|
+
const config = {
|
|
55
|
+
depth: opts.depth,
|
|
56
|
+
checks: opts.checks ? opts.checks.split(',') : [],
|
|
57
|
+
excludeChecks: opts.excludeChecks ? opts.excludeChecks.split(',') : [],
|
|
58
|
+
timeout: parseInt(opts.timeout),
|
|
59
|
+
concurrency: parseInt(opts.concurrency),
|
|
60
|
+
rateLimit: parseInt(opts.rateLimit),
|
|
61
|
+
verbose: opts.verbose || false,
|
|
62
|
+
noColor: !opts.color,
|
|
63
|
+
};
|
|
64
|
+
const checks = config.checks.length > 0
|
|
65
|
+
? getChecksByIds(config.checks)
|
|
66
|
+
: getAllChecks().filter(c => !config.excludeChecks.includes(c.id));
|
|
67
|
+
info(` Checks: ${checks.length} modules\n`);
|
|
68
|
+
const startedAt = new Date();
|
|
69
|
+
let totalRequests = 0;
|
|
70
|
+
const findings = [];
|
|
71
|
+
for (const check of checks) {
|
|
72
|
+
info(` Running: ${check.name}...`);
|
|
73
|
+
const result = await check.run(target, config);
|
|
74
|
+
findings.push(...result.findings);
|
|
75
|
+
totalRequests += result.requestCount;
|
|
76
|
+
}
|
|
77
|
+
const completedAt = new Date();
|
|
78
|
+
const report = {
|
|
79
|
+
target: targetUrl,
|
|
80
|
+
startedAt: startedAt.toISOString(),
|
|
81
|
+
completedAt: completedAt.toISOString(),
|
|
82
|
+
duration: completedAt.getTime() - startedAt.getTime(),
|
|
83
|
+
totalRequests,
|
|
84
|
+
findings,
|
|
85
|
+
summary: {
|
|
86
|
+
critical: findings.filter(f => f.severity === 'critical').length,
|
|
87
|
+
high: findings.filter(f => f.severity === 'high').length,
|
|
88
|
+
medium: findings.filter(f => f.severity === 'medium').length,
|
|
89
|
+
low: findings.filter(f => f.severity === 'low').length,
|
|
90
|
+
info: findings.filter(f => f.severity === 'info').length,
|
|
91
|
+
},
|
|
92
|
+
checksRun: checks.map(c => c.id),
|
|
93
|
+
};
|
|
94
|
+
if (opts.format === 'json') {
|
|
95
|
+
reportJson(report, opts.output);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
reportTerminal(report, !opts.color);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
// fuzz command
|
|
102
|
+
program
|
|
103
|
+
.command('fuzz')
|
|
104
|
+
.description('Fuzz API endpoints with mutated payloads')
|
|
105
|
+
.argument('<target>', 'URL to fuzz')
|
|
106
|
+
.option('--method <method>', 'HTTP method', 'GET')
|
|
107
|
+
.option('--body <json>', 'Request body (use FUZZ as placeholder)')
|
|
108
|
+
.option('--headers <json>', 'Additional headers as JSON')
|
|
109
|
+
.option('--fuzz-params <list>', 'Parameters to fuzz (comma-separated)')
|
|
110
|
+
.option('--wordlist <name>', 'Wordlist: sqli, xss, path-traversal, ssrf, all', 'all')
|
|
111
|
+
.option('--detect <methods>', 'Anomaly detection: status,length,time,content', 'status,length,time,content')
|
|
112
|
+
.option('--baseline', 'Send clean request first for comparison')
|
|
113
|
+
.option('--auth-header <header>', 'Auth header', collect, [])
|
|
114
|
+
.option('--auth-token <token>', 'Bearer token shorthand')
|
|
115
|
+
.option('--timeout <ms>', 'Per-request timeout', '10000')
|
|
116
|
+
.option('--concurrency <n>', 'Max concurrent requests', '10')
|
|
117
|
+
.option('--rate-limit <n>', 'Max requests per second', '50')
|
|
118
|
+
.option('--output <file>', 'Write results to file')
|
|
119
|
+
.option('--verbose', 'Show each request/response')
|
|
120
|
+
.action(async (targetUrl, opts) => {
|
|
121
|
+
setVerbose(opts.verbose || false);
|
|
122
|
+
const auth = parseAuth(opts.authHeader, opts.authToken);
|
|
123
|
+
const engine = new FuzzEngine({
|
|
124
|
+
timeout: parseInt(opts.timeout),
|
|
125
|
+
concurrency: parseInt(opts.concurrency),
|
|
126
|
+
rateLimit: parseInt(opts.rateLimit),
|
|
127
|
+
verbose: opts.verbose || false,
|
|
128
|
+
});
|
|
129
|
+
info(`\n OPENPEN v0.1.0 -- Fuzzer\n`);
|
|
130
|
+
info(` Target: ${targetUrl}`);
|
|
131
|
+
info(` Method: ${opts.method}`);
|
|
132
|
+
info(` Wordlist: ${opts.wordlist}\n`);
|
|
133
|
+
const results = await engine.fuzz({
|
|
134
|
+
url: targetUrl,
|
|
135
|
+
method: opts.method,
|
|
136
|
+
body: opts.body,
|
|
137
|
+
headers: {
|
|
138
|
+
...(opts.headers ? JSON.parse(opts.headers) : {}),
|
|
139
|
+
...(auth?.headers || {}),
|
|
140
|
+
},
|
|
141
|
+
fuzzParams: opts.fuzzParams?.split(',') || [],
|
|
142
|
+
wordlist: opts.wordlist,
|
|
143
|
+
detect: opts.detect.split(','),
|
|
144
|
+
baseline: opts.baseline || false,
|
|
145
|
+
});
|
|
146
|
+
const anomalies = results.filter(r => r.anomalies.length > 0);
|
|
147
|
+
info(`\n Results: ${anomalies.length} anomalies from ${results.length} requests\n`);
|
|
148
|
+
for (const r of anomalies) {
|
|
149
|
+
info(` [${r.anomalies.join(',')}] ${r.payload} -> ${r.response.statusCode} (${r.response.responseTime}ms)`);
|
|
150
|
+
}
|
|
151
|
+
if (opts.output) {
|
|
152
|
+
const { writeFileSync } = await import('fs');
|
|
153
|
+
writeFileSync(opts.output, JSON.stringify(results, null, 2));
|
|
154
|
+
info(`\n Report written to ${opts.output}`);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
// info command
|
|
158
|
+
program
|
|
159
|
+
.command('info')
|
|
160
|
+
.description('Reconnaissance - fingerprint an API')
|
|
161
|
+
.argument('<target>', 'Base URL of the API')
|
|
162
|
+
.option('--spec <path>', 'Path or URL to OpenAPI/Swagger spec')
|
|
163
|
+
.option('--auth-header <header>', 'Auth header', collect, [])
|
|
164
|
+
.option('--auth-token <token>', 'Bearer token shorthand')
|
|
165
|
+
.action(async (targetUrl, opts) => {
|
|
166
|
+
const auth = parseAuth(opts.authHeader, opts.authToken);
|
|
167
|
+
const target = await loadTarget(targetUrl, {
|
|
168
|
+
specPath: opts.spec,
|
|
169
|
+
auth,
|
|
170
|
+
});
|
|
171
|
+
info(`\n OPENPEN v0.1.0 -- API Recon\n`);
|
|
172
|
+
info(` Target: ${targetUrl}`);
|
|
173
|
+
info(` Endpoints: ${target.endpoints.length}\n`);
|
|
174
|
+
for (const ep of target.endpoints) {
|
|
175
|
+
const params = ep.parameters.map(p => `${p.name}(${p.in})`).join(', ');
|
|
176
|
+
info(` ${ep.method.padEnd(7)} ${ep.path}${params ? ' [' + params + ']' : ''}`);
|
|
177
|
+
}
|
|
178
|
+
// Fingerprint
|
|
179
|
+
try {
|
|
180
|
+
const res = await fetch(targetUrl, { signal: AbortSignal.timeout(5000) });
|
|
181
|
+
info(`\n Server Headers:`);
|
|
182
|
+
for (const [k, v] of res.headers) {
|
|
183
|
+
if (['server', 'x-powered-by', 'x-frame-options', 'x-content-type-options',
|
|
184
|
+
'strict-transport-security', 'content-security-policy',
|
|
185
|
+
'access-control-allow-origin'].includes(k.toLowerCase())) {
|
|
186
|
+
info(` ${k}: ${v}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
info(`\n Could not reach ${targetUrl} for fingerprinting`);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
// ws-test command
|
|
195
|
+
program
|
|
196
|
+
.command('ws-test')
|
|
197
|
+
.description('Run security checks against a WebSocket endpoint')
|
|
198
|
+
.argument('<target>', 'WebSocket URL (ws:// or wss://)')
|
|
199
|
+
.option('--checks <list>', 'Comma-separated check IDs to run')
|
|
200
|
+
.option('--exclude-checks <list>', 'Checks to skip')
|
|
201
|
+
.option('--timeout <ms>', 'Connection timeout in ms', '5000')
|
|
202
|
+
.option('--output <file>', 'Write report to file')
|
|
203
|
+
.option('--format <fmt>', 'Output format: terminal, json', 'terminal')
|
|
204
|
+
.option('--verbose', 'Show detailed test output')
|
|
205
|
+
.action(async (targetUrl, opts) => {
|
|
206
|
+
setVerbose(opts.verbose || false);
|
|
207
|
+
// Validate URL scheme
|
|
208
|
+
if (!targetUrl.startsWith('ws://') && !targetUrl.startsWith('wss://')) {
|
|
209
|
+
// Auto-upgrade http(s) to ws(s)
|
|
210
|
+
targetUrl = targetUrl.replace(/^https:/, 'wss:').replace(/^http:/, 'ws:');
|
|
211
|
+
if (!targetUrl.startsWith('ws://') && !targetUrl.startsWith('wss://')) {
|
|
212
|
+
targetUrl = `wss://${targetUrl}`;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const timeout = parseInt(opts.timeout);
|
|
216
|
+
const allChecks = opts.checks
|
|
217
|
+
? getWsChecksByIds(opts.checks.split(','))
|
|
218
|
+
: getAllWsChecks().filter(c => !(opts.excludeChecks || '').split(',').includes(c.info.id));
|
|
219
|
+
info(`\n OPENPEN v0.1.0 -- WebSocket Security Tester\n`);
|
|
220
|
+
info(` Target: ${targetUrl}`);
|
|
221
|
+
info(` Checks: ${allChecks.length} modules\n`);
|
|
222
|
+
const startedAt = new Date();
|
|
223
|
+
const results = [];
|
|
224
|
+
for (const check of allChecks) {
|
|
225
|
+
info(` Running: ${check.info.name}...`);
|
|
226
|
+
const result = await check.run(targetUrl, timeout);
|
|
227
|
+
results.push(result);
|
|
228
|
+
const icon = result.status === 'pass' ? 'OK' :
|
|
229
|
+
result.status === 'fail' ? 'FAIL' :
|
|
230
|
+
result.status === 'warn' ? 'WARN' : 'ERR';
|
|
231
|
+
const sev = result.status === 'pass' ? '' : ` [${result.severity.toUpperCase()}]`;
|
|
232
|
+
info(` ${icon}${sev} ${result.description}`);
|
|
233
|
+
}
|
|
234
|
+
const completedAt = new Date();
|
|
235
|
+
const report = {
|
|
236
|
+
target: targetUrl,
|
|
237
|
+
startedAt: startedAt.toISOString(),
|
|
238
|
+
completedAt: completedAt.toISOString(),
|
|
239
|
+
duration: completedAt.getTime() - startedAt.getTime(),
|
|
240
|
+
results,
|
|
241
|
+
summary: {
|
|
242
|
+
pass: results.filter(r => r.status === 'pass').length,
|
|
243
|
+
fail: results.filter(r => r.status === 'fail').length,
|
|
244
|
+
warn: results.filter(r => r.status === 'warn').length,
|
|
245
|
+
error: results.filter(r => r.status === 'error').length,
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
info(`\n Summary: ${report.summary.pass} pass, ${report.summary.fail} fail, ${report.summary.warn} warn, ${report.summary.error} error`);
|
|
249
|
+
info(` Duration: ${report.duration}ms\n`);
|
|
250
|
+
if (opts.format === 'json' || opts.output) {
|
|
251
|
+
const json = JSON.stringify(report, null, 2);
|
|
252
|
+
if (opts.output) {
|
|
253
|
+
const { writeFileSync } = await import('fs');
|
|
254
|
+
writeFileSync(opts.output, json);
|
|
255
|
+
info(` Report written to ${opts.output}\n`);
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
console.log(json);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
// list-checks command
|
|
263
|
+
program
|
|
264
|
+
.command('list-checks')
|
|
265
|
+
.description('List available security check modules')
|
|
266
|
+
.action(() => {
|
|
267
|
+
info(`\n OPENPEN v0.1.0 -- Available Checks\n`);
|
|
268
|
+
info(` HTTP API Checks:`);
|
|
269
|
+
const checks = listChecks();
|
|
270
|
+
for (const c of checks) {
|
|
271
|
+
info(` ${c.id.padEnd(20)} ${c.owaspCategory.padEnd(25)} ${c.name}`);
|
|
272
|
+
}
|
|
273
|
+
info(`\n WebSocket Checks:`);
|
|
274
|
+
const wsChecks = listWsChecks();
|
|
275
|
+
for (const c of wsChecks) {
|
|
276
|
+
info(` ${c.id.padEnd(20)} ${''.padEnd(25)} ${c.name}`);
|
|
277
|
+
}
|
|
278
|
+
info('');
|
|
279
|
+
});
|
|
280
|
+
// Helpers
|
|
281
|
+
function collect(val, prev) {
|
|
282
|
+
prev.push(val);
|
|
283
|
+
return prev;
|
|
284
|
+
}
|
|
285
|
+
function parseAuth(headers, token) {
|
|
286
|
+
const result = {};
|
|
287
|
+
for (const h of headers) {
|
|
288
|
+
const idx = h.indexOf(':');
|
|
289
|
+
if (idx > 0) {
|
|
290
|
+
result[h.slice(0, idx).trim()] = h.slice(idx + 1).trim();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (token) {
|
|
294
|
+
result['Authorization'] = `Bearer ${token}`;
|
|
295
|
+
}
|
|
296
|
+
if (Object.keys(result).length === 0)
|
|
297
|
+
return undefined;
|
|
298
|
+
return { headers: result };
|
|
299
|
+
}
|
|
300
|
+
program.parse();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core fuzzing orchestrator
|
|
3
|
+
*/
|
|
4
|
+
import type { FuzzResult } from '../types.js';
|
|
5
|
+
export interface FuzzEngineConfig {
|
|
6
|
+
timeout: number;
|
|
7
|
+
concurrency: number;
|
|
8
|
+
rateLimit: number;
|
|
9
|
+
verbose: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface FuzzRequest {
|
|
12
|
+
url: string;
|
|
13
|
+
method: string;
|
|
14
|
+
body?: string;
|
|
15
|
+
headers: Record<string, string>;
|
|
16
|
+
fuzzParams: string[];
|
|
17
|
+
wordlist: string;
|
|
18
|
+
detect: string[];
|
|
19
|
+
baseline: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare class FuzzEngine {
|
|
22
|
+
private semaphore;
|
|
23
|
+
private rateLimiter;
|
|
24
|
+
private config;
|
|
25
|
+
constructor(config: FuzzEngineConfig);
|
|
26
|
+
fuzz(req: FuzzRequest): Promise<FuzzResult[]>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core fuzzing orchestrator
|
|
3
|
+
*/
|
|
4
|
+
import { sendRequest, Semaphore, RateLimiter } from '../utils/http.js';
|
|
5
|
+
import { getPayloads } from './payloads.js';
|
|
6
|
+
import { info, verbose } from '../utils/logger.js';
|
|
7
|
+
export class FuzzEngine {
|
|
8
|
+
semaphore;
|
|
9
|
+
rateLimiter;
|
|
10
|
+
config;
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
this.semaphore = new Semaphore(config.concurrency);
|
|
14
|
+
this.rateLimiter = new RateLimiter(config.rateLimit);
|
|
15
|
+
}
|
|
16
|
+
async fuzz(req) {
|
|
17
|
+
const payloads = getPayloads(req.wordlist);
|
|
18
|
+
info(` Payloads: ${payloads.length}`);
|
|
19
|
+
// Get baseline if requested
|
|
20
|
+
let baselineResponse;
|
|
21
|
+
if (req.baseline) {
|
|
22
|
+
verbose(' Getting baseline response...');
|
|
23
|
+
const cleanUrl = req.url.replace(/FUZZ/g, 'test');
|
|
24
|
+
const cleanBody = req.body?.replace(/FUZZ/g, 'test');
|
|
25
|
+
try {
|
|
26
|
+
const res = await sendRequest({
|
|
27
|
+
url: cleanUrl,
|
|
28
|
+
method: req.method,
|
|
29
|
+
headers: req.headers,
|
|
30
|
+
body: cleanBody,
|
|
31
|
+
timeout: this.config.timeout,
|
|
32
|
+
});
|
|
33
|
+
baselineResponse = res.response;
|
|
34
|
+
verbose(` Baseline: ${baselineResponse.statusCode} (${baselineResponse.responseTime}ms, ${baselineResponse.bodySnippet.length} bytes)`);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
verbose(' Baseline request failed, continuing without');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const results = [];
|
|
41
|
+
const detectors = new Set(req.detect);
|
|
42
|
+
const tasks = payloads.map(payload => async () => {
|
|
43
|
+
await this.semaphore.acquire();
|
|
44
|
+
try {
|
|
45
|
+
await this.rateLimiter.wait();
|
|
46
|
+
const fuzzedUrl = req.url.replace(/FUZZ/g, encodeURIComponent(payload));
|
|
47
|
+
const fuzzedBody = req.body?.replace(/FUZZ/g, payload);
|
|
48
|
+
const res = await sendRequest({
|
|
49
|
+
url: fuzzedUrl,
|
|
50
|
+
method: req.method,
|
|
51
|
+
headers: req.headers,
|
|
52
|
+
body: fuzzedBody,
|
|
53
|
+
timeout: this.config.timeout,
|
|
54
|
+
});
|
|
55
|
+
const anomalies = detectAnomalies(res.response, baselineResponse, payload, detectors);
|
|
56
|
+
results.push({
|
|
57
|
+
payload,
|
|
58
|
+
request: res.request,
|
|
59
|
+
response: res.response,
|
|
60
|
+
anomalies,
|
|
61
|
+
baseline: baselineResponse,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
verbose(` Error fuzzing with payload "${payload.slice(0, 30)}": ${err}`);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
this.semaphore.release();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
// Execute all fuzz tasks
|
|
72
|
+
await Promise.all(tasks.map(t => t()));
|
|
73
|
+
return results;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function detectAnomalies(response, baseline, payload, detectors) {
|
|
77
|
+
const anomalies = [];
|
|
78
|
+
// Status code change
|
|
79
|
+
if (detectors.has('status') && baseline) {
|
|
80
|
+
if (response.statusCode !== baseline.statusCode) {
|
|
81
|
+
// 500 errors are especially interesting
|
|
82
|
+
if (response.statusCode >= 500) {
|
|
83
|
+
anomalies.push('status_change');
|
|
84
|
+
}
|
|
85
|
+
else if (Math.floor(response.statusCode / 100) !== Math.floor(baseline.statusCode / 100)) {
|
|
86
|
+
anomalies.push('status_change');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Response length anomaly
|
|
91
|
+
if (detectors.has('length') && baseline) {
|
|
92
|
+
const baseLen = baseline.bodySnippet.length;
|
|
93
|
+
const resLen = response.bodySnippet.length;
|
|
94
|
+
if (baseLen > 0) {
|
|
95
|
+
const ratio = Math.abs(resLen - baseLen) / baseLen;
|
|
96
|
+
if (ratio > 0.5) {
|
|
97
|
+
anomalies.push('length_anomaly');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Timing anomaly (possible time-based injection)
|
|
102
|
+
if (detectors.has('time') && baseline) {
|
|
103
|
+
if (response.responseTime > baseline.responseTime * 3 && response.responseTime > 3000) {
|
|
104
|
+
anomalies.push('time_anomaly');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Error string detection
|
|
108
|
+
if (detectors.has('content')) {
|
|
109
|
+
const body = response.bodySnippet.toLowerCase();
|
|
110
|
+
const errorPatterns = [
|
|
111
|
+
'sql syntax', 'mysql', 'postgresql', 'sqlite', 'ora-',
|
|
112
|
+
'syntax error', 'uncaught exception', 'stack trace',
|
|
113
|
+
'internal server error', 'fatal error',
|
|
114
|
+
'root:', '/etc/passwd', '/bin/sh',
|
|
115
|
+
'access denied', 'permission denied',
|
|
116
|
+
];
|
|
117
|
+
if (errorPatterns.some(p => body.includes(p))) {
|
|
118
|
+
anomalies.push('error_string');
|
|
119
|
+
}
|
|
120
|
+
// Check for payload reflection (potential XSS)
|
|
121
|
+
if (response.bodySnippet.includes(payload)) {
|
|
122
|
+
anomalies.push('reflection');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return anomalies;
|
|
126
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payload mutation strategies - encoding, case variation, null bytes
|
|
3
|
+
*/
|
|
4
|
+
export type MutationStrategy = 'url-encode' | 'double-encode' | 'unicode' | 'case-swap' | 'null-byte' | 'concat';
|
|
5
|
+
/**
|
|
6
|
+
* Apply a set of mutations to a payload, returning all variants
|
|
7
|
+
*/
|
|
8
|
+
export declare function mutatePayload(payload: string, strategies?: MutationStrategy[]): string[];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payload mutation strategies - encoding, case variation, null bytes
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Apply a set of mutations to a payload, returning all variants
|
|
6
|
+
*/
|
|
7
|
+
export function mutatePayload(payload, strategies) {
|
|
8
|
+
const strats = strategies || ['url-encode', 'double-encode', 'case-swap', 'null-byte'];
|
|
9
|
+
const results = [payload]; // always include original
|
|
10
|
+
for (const s of strats) {
|
|
11
|
+
const mutated = applyMutation(payload, s);
|
|
12
|
+
if (mutated !== payload) {
|
|
13
|
+
results.push(mutated);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return results;
|
|
17
|
+
}
|
|
18
|
+
function applyMutation(payload, strategy) {
|
|
19
|
+
switch (strategy) {
|
|
20
|
+
case 'url-encode':
|
|
21
|
+
return urlEncode(payload);
|
|
22
|
+
case 'double-encode':
|
|
23
|
+
return urlEncode(urlEncode(payload));
|
|
24
|
+
case 'unicode':
|
|
25
|
+
return unicodeEscape(payload);
|
|
26
|
+
case 'case-swap':
|
|
27
|
+
return caseSwap(payload);
|
|
28
|
+
case 'null-byte':
|
|
29
|
+
return payload + '%00';
|
|
30
|
+
case 'concat':
|
|
31
|
+
return concatSplit(payload);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function urlEncode(s) {
|
|
35
|
+
return encodeURIComponent(s);
|
|
36
|
+
}
|
|
37
|
+
function unicodeEscape(s) {
|
|
38
|
+
return s.replace(/[<>'"/\\]/g, ch => {
|
|
39
|
+
const code = ch.charCodeAt(0);
|
|
40
|
+
return `\\u${code.toString(16).padStart(4, '0')}`;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function caseSwap(s) {
|
|
44
|
+
return s.replace(/[a-zA-Z]/g, ch => {
|
|
45
|
+
return ch === ch.toLowerCase() ? ch.toUpperCase() : ch.toLowerCase();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function concatSplit(s) {
|
|
49
|
+
// Split strings at midpoint with concat operator
|
|
50
|
+
if (s.length < 4)
|
|
51
|
+
return s;
|
|
52
|
+
const mid = Math.floor(s.length / 2);
|
|
53
|
+
return s.slice(0, mid) + "'+'" + s.slice(mid);
|
|
54
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in payload dictionaries for fuzzing
|
|
3
|
+
*/
|
|
4
|
+
export declare const SQLI_PAYLOADS: string[];
|
|
5
|
+
export declare const XSS_PAYLOADS: string[];
|
|
6
|
+
export declare const PATH_TRAVERSAL_PAYLOADS: string[];
|
|
7
|
+
export declare const SSRF_PAYLOADS: string[];
|
|
8
|
+
export declare const NOSQL_PAYLOADS: string[];
|
|
9
|
+
export declare const COMMAND_INJECTION_PAYLOADS: string[];
|
|
10
|
+
export declare const PROMPT_INJECTION_PAYLOADS: string[];
|
|
11
|
+
export declare const LLM_LEAK_PAYLOADS: string[];
|
|
12
|
+
export type WordlistName = 'sqli' | 'xss' | 'path-traversal' | 'ssrf' | 'nosql' | 'command-injection' | 'prompt-injection' | 'llm-leak' | 'all';
|
|
13
|
+
export declare function getPayloads(name: WordlistName): string[];
|