@vulcn/driver-browser 0.2.0 → 0.3.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 +487 -192
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +90 -21
- package/dist/index.d.ts +90 -21
- package/dist/index.js +483 -191
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -368,9 +368,12 @@ var BrowserRecorder = class _BrowserRecorder {
|
|
|
368
368
|
};
|
|
369
369
|
|
|
370
370
|
// src/runner.ts
|
|
371
|
-
var BrowserRunner = class
|
|
371
|
+
var BrowserRunner = class {
|
|
372
372
|
/**
|
|
373
|
-
* Execute a session with security payloads
|
|
373
|
+
* Execute a session with security payloads.
|
|
374
|
+
*
|
|
375
|
+
* If ctx.options.browser is provided, reuses that browser (persistent mode).
|
|
376
|
+
* Otherwise, launches and closes its own browser (standalone mode).
|
|
374
377
|
*/
|
|
375
378
|
static async execute(session, ctx) {
|
|
376
379
|
const config = session.driverConfig;
|
|
@@ -396,83 +399,39 @@ var BrowserRunner = class _BrowserRunner {
|
|
|
396
399
|
]
|
|
397
400
|
};
|
|
398
401
|
}
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const
|
|
402
|
+
const sharedBrowser = ctx.options.browser;
|
|
403
|
+
const ownBrowser = sharedBrowser ? null : (await launchBrowser({ browser: browserType, headless })).browser;
|
|
404
|
+
const browser = sharedBrowser ?? ownBrowser;
|
|
405
|
+
const storageState = ctx.options.storageState;
|
|
406
|
+
const contextOptions = { viewport };
|
|
407
|
+
if (storageState) {
|
|
408
|
+
contextOptions.storageState = JSON.parse(storageState);
|
|
409
|
+
}
|
|
410
|
+
const context = await browser.newContext(contextOptions);
|
|
404
411
|
const page = await context.newPage();
|
|
405
412
|
await ctx.options.onPageReady?.(page);
|
|
406
413
|
const eventFindings = [];
|
|
407
414
|
let currentPayloadInfo = null;
|
|
408
|
-
const dialogHandler =
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
description: `JavaScript ${dialogType}() dialog was triggered by payload injection. Message: "${message}"`,
|
|
418
|
-
stepId: currentPayloadInfo.stepId,
|
|
419
|
-
payload: currentPayloadInfo.payloadValue,
|
|
420
|
-
url: page.url(),
|
|
421
|
-
evidence: `Dialog type: ${dialogType}, Message: ${message}`,
|
|
422
|
-
metadata: {
|
|
423
|
-
dialogType,
|
|
424
|
-
dialogMessage: message,
|
|
425
|
-
detectionMethod: "dialog"
|
|
426
|
-
}
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
try {
|
|
431
|
-
await dialog.dismiss();
|
|
432
|
-
} catch {
|
|
433
|
-
}
|
|
434
|
-
};
|
|
435
|
-
const consoleHandler = async (msg) => {
|
|
436
|
-
if (currentPayloadInfo && msg.type() === "log") {
|
|
437
|
-
const text = msg.text();
|
|
438
|
-
if (text.includes("vulcn") || text.includes(currentPayloadInfo.payloadValue)) {
|
|
439
|
-
eventFindings.push({
|
|
440
|
-
type: "xss",
|
|
441
|
-
severity: "high",
|
|
442
|
-
title: "XSS Confirmed - Console Output",
|
|
443
|
-
description: `JavaScript console.log was triggered by payload injection`,
|
|
444
|
-
stepId: currentPayloadInfo.stepId,
|
|
445
|
-
payload: currentPayloadInfo.payloadValue,
|
|
446
|
-
url: page.url(),
|
|
447
|
-
evidence: `Console output: ${text}`,
|
|
448
|
-
metadata: {
|
|
449
|
-
consoleType: msg.type(),
|
|
450
|
-
detectionMethod: "console"
|
|
451
|
-
}
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
};
|
|
415
|
+
const dialogHandler = createDialogHandler(
|
|
416
|
+
page,
|
|
417
|
+
eventFindings,
|
|
418
|
+
() => currentPayloadInfo
|
|
419
|
+
);
|
|
420
|
+
const consoleHandler = createConsoleHandler(
|
|
421
|
+
eventFindings,
|
|
422
|
+
() => currentPayloadInfo
|
|
423
|
+
);
|
|
456
424
|
page.on("dialog", dialogHandler);
|
|
457
425
|
page.on("console", consoleHandler);
|
|
458
426
|
try {
|
|
459
427
|
const injectableSteps = session.steps.filter(
|
|
460
428
|
(step) => step.type === "browser.input" && step.injectable !== false
|
|
461
429
|
);
|
|
462
|
-
const allPayloads =
|
|
463
|
-
const payloadsByCategory = payloads.map(
|
|
464
|
-
(ps) => ps.payloads.map((value) => ({ payloadSet: ps, value }))
|
|
465
|
-
);
|
|
466
|
-
const maxLen = Math.max(...payloadsByCategory.map((c) => c.length));
|
|
467
|
-
for (let i = 0; i < maxLen; i++) {
|
|
468
|
-
for (const category of payloadsByCategory) {
|
|
469
|
-
if (i < category.length) {
|
|
470
|
-
allPayloads.push(category[i]);
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
430
|
+
const allPayloads = interleavePayloads(payloads);
|
|
474
431
|
const confirmedTypes = /* @__PURE__ */ new Set();
|
|
475
432
|
for (const injectableStep of injectableSteps) {
|
|
433
|
+
let isFirstPayload = true;
|
|
434
|
+
let formPageUrl = null;
|
|
476
435
|
for (const { payloadSet, value } of allPayloads) {
|
|
477
436
|
const stepTypeKey = `${injectableStep.id}::${payloadSet.category}`;
|
|
478
437
|
if (confirmedTypes.has(stepTypeKey)) {
|
|
@@ -484,14 +443,35 @@ var BrowserRunner = class _BrowserRunner {
|
|
|
484
443
|
payloadSet,
|
|
485
444
|
payloadValue: value
|
|
486
445
|
};
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
446
|
+
if (isFirstPayload) {
|
|
447
|
+
await replayWithPayload(
|
|
448
|
+
page,
|
|
449
|
+
session,
|
|
450
|
+
injectableStep,
|
|
451
|
+
value,
|
|
452
|
+
startUrl
|
|
453
|
+
);
|
|
454
|
+
isFirstPayload = false;
|
|
455
|
+
formPageUrl = startUrl;
|
|
456
|
+
} else {
|
|
457
|
+
const cycled = await cyclePayload(
|
|
458
|
+
page,
|
|
459
|
+
session,
|
|
460
|
+
injectableStep,
|
|
461
|
+
value,
|
|
462
|
+
formPageUrl ?? startUrl
|
|
463
|
+
);
|
|
464
|
+
if (!cycled) {
|
|
465
|
+
await replayWithPayload(
|
|
466
|
+
page,
|
|
467
|
+
session,
|
|
468
|
+
injectableStep,
|
|
469
|
+
value,
|
|
470
|
+
startUrl
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const reflectionFinding = await checkReflection(
|
|
495
475
|
page,
|
|
496
476
|
injectableStep,
|
|
497
477
|
payloadSet,
|
|
@@ -517,6 +497,7 @@ var BrowserRunner = class _BrowserRunner {
|
|
|
517
497
|
ctx.options.onStepComplete?.(injectableStep.id, payloadsTested);
|
|
518
498
|
} catch (err) {
|
|
519
499
|
errors.push(`${injectableStep.id}: ${String(err)}`);
|
|
500
|
+
isFirstPayload = true;
|
|
520
501
|
}
|
|
521
502
|
}
|
|
522
503
|
}
|
|
@@ -525,7 +506,10 @@ var BrowserRunner = class _BrowserRunner {
|
|
|
525
506
|
page.off("console", consoleHandler);
|
|
526
507
|
currentPayloadInfo = null;
|
|
527
508
|
await ctx.options.onBeforeClose?.(page);
|
|
528
|
-
await
|
|
509
|
+
await context.close();
|
|
510
|
+
if (ownBrowser) {
|
|
511
|
+
await ownBrowser.close();
|
|
512
|
+
}
|
|
529
513
|
}
|
|
530
514
|
return {
|
|
531
515
|
findings: ctx.findings,
|
|
@@ -535,136 +519,264 @@ var BrowserRunner = class _BrowserRunner {
|
|
|
535
519
|
errors
|
|
536
520
|
};
|
|
537
521
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
case "browser.click":
|
|
560
|
-
if (injected) {
|
|
561
|
-
await Promise.all([
|
|
562
|
-
page.waitForNavigation({
|
|
563
|
-
waitUntil: "domcontentloaded",
|
|
564
|
-
timeout: 5e3
|
|
565
|
-
}).catch(() => {
|
|
566
|
-
}),
|
|
567
|
-
page.click(browserStep.selector, { timeout: 5e3 })
|
|
568
|
-
]);
|
|
569
|
-
} else {
|
|
570
|
-
await page.click(browserStep.selector, { timeout: 5e3 });
|
|
571
|
-
}
|
|
572
|
-
break;
|
|
573
|
-
case "browser.input": {
|
|
574
|
-
const value = step.id === targetStep.id ? payloadValue : browserStep.value;
|
|
575
|
-
await page.fill(browserStep.selector, value, { timeout: 5e3 });
|
|
576
|
-
if (step.id === targetStep.id) {
|
|
577
|
-
injected = true;
|
|
578
|
-
}
|
|
579
|
-
break;
|
|
522
|
+
};
|
|
523
|
+
function createDialogHandler(page, eventFindings, getPayloadInfo) {
|
|
524
|
+
return async (dialog) => {
|
|
525
|
+
const info = getPayloadInfo();
|
|
526
|
+
if (info) {
|
|
527
|
+
const message = dialog.message();
|
|
528
|
+
const dialogType = dialog.type();
|
|
529
|
+
if (dialogType !== "beforeunload") {
|
|
530
|
+
eventFindings.push({
|
|
531
|
+
type: "xss",
|
|
532
|
+
severity: "high",
|
|
533
|
+
title: `XSS Confirmed - ${dialogType}() triggered`,
|
|
534
|
+
description: `JavaScript ${dialogType}() dialog was triggered by payload injection. Message: "${message}"`,
|
|
535
|
+
stepId: info.stepId,
|
|
536
|
+
payload: info.payloadValue,
|
|
537
|
+
url: page.url(),
|
|
538
|
+
evidence: `Dialog type: ${dialogType}, Message: ${message}`,
|
|
539
|
+
metadata: {
|
|
540
|
+
dialogType,
|
|
541
|
+
dialogMessage: message,
|
|
542
|
+
detectionMethod: "dialog"
|
|
580
543
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
try {
|
|
548
|
+
await dialog.dismiss();
|
|
549
|
+
} catch {
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
function createConsoleHandler(eventFindings, getPayloadInfo) {
|
|
554
|
+
return async (msg) => {
|
|
555
|
+
const info = getPayloadInfo();
|
|
556
|
+
if (info && msg.type() === "log") {
|
|
557
|
+
const text = msg.text();
|
|
558
|
+
if (text.includes("vulcn") || text.includes(info.payloadValue)) {
|
|
559
|
+
eventFindings.push({
|
|
560
|
+
type: "xss",
|
|
561
|
+
severity: "high",
|
|
562
|
+
title: "XSS Confirmed - Console Output",
|
|
563
|
+
description: `JavaScript console.log was triggered by payload injection`,
|
|
564
|
+
stepId: info.stepId,
|
|
565
|
+
payload: info.payloadValue,
|
|
566
|
+
url: "",
|
|
567
|
+
evidence: `Console output: ${text}`,
|
|
568
|
+
metadata: {
|
|
569
|
+
consoleType: msg.type(),
|
|
570
|
+
detectionMethod: "console"
|
|
595
571
|
}
|
|
596
|
-
|
|
597
|
-
if (browserStep.selector) {
|
|
598
|
-
await page.locator(browserStep.selector).evaluate((el, pos) => {
|
|
599
|
-
el.scrollTo(pos.x, pos.y);
|
|
600
|
-
}, browserStep.position);
|
|
601
|
-
} else {
|
|
602
|
-
await page.evaluate((pos) => {
|
|
603
|
-
window.scrollTo(pos.x, pos.y);
|
|
604
|
-
}, browserStep.position);
|
|
605
|
-
}
|
|
606
|
-
break;
|
|
607
|
-
case "browser.wait":
|
|
608
|
-
await page.waitForTimeout(browserStep.duration);
|
|
609
|
-
break;
|
|
610
|
-
}
|
|
611
|
-
} catch {
|
|
572
|
+
});
|
|
612
573
|
}
|
|
613
574
|
}
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
async function cyclePayload(page, session, targetStep, payloadValue, formPageUrl) {
|
|
578
|
+
try {
|
|
579
|
+
await page.goBack({ waitUntil: "domcontentloaded", timeout: 5e3 });
|
|
580
|
+
const targetSelector = targetStep.selector;
|
|
581
|
+
const formPresent = await page.waitForSelector(targetSelector, { timeout: 3e3 }).then(() => true).catch(() => false);
|
|
582
|
+
if (!formPresent) {
|
|
583
|
+
await page.goto(formPageUrl, {
|
|
584
|
+
waitUntil: "domcontentloaded",
|
|
585
|
+
timeout: 5e3
|
|
586
|
+
});
|
|
587
|
+
const formPresentAfterNav = await page.waitForSelector(targetSelector, { timeout: 3e3 }).then(() => true).catch(() => false);
|
|
588
|
+
if (!formPresentAfterNav) {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
await page.fill(targetSelector, payloadValue, { timeout: 5e3 });
|
|
593
|
+
await replayStepsAfter(page, session, targetStep);
|
|
614
594
|
await page.waitForTimeout(500);
|
|
595
|
+
return true;
|
|
596
|
+
} catch {
|
|
597
|
+
return false;
|
|
615
598
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
599
|
+
}
|
|
600
|
+
async function replayWithPayload(page, session, targetStep, payloadValue, startUrl) {
|
|
601
|
+
await page.goto(startUrl, { waitUntil: "domcontentloaded" });
|
|
602
|
+
let injected = false;
|
|
603
|
+
for (const step of session.steps) {
|
|
604
|
+
const browserStep = step;
|
|
605
|
+
try {
|
|
606
|
+
switch (browserStep.type) {
|
|
607
|
+
case "browser.navigate":
|
|
608
|
+
if (injected && browserStep.url.includes("sid=")) {
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
await page.goto(browserStep.url, { waitUntil: "domcontentloaded" });
|
|
612
|
+
break;
|
|
613
|
+
case "browser.click":
|
|
614
|
+
if (injected) {
|
|
615
|
+
await Promise.all([
|
|
616
|
+
page.waitForNavigation({
|
|
617
|
+
waitUntil: "domcontentloaded",
|
|
618
|
+
timeout: 5e3
|
|
619
|
+
}).catch(() => {
|
|
620
|
+
}),
|
|
621
|
+
page.click(browserStep.selector, { timeout: 5e3 })
|
|
622
|
+
]);
|
|
623
|
+
} else {
|
|
624
|
+
await page.click(browserStep.selector, { timeout: 5e3 });
|
|
625
|
+
}
|
|
626
|
+
break;
|
|
627
|
+
case "browser.input": {
|
|
628
|
+
const value = step.id === targetStep.id ? payloadValue : browserStep.value;
|
|
629
|
+
await page.fill(browserStep.selector, value, { timeout: 5e3 });
|
|
630
|
+
if (step.id === targetStep.id) {
|
|
631
|
+
injected = true;
|
|
632
|
+
}
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
case "browser.keypress": {
|
|
636
|
+
const modifiers = browserStep.modifiers ?? [];
|
|
637
|
+
for (const mod of modifiers) {
|
|
638
|
+
await page.keyboard.down(
|
|
639
|
+
mod
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
await page.keyboard.press(browserStep.key);
|
|
643
|
+
for (const mod of modifiers.reverse()) {
|
|
644
|
+
await page.keyboard.up(mod);
|
|
645
|
+
}
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
case "browser.scroll":
|
|
649
|
+
if (browserStep.selector) {
|
|
650
|
+
await page.locator(browserStep.selector).evaluate((el, pos) => {
|
|
651
|
+
el.scrollTo(pos.x, pos.y);
|
|
652
|
+
}, browserStep.position);
|
|
653
|
+
} else {
|
|
654
|
+
await page.evaluate((pos) => {
|
|
655
|
+
window.scrollTo(pos.x, pos.y);
|
|
656
|
+
}, browserStep.position);
|
|
657
|
+
}
|
|
658
|
+
break;
|
|
659
|
+
case "browser.wait":
|
|
660
|
+
await page.waitForTimeout(browserStep.duration);
|
|
661
|
+
break;
|
|
633
662
|
}
|
|
663
|
+
} catch {
|
|
634
664
|
}
|
|
635
|
-
|
|
665
|
+
}
|
|
666
|
+
await page.waitForTimeout(500);
|
|
667
|
+
}
|
|
668
|
+
async function replayStepsAfter(page, session, targetStep) {
|
|
669
|
+
let pastTarget = false;
|
|
670
|
+
for (const step of session.steps) {
|
|
671
|
+
if (step.id === targetStep.id) {
|
|
672
|
+
pastTarget = true;
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
if (!pastTarget) continue;
|
|
676
|
+
const browserStep = step;
|
|
677
|
+
try {
|
|
678
|
+
switch (browserStep.type) {
|
|
679
|
+
case "browser.navigate":
|
|
680
|
+
break;
|
|
681
|
+
case "browser.click":
|
|
682
|
+
await Promise.all([
|
|
683
|
+
page.waitForNavigation({
|
|
684
|
+
waitUntil: "domcontentloaded",
|
|
685
|
+
timeout: 5e3
|
|
686
|
+
}).catch(() => {
|
|
687
|
+
}),
|
|
688
|
+
page.click(browserStep.selector, { timeout: 5e3 })
|
|
689
|
+
]);
|
|
690
|
+
break;
|
|
691
|
+
case "browser.input":
|
|
692
|
+
await page.fill(browserStep.selector, browserStep.value, {
|
|
693
|
+
timeout: 5e3
|
|
694
|
+
});
|
|
695
|
+
break;
|
|
696
|
+
case "browser.keypress": {
|
|
697
|
+
const modifiers = browserStep.modifiers ?? [];
|
|
698
|
+
for (const mod of modifiers) {
|
|
699
|
+
await page.keyboard.down(
|
|
700
|
+
mod
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
await page.keyboard.press(browserStep.key);
|
|
704
|
+
for (const mod of modifiers.reverse()) {
|
|
705
|
+
await page.keyboard.up(mod);
|
|
706
|
+
}
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
case "browser.scroll":
|
|
710
|
+
break;
|
|
711
|
+
// skip scrolls in fast path
|
|
712
|
+
case "browser.wait":
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
} catch {
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
await page.waitForTimeout(500);
|
|
719
|
+
}
|
|
720
|
+
async function checkReflection(page, step, payloadSet, payloadValue) {
|
|
721
|
+
const content = await page.content();
|
|
722
|
+
for (const pattern of payloadSet.detectPatterns) {
|
|
723
|
+
if (pattern.test(content)) {
|
|
636
724
|
return {
|
|
637
725
|
type: payloadSet.category,
|
|
638
|
-
severity:
|
|
639
|
-
title:
|
|
640
|
-
description: `Payload was reflected in page
|
|
726
|
+
severity: getSeverity(payloadSet.category),
|
|
727
|
+
title: `${payloadSet.category.toUpperCase()} vulnerability detected`,
|
|
728
|
+
description: `Payload pattern was reflected in page content`,
|
|
641
729
|
stepId: step.id,
|
|
642
730
|
payload: payloadValue,
|
|
643
|
-
url: page.url()
|
|
731
|
+
url: page.url(),
|
|
732
|
+
evidence: content.match(pattern)?.[0]?.slice(0, 200)
|
|
644
733
|
};
|
|
645
734
|
}
|
|
646
|
-
return void 0;
|
|
647
735
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
736
|
+
if (content.includes(payloadValue)) {
|
|
737
|
+
return {
|
|
738
|
+
type: payloadSet.category,
|
|
739
|
+
severity: "medium",
|
|
740
|
+
title: `Potential ${payloadSet.category.toUpperCase()} - payload reflection`,
|
|
741
|
+
description: `Payload was reflected in page without encoding`,
|
|
742
|
+
stepId: step.id,
|
|
743
|
+
payload: payloadValue,
|
|
744
|
+
url: page.url()
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
return void 0;
|
|
748
|
+
}
|
|
749
|
+
function getSeverity(category) {
|
|
750
|
+
switch (category) {
|
|
751
|
+
case "sqli":
|
|
752
|
+
case "command-injection":
|
|
753
|
+
case "xxe":
|
|
754
|
+
return "critical";
|
|
755
|
+
case "xss":
|
|
756
|
+
case "ssrf":
|
|
757
|
+
case "path-traversal":
|
|
758
|
+
return "high";
|
|
759
|
+
case "open-redirect":
|
|
760
|
+
return "medium";
|
|
761
|
+
default:
|
|
762
|
+
return "medium";
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
function interleavePayloads(payloads) {
|
|
766
|
+
const result = [];
|
|
767
|
+
const payloadsByCategory = payloads.map(
|
|
768
|
+
(ps) => ps.payloads.map((value) => ({ payloadSet: ps, value }))
|
|
769
|
+
);
|
|
770
|
+
const maxLen = Math.max(...payloadsByCategory.map((c) => c.length));
|
|
771
|
+
for (let i = 0; i < maxLen; i++) {
|
|
772
|
+
for (const category of payloadsByCategory) {
|
|
773
|
+
if (i < category.length) {
|
|
774
|
+
result.push(category[i]);
|
|
775
|
+
}
|
|
665
776
|
}
|
|
666
777
|
}
|
|
667
|
-
|
|
778
|
+
return result;
|
|
779
|
+
}
|
|
668
780
|
|
|
669
781
|
// src/crawler.ts
|
|
670
782
|
var INJECTABLE_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -701,7 +813,8 @@ async function crawlAndBuildSessions(config, options = {}) {
|
|
|
701
813
|
headless: config.headless ?? true
|
|
702
814
|
});
|
|
703
815
|
const context = await browser.newContext({
|
|
704
|
-
viewport: config.viewport ?? { width: 1280, height: 720 }
|
|
816
|
+
viewport: config.viewport ?? { width: 1280, height: 720 },
|
|
817
|
+
...options.storageState ? { storageState: JSON.parse(options.storageState) } : {}
|
|
705
818
|
});
|
|
706
819
|
try {
|
|
707
820
|
while (queue.length > 0 && visited.size < opts.maxPages) {
|
|
@@ -1016,6 +1129,182 @@ function normalizeUrl(url) {
|
|
|
1016
1129
|
}
|
|
1017
1130
|
}
|
|
1018
1131
|
|
|
1132
|
+
// src/auth.ts
|
|
1133
|
+
async function detectLoginForm(page) {
|
|
1134
|
+
return page.evaluate(() => {
|
|
1135
|
+
function findUsernameInput(container) {
|
|
1136
|
+
const selectors = [
|
|
1137
|
+
'input[autocomplete="username"]',
|
|
1138
|
+
'input[autocomplete="email"]',
|
|
1139
|
+
'input[type="email"]',
|
|
1140
|
+
'input[name*="user" i]',
|
|
1141
|
+
'input[name*="login" i]',
|
|
1142
|
+
'input[name*="email" i]',
|
|
1143
|
+
'input[id*="user" i]',
|
|
1144
|
+
'input[id*="login" i]',
|
|
1145
|
+
'input[id*="email" i]',
|
|
1146
|
+
'input[name*="name" i]',
|
|
1147
|
+
'input[type="text"]'
|
|
1148
|
+
];
|
|
1149
|
+
for (const sel of selectors) {
|
|
1150
|
+
const el = container.querySelector(sel);
|
|
1151
|
+
if (el && el.type !== "password" && el.type !== "hidden") {
|
|
1152
|
+
return el;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
return null;
|
|
1156
|
+
}
|
|
1157
|
+
function findSubmitButton(container) {
|
|
1158
|
+
const selectors = [
|
|
1159
|
+
'button[type="submit"]',
|
|
1160
|
+
'input[type="submit"]',
|
|
1161
|
+
"button:not([type])",
|
|
1162
|
+
'button[type="button"]'
|
|
1163
|
+
];
|
|
1164
|
+
for (const sel of selectors) {
|
|
1165
|
+
const el = container.querySelector(sel);
|
|
1166
|
+
if (el) return el;
|
|
1167
|
+
}
|
|
1168
|
+
return null;
|
|
1169
|
+
}
|
|
1170
|
+
function getSelector(el) {
|
|
1171
|
+
if (el.id) return `#${CSS.escape(el.id)}`;
|
|
1172
|
+
if (el.getAttribute("name"))
|
|
1173
|
+
return `${el.tagName.toLowerCase()}[name="${CSS.escape(el.getAttribute("name"))}"]`;
|
|
1174
|
+
if (el.getAttribute("type") && el.tagName === "INPUT")
|
|
1175
|
+
return `input[type="${el.getAttribute("type")}"]`;
|
|
1176
|
+
const parent = el.parentElement;
|
|
1177
|
+
if (!parent) return el.tagName.toLowerCase();
|
|
1178
|
+
const siblings = Array.from(parent.children);
|
|
1179
|
+
const index = siblings.indexOf(el) + 1;
|
|
1180
|
+
return `${parent.tagName.toLowerCase()} > ${el.tagName.toLowerCase()}:nth-child(${index})`;
|
|
1181
|
+
}
|
|
1182
|
+
const forms = document.querySelectorAll("form");
|
|
1183
|
+
for (const form of forms) {
|
|
1184
|
+
const passwordInput2 = form.querySelector(
|
|
1185
|
+
'input[type="password"]'
|
|
1186
|
+
);
|
|
1187
|
+
if (!passwordInput2) continue;
|
|
1188
|
+
const usernameInput = findUsernameInput(form);
|
|
1189
|
+
const submitButton = findSubmitButton(form);
|
|
1190
|
+
return {
|
|
1191
|
+
usernameSelector: usernameInput ? getSelector(usernameInput) : 'input[type="text"]',
|
|
1192
|
+
passwordSelector: getSelector(passwordInput2),
|
|
1193
|
+
submitSelector: submitButton ? getSelector(submitButton) : null,
|
|
1194
|
+
autoDetected: true
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
const passwordInput = document.querySelector(
|
|
1198
|
+
'input[type="password"]'
|
|
1199
|
+
);
|
|
1200
|
+
if (passwordInput) {
|
|
1201
|
+
const usernameInput = findUsernameInput(document.body);
|
|
1202
|
+
const submitButton = findSubmitButton(document.body);
|
|
1203
|
+
return {
|
|
1204
|
+
usernameSelector: usernameInput ? getSelector(usernameInput) : 'input[type="text"]',
|
|
1205
|
+
passwordSelector: getSelector(passwordInput),
|
|
1206
|
+
submitSelector: submitButton ? getSelector(submitButton) : null,
|
|
1207
|
+
autoDetected: true
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
return null;
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
async function performLogin(page, context, credentials, options) {
|
|
1214
|
+
const loginUrl = credentials.loginUrl ?? options.targetUrl;
|
|
1215
|
+
await page.goto(loginUrl, { waitUntil: "domcontentloaded", timeout: 15e3 });
|
|
1216
|
+
let usernameSelector;
|
|
1217
|
+
let passwordSelector;
|
|
1218
|
+
let submitSelector;
|
|
1219
|
+
if (credentials.userSelector && credentials.passSelector) {
|
|
1220
|
+
usernameSelector = credentials.userSelector;
|
|
1221
|
+
passwordSelector = credentials.passSelector;
|
|
1222
|
+
submitSelector = null;
|
|
1223
|
+
} else {
|
|
1224
|
+
const form = await detectLoginForm(page);
|
|
1225
|
+
if (!form) {
|
|
1226
|
+
return {
|
|
1227
|
+
success: false,
|
|
1228
|
+
message: `No login form detected on ${loginUrl}. Use --user-field and --pass-field to specify selectors.`
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
usernameSelector = form.usernameSelector;
|
|
1232
|
+
passwordSelector = form.passwordSelector;
|
|
1233
|
+
submitSelector = form.submitSelector;
|
|
1234
|
+
}
|
|
1235
|
+
try {
|
|
1236
|
+
await page.fill(usernameSelector, credentials.username, { timeout: 5e3 });
|
|
1237
|
+
} catch {
|
|
1238
|
+
return {
|
|
1239
|
+
success: false,
|
|
1240
|
+
message: `Could not find username field: ${usernameSelector}`
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
try {
|
|
1244
|
+
await page.fill(passwordSelector, credentials.password, { timeout: 5e3 });
|
|
1245
|
+
} catch {
|
|
1246
|
+
return {
|
|
1247
|
+
success: false,
|
|
1248
|
+
message: `Could not find password field: ${passwordSelector}`
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
try {
|
|
1252
|
+
if (submitSelector) {
|
|
1253
|
+
await Promise.all([
|
|
1254
|
+
page.waitForNavigation({ waitUntil: "domcontentloaded", timeout: 1e4 }).catch(() => {
|
|
1255
|
+
}),
|
|
1256
|
+
page.click(submitSelector, { timeout: 5e3 })
|
|
1257
|
+
]);
|
|
1258
|
+
} else {
|
|
1259
|
+
await Promise.all([
|
|
1260
|
+
page.waitForNavigation({ waitUntil: "domcontentloaded", timeout: 1e4 }).catch(() => {
|
|
1261
|
+
}),
|
|
1262
|
+
page.press(passwordSelector, "Enter")
|
|
1263
|
+
]);
|
|
1264
|
+
}
|
|
1265
|
+
} catch {
|
|
1266
|
+
return {
|
|
1267
|
+
success: false,
|
|
1268
|
+
message: "Failed to submit login form"
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
await page.waitForTimeout(1e3);
|
|
1272
|
+
const bodyText = await page.textContent("body").catch(() => "");
|
|
1273
|
+
if (options.loggedOutIndicator && bodyText?.includes(options.loggedOutIndicator)) {
|
|
1274
|
+
return {
|
|
1275
|
+
success: false,
|
|
1276
|
+
message: `Login failed \u2014 "${options.loggedOutIndicator}" still visible on page`
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
if (options.loggedInIndicator && !bodyText?.includes(options.loggedInIndicator)) {
|
|
1280
|
+
return {
|
|
1281
|
+
success: false,
|
|
1282
|
+
message: `Login uncertain \u2014 "${options.loggedInIndicator}" not found on page`
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
const storageState = JSON.stringify(await context.storageState());
|
|
1286
|
+
return {
|
|
1287
|
+
success: true,
|
|
1288
|
+
message: "Login successful",
|
|
1289
|
+
storageState
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
async function checkSessionAlive(page, config) {
|
|
1293
|
+
try {
|
|
1294
|
+
const bodyText = await page.textContent("body");
|
|
1295
|
+
if (!bodyText) return true;
|
|
1296
|
+
if (config.loggedOutIndicator && bodyText.includes(config.loggedOutIndicator)) {
|
|
1297
|
+
return false;
|
|
1298
|
+
}
|
|
1299
|
+
if (config.loggedInIndicator && !bodyText.includes(config.loggedInIndicator)) {
|
|
1300
|
+
return false;
|
|
1301
|
+
}
|
|
1302
|
+
return true;
|
|
1303
|
+
} catch {
|
|
1304
|
+
return false;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1019
1308
|
// src/index.ts
|
|
1020
1309
|
var configSchema = z.object({
|
|
1021
1310
|
/** Starting URL for recording */
|
|
@@ -1121,10 +1410,13 @@ export {
|
|
|
1121
1410
|
BrowserRunner,
|
|
1122
1411
|
BrowserStepSchema,
|
|
1123
1412
|
checkBrowsers,
|
|
1413
|
+
checkSessionAlive,
|
|
1124
1414
|
configSchema,
|
|
1125
1415
|
crawlAndBuildSessions,
|
|
1126
1416
|
index_default as default,
|
|
1417
|
+
detectLoginForm,
|
|
1127
1418
|
installBrowsers,
|
|
1128
|
-
launchBrowser
|
|
1419
|
+
launchBrowser,
|
|
1420
|
+
performLogin
|
|
1129
1421
|
};
|
|
1130
1422
|
//# sourceMappingURL=index.js.map
|