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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/dist/checks/auth-bypass.d.ts +12 -0
  4. package/dist/checks/auth-bypass.js +93 -0
  5. package/dist/checks/bac.d.ts +12 -0
  6. package/dist/checks/bac.js +107 -0
  7. package/dist/checks/base.d.ts +22 -0
  8. package/dist/checks/base.js +13 -0
  9. package/dist/checks/index.d.ts +7 -0
  10. package/dist/checks/index.js +40 -0
  11. package/dist/checks/llm-leak.d.ts +23 -0
  12. package/dist/checks/llm-leak.js +251 -0
  13. package/dist/checks/mass-assignment.d.ts +12 -0
  14. package/dist/checks/mass-assignment.js +169 -0
  15. package/dist/checks/prompt-injection.d.ts +23 -0
  16. package/dist/checks/prompt-injection.js +262 -0
  17. package/dist/checks/security-headers.d.ts +12 -0
  18. package/dist/checks/security-headers.js +133 -0
  19. package/dist/checks/sensitive-data.d.ts +12 -0
  20. package/dist/checks/sensitive-data.js +122 -0
  21. package/dist/checks/sqli.d.ts +12 -0
  22. package/dist/checks/sqli.js +178 -0
  23. package/dist/checks/ssrf.d.ts +12 -0
  24. package/dist/checks/ssrf.js +126 -0
  25. package/dist/checks/xss.d.ts +12 -0
  26. package/dist/checks/xss.js +79 -0
  27. package/dist/cli.d.ts +5 -0
  28. package/dist/cli.js +300 -0
  29. package/dist/fuzzer/engine.d.ts +27 -0
  30. package/dist/fuzzer/engine.js +126 -0
  31. package/dist/fuzzer/mutator.d.ts +8 -0
  32. package/dist/fuzzer/mutator.js +54 -0
  33. package/dist/fuzzer/payloads.d.ts +13 -0
  34. package/dist/fuzzer/payloads.js +167 -0
  35. package/dist/reporter/index.d.ts +5 -0
  36. package/dist/reporter/index.js +5 -0
  37. package/dist/reporter/json.d.ts +5 -0
  38. package/dist/reporter/json.js +14 -0
  39. package/dist/reporter/terminal.d.ts +5 -0
  40. package/dist/reporter/terminal.js +59 -0
  41. package/dist/spec/openapi.d.ts +5 -0
  42. package/dist/spec/openapi.js +119 -0
  43. package/dist/spec/parser.d.ts +11 -0
  44. package/dist/spec/parser.js +45 -0
  45. package/dist/types.d.ts +145 -0
  46. package/dist/types.js +4 -0
  47. package/dist/utils/http.d.ts +37 -0
  48. package/dist/utils/http.js +92 -0
  49. package/dist/utils/logger.d.ts +8 -0
  50. package/dist/utils/logger.js +20 -0
  51. package/dist/ws/checks.d.ts +18 -0
  52. package/dist/ws/checks.js +558 -0
  53. package/dist/ws/engine.d.ts +47 -0
  54. package/dist/ws/engine.js +139 -0
  55. 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
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * openpen - Open source API fuzzing and penetration testing CLI
4
+ */
5
+ export {};