@vulcn/driver-browser 0.3.0 → 0.4.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/dist/index.cjs +262 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +92 -3
- package/dist/index.d.ts +92 -3
- package/dist/index.js +260 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { RecordOptions, RecordingHandle, Session, RunContext, RunResult, CrawlOptions, FormCredentials, VulcnDriver } from '@vulcn/engine';
|
|
2
|
+
import { RecordOptions, RecordingHandle, Session, RunContext, RunResult, CapturedRequest, CrawlOptions, Finding, PayloadCategory, RuntimePayload, FormCredentials, VulcnDriver } from '@vulcn/engine';
|
|
3
3
|
import { Browser, Page, BrowserContext } from 'playwright';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -103,6 +103,7 @@ declare function checkBrowsers(): Promise<{
|
|
|
103
103
|
* - Links to follow for deeper crawling
|
|
104
104
|
*
|
|
105
105
|
* Outputs Session[] that are directly compatible with BrowserRunner.
|
|
106
|
+
* Also generates CapturedRequest[] metadata for Tier 1 HTTP fast scanning.
|
|
106
107
|
*
|
|
107
108
|
* This is the "auto-record" mode — instead of a human clicking around,
|
|
108
109
|
* the crawler automatically discovers injection points.
|
|
@@ -121,12 +122,100 @@ interface BrowserCrawlConfig {
|
|
|
121
122
|
height: number;
|
|
122
123
|
};
|
|
123
124
|
}
|
|
125
|
+
/** Result from crawling — sessions for browser replay + requests for HTTP scanning */
|
|
126
|
+
interface CrawlResult {
|
|
127
|
+
sessions: Session[];
|
|
128
|
+
capturedRequests: CapturedRequest[];
|
|
129
|
+
}
|
|
124
130
|
/**
|
|
125
131
|
* Crawl a URL and generate sessions.
|
|
126
132
|
*
|
|
127
133
|
* This is called by the browser driver's recorder.crawl() method.
|
|
134
|
+
* Returns both Session[] for Tier 2 browser replay and
|
|
135
|
+
* CapturedRequest[] for Tier 1 HTTP fast scanning.
|
|
128
136
|
*/
|
|
129
|
-
declare function crawlAndBuildSessions(config: BrowserCrawlConfig, options?: CrawlOptions): Promise<
|
|
137
|
+
declare function crawlAndBuildSessions(config: BrowserCrawlConfig, options?: CrawlOptions): Promise<CrawlResult>;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* HTTP Scanner — Tier 1 Fast Scan
|
|
141
|
+
*
|
|
142
|
+
* Replays captured HTTP requests via fetch() instead of Playwright.
|
|
143
|
+
* Substitutes security payloads into injectable form fields and checks
|
|
144
|
+
* the response body for payload reflection or error patterns.
|
|
145
|
+
*
|
|
146
|
+
* Speed: ~50ms per payload (vs 2-5s for browser replay)
|
|
147
|
+
*
|
|
148
|
+
* Catches:
|
|
149
|
+
* - Reflected XSS (payload appears in response body)
|
|
150
|
+
* - Error-based SQLi (SQL error patterns in response)
|
|
151
|
+
* - Server-side reflection of any payload
|
|
152
|
+
*
|
|
153
|
+
* Misses:
|
|
154
|
+
* - DOM-based XSS (requires JS execution)
|
|
155
|
+
* - Client-side state bugs
|
|
156
|
+
* - CSP-blocked attacks
|
|
157
|
+
*
|
|
158
|
+
* When reflection IS found, the finding is marked as `needsBrowserConfirmation`
|
|
159
|
+
* so the caller can escalate to Tier 2 for execution-based proof.
|
|
160
|
+
*/
|
|
161
|
+
|
|
162
|
+
interface HttpScanResult {
|
|
163
|
+
/** Total HTTP requests sent */
|
|
164
|
+
requestsSent: number;
|
|
165
|
+
/** How long the scan took (ms) */
|
|
166
|
+
duration: number;
|
|
167
|
+
/** Findings from reflection detection */
|
|
168
|
+
findings: Finding[];
|
|
169
|
+
/** Requests where reflection was found — should be escalated to Tier 2 */
|
|
170
|
+
reflectedRequests: ReflectedRequest[];
|
|
171
|
+
}
|
|
172
|
+
interface ReflectedRequest {
|
|
173
|
+
/** Original captured request */
|
|
174
|
+
request: CapturedRequest;
|
|
175
|
+
/** Payload that caused reflection */
|
|
176
|
+
payload: string;
|
|
177
|
+
/** Category of the payload */
|
|
178
|
+
category: PayloadCategory;
|
|
179
|
+
}
|
|
180
|
+
interface HttpScanOptions {
|
|
181
|
+
/** Request timeout in ms (default: 10000) */
|
|
182
|
+
timeout?: number;
|
|
183
|
+
/** Max concurrent requests (default: 10) */
|
|
184
|
+
concurrency?: number;
|
|
185
|
+
/** Cookie header to send with requests (for auth) */
|
|
186
|
+
cookies?: string;
|
|
187
|
+
/** Extra headers to send */
|
|
188
|
+
headers?: Record<string, string>;
|
|
189
|
+
/** Callback for progress reporting */
|
|
190
|
+
onProgress?: (completed: number, total: number) => void;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Run Tier 1 HTTP-level scan on captured requests.
|
|
194
|
+
*
|
|
195
|
+
* For each CapturedRequest × each payload:
|
|
196
|
+
* 1. Substitute the payload into the injectable field
|
|
197
|
+
* 2. Send via fetch()
|
|
198
|
+
* 3. Check response body for reflection patterns
|
|
199
|
+
* 4. If reflected, add finding + mark for Tier 2 escalation
|
|
200
|
+
*/
|
|
201
|
+
declare function httpScan(requests: CapturedRequest[], payloads: RuntimePayload[], options?: HttpScanOptions): Promise<HttpScanResult>;
|
|
202
|
+
/**
|
|
203
|
+
* Convert discovered forms into CapturedRequest metadata.
|
|
204
|
+
*
|
|
205
|
+
* Called by the crawler after form discovery. Each injectable form
|
|
206
|
+
* produces one CapturedRequest per injectable input field.
|
|
207
|
+
*/
|
|
208
|
+
declare function buildCapturedRequests(forms: Array<{
|
|
209
|
+
pageUrl: string;
|
|
210
|
+
action: string;
|
|
211
|
+
method: string;
|
|
212
|
+
inputs: Array<{
|
|
213
|
+
name: string;
|
|
214
|
+
injectable: boolean;
|
|
215
|
+
type: string;
|
|
216
|
+
}>;
|
|
217
|
+
sessionName: string;
|
|
218
|
+
}>): CapturedRequest[];
|
|
130
219
|
|
|
131
220
|
/**
|
|
132
221
|
* Login Form Auto-Detection & Auth Replay
|
|
@@ -403,4 +492,4 @@ type BrowserStep = z.infer<typeof BrowserStepSchema>;
|
|
|
403
492
|
*/
|
|
404
493
|
declare const browserDriver: VulcnDriver;
|
|
405
494
|
|
|
406
|
-
export { BROWSER_STEP_TYPES, type BrowserConfig, BrowserRecorder, BrowserRunner, type BrowserStep, BrowserStepSchema, type BrowserStepType, type LoginForm, type LoginResult, checkBrowsers, checkSessionAlive, configSchema, crawlAndBuildSessions, browserDriver as default, detectLoginForm, installBrowsers, launchBrowser, performLogin };
|
|
495
|
+
export { BROWSER_STEP_TYPES, type BrowserConfig, BrowserRecorder, BrowserRunner, type BrowserStep, BrowserStepSchema, type BrowserStepType, type CrawlResult, type HttpScanOptions, type HttpScanResult, type LoginForm, type LoginResult, type ReflectedRequest, buildCapturedRequests, checkBrowsers, checkSessionAlive, configSchema, crawlAndBuildSessions, browserDriver as default, detectLoginForm, httpScan, installBrowsers, launchBrowser, performLogin };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { RecordOptions, RecordingHandle, Session, RunContext, RunResult, CrawlOptions, FormCredentials, VulcnDriver } from '@vulcn/engine';
|
|
2
|
+
import { RecordOptions, RecordingHandle, Session, RunContext, RunResult, CapturedRequest, CrawlOptions, Finding, PayloadCategory, RuntimePayload, FormCredentials, VulcnDriver } from '@vulcn/engine';
|
|
3
3
|
import { Browser, Page, BrowserContext } from 'playwright';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -103,6 +103,7 @@ declare function checkBrowsers(): Promise<{
|
|
|
103
103
|
* - Links to follow for deeper crawling
|
|
104
104
|
*
|
|
105
105
|
* Outputs Session[] that are directly compatible with BrowserRunner.
|
|
106
|
+
* Also generates CapturedRequest[] metadata for Tier 1 HTTP fast scanning.
|
|
106
107
|
*
|
|
107
108
|
* This is the "auto-record" mode — instead of a human clicking around,
|
|
108
109
|
* the crawler automatically discovers injection points.
|
|
@@ -121,12 +122,100 @@ interface BrowserCrawlConfig {
|
|
|
121
122
|
height: number;
|
|
122
123
|
};
|
|
123
124
|
}
|
|
125
|
+
/** Result from crawling — sessions for browser replay + requests for HTTP scanning */
|
|
126
|
+
interface CrawlResult {
|
|
127
|
+
sessions: Session[];
|
|
128
|
+
capturedRequests: CapturedRequest[];
|
|
129
|
+
}
|
|
124
130
|
/**
|
|
125
131
|
* Crawl a URL and generate sessions.
|
|
126
132
|
*
|
|
127
133
|
* This is called by the browser driver's recorder.crawl() method.
|
|
134
|
+
* Returns both Session[] for Tier 2 browser replay and
|
|
135
|
+
* CapturedRequest[] for Tier 1 HTTP fast scanning.
|
|
128
136
|
*/
|
|
129
|
-
declare function crawlAndBuildSessions(config: BrowserCrawlConfig, options?: CrawlOptions): Promise<
|
|
137
|
+
declare function crawlAndBuildSessions(config: BrowserCrawlConfig, options?: CrawlOptions): Promise<CrawlResult>;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* HTTP Scanner — Tier 1 Fast Scan
|
|
141
|
+
*
|
|
142
|
+
* Replays captured HTTP requests via fetch() instead of Playwright.
|
|
143
|
+
* Substitutes security payloads into injectable form fields and checks
|
|
144
|
+
* the response body for payload reflection or error patterns.
|
|
145
|
+
*
|
|
146
|
+
* Speed: ~50ms per payload (vs 2-5s for browser replay)
|
|
147
|
+
*
|
|
148
|
+
* Catches:
|
|
149
|
+
* - Reflected XSS (payload appears in response body)
|
|
150
|
+
* - Error-based SQLi (SQL error patterns in response)
|
|
151
|
+
* - Server-side reflection of any payload
|
|
152
|
+
*
|
|
153
|
+
* Misses:
|
|
154
|
+
* - DOM-based XSS (requires JS execution)
|
|
155
|
+
* - Client-side state bugs
|
|
156
|
+
* - CSP-blocked attacks
|
|
157
|
+
*
|
|
158
|
+
* When reflection IS found, the finding is marked as `needsBrowserConfirmation`
|
|
159
|
+
* so the caller can escalate to Tier 2 for execution-based proof.
|
|
160
|
+
*/
|
|
161
|
+
|
|
162
|
+
interface HttpScanResult {
|
|
163
|
+
/** Total HTTP requests sent */
|
|
164
|
+
requestsSent: number;
|
|
165
|
+
/** How long the scan took (ms) */
|
|
166
|
+
duration: number;
|
|
167
|
+
/** Findings from reflection detection */
|
|
168
|
+
findings: Finding[];
|
|
169
|
+
/** Requests where reflection was found — should be escalated to Tier 2 */
|
|
170
|
+
reflectedRequests: ReflectedRequest[];
|
|
171
|
+
}
|
|
172
|
+
interface ReflectedRequest {
|
|
173
|
+
/** Original captured request */
|
|
174
|
+
request: CapturedRequest;
|
|
175
|
+
/** Payload that caused reflection */
|
|
176
|
+
payload: string;
|
|
177
|
+
/** Category of the payload */
|
|
178
|
+
category: PayloadCategory;
|
|
179
|
+
}
|
|
180
|
+
interface HttpScanOptions {
|
|
181
|
+
/** Request timeout in ms (default: 10000) */
|
|
182
|
+
timeout?: number;
|
|
183
|
+
/** Max concurrent requests (default: 10) */
|
|
184
|
+
concurrency?: number;
|
|
185
|
+
/** Cookie header to send with requests (for auth) */
|
|
186
|
+
cookies?: string;
|
|
187
|
+
/** Extra headers to send */
|
|
188
|
+
headers?: Record<string, string>;
|
|
189
|
+
/** Callback for progress reporting */
|
|
190
|
+
onProgress?: (completed: number, total: number) => void;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Run Tier 1 HTTP-level scan on captured requests.
|
|
194
|
+
*
|
|
195
|
+
* For each CapturedRequest × each payload:
|
|
196
|
+
* 1. Substitute the payload into the injectable field
|
|
197
|
+
* 2. Send via fetch()
|
|
198
|
+
* 3. Check response body for reflection patterns
|
|
199
|
+
* 4. If reflected, add finding + mark for Tier 2 escalation
|
|
200
|
+
*/
|
|
201
|
+
declare function httpScan(requests: CapturedRequest[], payloads: RuntimePayload[], options?: HttpScanOptions): Promise<HttpScanResult>;
|
|
202
|
+
/**
|
|
203
|
+
* Convert discovered forms into CapturedRequest metadata.
|
|
204
|
+
*
|
|
205
|
+
* Called by the crawler after form discovery. Each injectable form
|
|
206
|
+
* produces one CapturedRequest per injectable input field.
|
|
207
|
+
*/
|
|
208
|
+
declare function buildCapturedRequests(forms: Array<{
|
|
209
|
+
pageUrl: string;
|
|
210
|
+
action: string;
|
|
211
|
+
method: string;
|
|
212
|
+
inputs: Array<{
|
|
213
|
+
name: string;
|
|
214
|
+
injectable: boolean;
|
|
215
|
+
type: string;
|
|
216
|
+
}>;
|
|
217
|
+
sessionName: string;
|
|
218
|
+
}>): CapturedRequest[];
|
|
130
219
|
|
|
131
220
|
/**
|
|
132
221
|
* Login Form Auto-Detection & Auth Replay
|
|
@@ -403,4 +492,4 @@ type BrowserStep = z.infer<typeof BrowserStepSchema>;
|
|
|
403
492
|
*/
|
|
404
493
|
declare const browserDriver: VulcnDriver;
|
|
405
494
|
|
|
406
|
-
export { BROWSER_STEP_TYPES, type BrowserConfig, BrowserRecorder, BrowserRunner, type BrowserStep, BrowserStepSchema, type BrowserStepType, type LoginForm, type LoginResult, checkBrowsers, checkSessionAlive, configSchema, crawlAndBuildSessions, browserDriver as default, detectLoginForm, installBrowsers, launchBrowser, performLogin };
|
|
495
|
+
export { BROWSER_STEP_TYPES, type BrowserConfig, BrowserRecorder, BrowserRunner, type BrowserStep, BrowserStepSchema, type BrowserStepType, type CrawlResult, type HttpScanOptions, type HttpScanResult, type LoginForm, type LoginResult, type ReflectedRequest, buildCapturedRequests, checkBrowsers, checkSessionAlive, configSchema, crawlAndBuildSessions, browserDriver as default, detectLoginForm, httpScan, installBrowsers, launchBrowser, performLogin };
|
package/dist/index.js
CHANGED
|
@@ -778,6 +778,248 @@ function interleavePayloads(payloads) {
|
|
|
778
778
|
return result;
|
|
779
779
|
}
|
|
780
780
|
|
|
781
|
+
// src/http-scanner.ts
|
|
782
|
+
async function httpScan(requests, payloads, options = {}) {
|
|
783
|
+
const timeout = options.timeout ?? 1e4;
|
|
784
|
+
const concurrency = options.concurrency ?? 10;
|
|
785
|
+
const start = Date.now();
|
|
786
|
+
const findings = [];
|
|
787
|
+
const reflectedRequests = [];
|
|
788
|
+
let requestsSent = 0;
|
|
789
|
+
const tasks = [];
|
|
790
|
+
for (const request of requests) {
|
|
791
|
+
if (!request.injectableField) continue;
|
|
792
|
+
for (const payloadSet of payloads) {
|
|
793
|
+
for (const value of payloadSet.payloads) {
|
|
794
|
+
tasks.push({ request, payloadSet, value });
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
const totalTasks = tasks.length;
|
|
799
|
+
if (totalTasks === 0) {
|
|
800
|
+
return { requestsSent: 0, duration: 0, findings, reflectedRequests };
|
|
801
|
+
}
|
|
802
|
+
for (let i = 0; i < tasks.length; i += concurrency) {
|
|
803
|
+
const batch = tasks.slice(i, i + concurrency);
|
|
804
|
+
const results = await Promise.allSettled(
|
|
805
|
+
batch.map(async ({ request, payloadSet, value }) => {
|
|
806
|
+
try {
|
|
807
|
+
const body = await sendPayload(request, value, {
|
|
808
|
+
timeout,
|
|
809
|
+
cookies: options.cookies,
|
|
810
|
+
headers: options.headers
|
|
811
|
+
});
|
|
812
|
+
requestsSent++;
|
|
813
|
+
const finding = checkHttpReflection(body, request, payloadSet, value);
|
|
814
|
+
if (finding) {
|
|
815
|
+
findings.push(finding);
|
|
816
|
+
reflectedRequests.push({
|
|
817
|
+
request,
|
|
818
|
+
payload: value,
|
|
819
|
+
category: payloadSet.category
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
} catch {
|
|
823
|
+
requestsSent++;
|
|
824
|
+
}
|
|
825
|
+
})
|
|
826
|
+
);
|
|
827
|
+
const completed = Math.min(i + batch.length, totalTasks);
|
|
828
|
+
options.onProgress?.(completed, totalTasks);
|
|
829
|
+
for (const result of results) {
|
|
830
|
+
if (result.status === "rejected") {
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
requestsSent,
|
|
836
|
+
duration: Date.now() - start,
|
|
837
|
+
findings,
|
|
838
|
+
reflectedRequests
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
async function sendPayload(request, payload, options) {
|
|
842
|
+
const { method, url, headers, body, contentType, injectableField } = request;
|
|
843
|
+
const reqHeaders = {
|
|
844
|
+
...headers,
|
|
845
|
+
...options.headers ?? {}
|
|
846
|
+
};
|
|
847
|
+
if (options.cookies) {
|
|
848
|
+
reqHeaders["Cookie"] = options.cookies;
|
|
849
|
+
}
|
|
850
|
+
delete reqHeaders["content-length"];
|
|
851
|
+
delete reqHeaders["Content-Length"];
|
|
852
|
+
let requestUrl = url;
|
|
853
|
+
let requestBody;
|
|
854
|
+
if (method.toUpperCase() === "GET") {
|
|
855
|
+
requestUrl = injectIntoUrl(url, injectableField, payload);
|
|
856
|
+
} else {
|
|
857
|
+
requestBody = injectIntoBody(body, contentType, injectableField, payload);
|
|
858
|
+
if (contentType) {
|
|
859
|
+
reqHeaders["Content-Type"] = contentType;
|
|
860
|
+
} else {
|
|
861
|
+
reqHeaders["Content-Type"] = "application/x-www-form-urlencoded";
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
const controller = new AbortController();
|
|
865
|
+
const timer = setTimeout(() => controller.abort(), options.timeout);
|
|
866
|
+
try {
|
|
867
|
+
const response = await fetch(requestUrl, {
|
|
868
|
+
method: method.toUpperCase(),
|
|
869
|
+
headers: reqHeaders,
|
|
870
|
+
body: requestBody,
|
|
871
|
+
signal: controller.signal,
|
|
872
|
+
redirect: "follow"
|
|
873
|
+
});
|
|
874
|
+
return await response.text();
|
|
875
|
+
} finally {
|
|
876
|
+
clearTimeout(timer);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
function injectIntoUrl(url, field, payload) {
|
|
880
|
+
try {
|
|
881
|
+
const parsed = new URL(url);
|
|
882
|
+
parsed.searchParams.set(field, payload);
|
|
883
|
+
return parsed.toString();
|
|
884
|
+
} catch {
|
|
885
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
886
|
+
return `${url}${separator}${encodeURIComponent(field)}=${encodeURIComponent(payload)}`;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
function injectIntoBody(body, contentType, field, payload) {
|
|
890
|
+
if (!body) {
|
|
891
|
+
return `${encodeURIComponent(field)}=${encodeURIComponent(payload)}`;
|
|
892
|
+
}
|
|
893
|
+
const ct = (contentType ?? "").toLowerCase();
|
|
894
|
+
if (ct.includes("application/json")) {
|
|
895
|
+
return injectIntoJson(body, field, payload);
|
|
896
|
+
}
|
|
897
|
+
if (ct.includes("multipart/form-data")) {
|
|
898
|
+
return injectIntoMultipart(body, field, payload);
|
|
899
|
+
}
|
|
900
|
+
return injectIntoFormUrlEncoded(body, field, payload);
|
|
901
|
+
}
|
|
902
|
+
function injectIntoFormUrlEncoded(body, field, payload) {
|
|
903
|
+
const params = new URLSearchParams(body);
|
|
904
|
+
params.set(field, payload);
|
|
905
|
+
return params.toString();
|
|
906
|
+
}
|
|
907
|
+
function injectIntoJson(body, field, payload) {
|
|
908
|
+
try {
|
|
909
|
+
const parsed = JSON.parse(body);
|
|
910
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
911
|
+
parsed[field] = payload;
|
|
912
|
+
return JSON.stringify(parsed);
|
|
913
|
+
}
|
|
914
|
+
} catch {
|
|
915
|
+
}
|
|
916
|
+
const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
917
|
+
const regex = new RegExp(`("${escaped}"\\s*:\\s*)"[^"]*"`, "g");
|
|
918
|
+
const replaced = body.replace(regex, `$1"${payload}"`);
|
|
919
|
+
if (replaced !== body) return replaced;
|
|
920
|
+
return body;
|
|
921
|
+
}
|
|
922
|
+
function injectIntoMultipart(body, field, payload) {
|
|
923
|
+
const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
924
|
+
const regex = new RegExp(
|
|
925
|
+
`(Content-Disposition:\\s*form-data;\\s*name="${escaped}"\\r?\\n\\r?\\n)[^\\r\\n-]*`,
|
|
926
|
+
"i"
|
|
927
|
+
);
|
|
928
|
+
return body.replace(regex, `$1${payload}`);
|
|
929
|
+
}
|
|
930
|
+
function checkHttpReflection(responseBody, request, payloadSet, payloadValue) {
|
|
931
|
+
for (const pattern of payloadSet.detectPatterns) {
|
|
932
|
+
if (pattern.test(responseBody)) {
|
|
933
|
+
return {
|
|
934
|
+
type: payloadSet.category,
|
|
935
|
+
severity: getSeverity2(payloadSet.category),
|
|
936
|
+
title: `${payloadSet.category.toUpperCase()} reflection detected (HTTP)`,
|
|
937
|
+
description: `Payload pattern was reflected in HTTP response body. Needs browser confirmation for execution proof.`,
|
|
938
|
+
stepId: `http-${request.sessionName}`,
|
|
939
|
+
payload: payloadValue,
|
|
940
|
+
url: request.url,
|
|
941
|
+
evidence: responseBody.match(pattern)?.[0]?.slice(0, 200),
|
|
942
|
+
metadata: {
|
|
943
|
+
detectionMethod: "tier1-http",
|
|
944
|
+
needsBrowserConfirmation: true,
|
|
945
|
+
requestMethod: request.method,
|
|
946
|
+
injectableField: request.injectableField
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
if (responseBody.includes(payloadValue)) {
|
|
952
|
+
return {
|
|
953
|
+
type: payloadSet.category,
|
|
954
|
+
severity: "medium",
|
|
955
|
+
title: `Potential ${payloadSet.category.toUpperCase()} \u2014 payload reflected in HTTP response`,
|
|
956
|
+
description: `Payload was reflected in HTTP response without encoding. Escalate to browser for execution proof.`,
|
|
957
|
+
stepId: `http-${request.sessionName}`,
|
|
958
|
+
payload: payloadValue,
|
|
959
|
+
url: request.url,
|
|
960
|
+
metadata: {
|
|
961
|
+
detectionMethod: "tier1-http",
|
|
962
|
+
needsBrowserConfirmation: true,
|
|
963
|
+
requestMethod: request.method,
|
|
964
|
+
injectableField: request.injectableField
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
return void 0;
|
|
969
|
+
}
|
|
970
|
+
function getSeverity2(category) {
|
|
971
|
+
switch (category) {
|
|
972
|
+
case "sqli":
|
|
973
|
+
case "command-injection":
|
|
974
|
+
case "xxe":
|
|
975
|
+
return "critical";
|
|
976
|
+
case "xss":
|
|
977
|
+
case "ssrf":
|
|
978
|
+
case "path-traversal":
|
|
979
|
+
return "high";
|
|
980
|
+
case "open-redirect":
|
|
981
|
+
return "medium";
|
|
982
|
+
default:
|
|
983
|
+
return "medium";
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
function buildCapturedRequests(forms) {
|
|
987
|
+
const requests = [];
|
|
988
|
+
for (const form of forms) {
|
|
989
|
+
const injectableInputs = form.inputs.filter((i) => i.injectable);
|
|
990
|
+
if (injectableInputs.length === 0) continue;
|
|
991
|
+
let actionUrl;
|
|
992
|
+
try {
|
|
993
|
+
actionUrl = new URL(form.action, form.pageUrl).toString();
|
|
994
|
+
} catch {
|
|
995
|
+
actionUrl = form.pageUrl;
|
|
996
|
+
}
|
|
997
|
+
const method = (form.method || "GET").toUpperCase();
|
|
998
|
+
for (const input of injectableInputs) {
|
|
999
|
+
const formParams = new URLSearchParams();
|
|
1000
|
+
for (const inp of form.inputs) {
|
|
1001
|
+
formParams.set(inp.name || inp.type, inp.injectable ? "test" : "");
|
|
1002
|
+
}
|
|
1003
|
+
const request = {
|
|
1004
|
+
method,
|
|
1005
|
+
url: method === "GET" ? actionUrl : actionUrl,
|
|
1006
|
+
headers: {
|
|
1007
|
+
"User-Agent": "Vulcn/1.0 (Security Scanner)",
|
|
1008
|
+
Accept: "text/html,application/xhtml+xml,*/*"
|
|
1009
|
+
},
|
|
1010
|
+
...method !== "GET" ? {
|
|
1011
|
+
body: formParams.toString(),
|
|
1012
|
+
contentType: "application/x-www-form-urlencoded"
|
|
1013
|
+
} : {},
|
|
1014
|
+
injectableField: input.name || input.type,
|
|
1015
|
+
sessionName: form.sessionName
|
|
1016
|
+
};
|
|
1017
|
+
requests.push(request);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
return requests;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
781
1023
|
// src/crawler.ts
|
|
782
1024
|
var INJECTABLE_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
783
1025
|
"text",
|
|
@@ -864,7 +1106,20 @@ async function crawlAndBuildSessions(config, options = {}) {
|
|
|
864
1106
|
console.log(
|
|
865
1107
|
`[crawler] Complete: ${visited.size} page(s), ${allForms.length} form(s)`
|
|
866
1108
|
);
|
|
867
|
-
|
|
1109
|
+
const sessions = buildSessions(allForms);
|
|
1110
|
+
const capturedRequests = buildCapturedRequests(
|
|
1111
|
+
allForms.filter((f) => f.inputs.some((i) => i.injectable)).map((form, idx) => ({
|
|
1112
|
+
pageUrl: form.pageUrl,
|
|
1113
|
+
action: form.action,
|
|
1114
|
+
method: form.method,
|
|
1115
|
+
inputs: form.inputs,
|
|
1116
|
+
sessionName: sessions[idx]?.name ?? `form-${idx + 1}`
|
|
1117
|
+
}))
|
|
1118
|
+
);
|
|
1119
|
+
console.log(
|
|
1120
|
+
`[crawler] Generated ${sessions.length} session(s), ${capturedRequests.length} HTTP request(s) for Tier 1`
|
|
1121
|
+
);
|
|
1122
|
+
return { sessions, capturedRequests };
|
|
868
1123
|
}
|
|
869
1124
|
async function discoverForms(page, pageUrl) {
|
|
870
1125
|
const forms = [];
|
|
@@ -1377,7 +1632,7 @@ var recorderDriver = {
|
|
|
1377
1632
|
},
|
|
1378
1633
|
async crawl(config, options) {
|
|
1379
1634
|
const parsedConfig = configSchema.parse(config);
|
|
1380
|
-
|
|
1635
|
+
const result = await crawlAndBuildSessions(
|
|
1381
1636
|
{
|
|
1382
1637
|
startUrl: parsedConfig.startUrl ?? "",
|
|
1383
1638
|
browser: parsedConfig.browser,
|
|
@@ -1386,6 +1641,7 @@ var recorderDriver = {
|
|
|
1386
1641
|
},
|
|
1387
1642
|
options
|
|
1388
1643
|
);
|
|
1644
|
+
return result.sessions;
|
|
1389
1645
|
}
|
|
1390
1646
|
};
|
|
1391
1647
|
var runnerDriver = {
|
|
@@ -1409,12 +1665,14 @@ export {
|
|
|
1409
1665
|
BrowserRecorder,
|
|
1410
1666
|
BrowserRunner,
|
|
1411
1667
|
BrowserStepSchema,
|
|
1668
|
+
buildCapturedRequests,
|
|
1412
1669
|
checkBrowsers,
|
|
1413
1670
|
checkSessionAlive,
|
|
1414
1671
|
configSchema,
|
|
1415
1672
|
crawlAndBuildSessions,
|
|
1416
1673
|
index_default as default,
|
|
1417
1674
|
detectLoginForm,
|
|
1675
|
+
httpScan,
|
|
1418
1676
|
installBrowsers,
|
|
1419
1677
|
launchBrowser,
|
|
1420
1678
|
performLogin
|