@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.cjs
CHANGED
|
@@ -25,11 +25,14 @@ __export(index_exports, {
|
|
|
25
25
|
BrowserRunner: () => BrowserRunner,
|
|
26
26
|
BrowserStepSchema: () => BrowserStepSchema,
|
|
27
27
|
checkBrowsers: () => checkBrowsers,
|
|
28
|
+
checkSessionAlive: () => checkSessionAlive,
|
|
28
29
|
configSchema: () => configSchema,
|
|
29
30
|
crawlAndBuildSessions: () => crawlAndBuildSessions,
|
|
30
31
|
default: () => index_default,
|
|
32
|
+
detectLoginForm: () => detectLoginForm,
|
|
31
33
|
installBrowsers: () => installBrowsers,
|
|
32
|
-
launchBrowser: () => launchBrowser
|
|
34
|
+
launchBrowser: () => launchBrowser,
|
|
35
|
+
performLogin: () => performLogin
|
|
33
36
|
});
|
|
34
37
|
module.exports = __toCommonJS(index_exports);
|
|
35
38
|
var import_zod = require("zod");
|
|
@@ -401,9 +404,12 @@ var BrowserRecorder = class _BrowserRecorder {
|
|
|
401
404
|
};
|
|
402
405
|
|
|
403
406
|
// src/runner.ts
|
|
404
|
-
var BrowserRunner = class
|
|
407
|
+
var BrowserRunner = class {
|
|
405
408
|
/**
|
|
406
|
-
* Execute a session with security payloads
|
|
409
|
+
* Execute a session with security payloads.
|
|
410
|
+
*
|
|
411
|
+
* If ctx.options.browser is provided, reuses that browser (persistent mode).
|
|
412
|
+
* Otherwise, launches and closes its own browser (standalone mode).
|
|
407
413
|
*/
|
|
408
414
|
static async execute(session, ctx) {
|
|
409
415
|
const config = session.driverConfig;
|
|
@@ -429,83 +435,39 @@ var BrowserRunner = class _BrowserRunner {
|
|
|
429
435
|
]
|
|
430
436
|
};
|
|
431
437
|
}
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const
|
|
438
|
+
const sharedBrowser = ctx.options.browser;
|
|
439
|
+
const ownBrowser = sharedBrowser ? null : (await launchBrowser({ browser: browserType, headless })).browser;
|
|
440
|
+
const browser = sharedBrowser ?? ownBrowser;
|
|
441
|
+
const storageState = ctx.options.storageState;
|
|
442
|
+
const contextOptions = { viewport };
|
|
443
|
+
if (storageState) {
|
|
444
|
+
contextOptions.storageState = JSON.parse(storageState);
|
|
445
|
+
}
|
|
446
|
+
const context = await browser.newContext(contextOptions);
|
|
437
447
|
const page = await context.newPage();
|
|
438
448
|
await ctx.options.onPageReady?.(page);
|
|
439
449
|
const eventFindings = [];
|
|
440
450
|
let currentPayloadInfo = null;
|
|
441
|
-
const dialogHandler =
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
description: `JavaScript ${dialogType}() dialog was triggered by payload injection. Message: "${message}"`,
|
|
451
|
-
stepId: currentPayloadInfo.stepId,
|
|
452
|
-
payload: currentPayloadInfo.payloadValue,
|
|
453
|
-
url: page.url(),
|
|
454
|
-
evidence: `Dialog type: ${dialogType}, Message: ${message}`,
|
|
455
|
-
metadata: {
|
|
456
|
-
dialogType,
|
|
457
|
-
dialogMessage: message,
|
|
458
|
-
detectionMethod: "dialog"
|
|
459
|
-
}
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
try {
|
|
464
|
-
await dialog.dismiss();
|
|
465
|
-
} catch {
|
|
466
|
-
}
|
|
467
|
-
};
|
|
468
|
-
const consoleHandler = async (msg) => {
|
|
469
|
-
if (currentPayloadInfo && msg.type() === "log") {
|
|
470
|
-
const text = msg.text();
|
|
471
|
-
if (text.includes("vulcn") || text.includes(currentPayloadInfo.payloadValue)) {
|
|
472
|
-
eventFindings.push({
|
|
473
|
-
type: "xss",
|
|
474
|
-
severity: "high",
|
|
475
|
-
title: "XSS Confirmed - Console Output",
|
|
476
|
-
description: `JavaScript console.log was triggered by payload injection`,
|
|
477
|
-
stepId: currentPayloadInfo.stepId,
|
|
478
|
-
payload: currentPayloadInfo.payloadValue,
|
|
479
|
-
url: page.url(),
|
|
480
|
-
evidence: `Console output: ${text}`,
|
|
481
|
-
metadata: {
|
|
482
|
-
consoleType: msg.type(),
|
|
483
|
-
detectionMethod: "console"
|
|
484
|
-
}
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
};
|
|
451
|
+
const dialogHandler = createDialogHandler(
|
|
452
|
+
page,
|
|
453
|
+
eventFindings,
|
|
454
|
+
() => currentPayloadInfo
|
|
455
|
+
);
|
|
456
|
+
const consoleHandler = createConsoleHandler(
|
|
457
|
+
eventFindings,
|
|
458
|
+
() => currentPayloadInfo
|
|
459
|
+
);
|
|
489
460
|
page.on("dialog", dialogHandler);
|
|
490
461
|
page.on("console", consoleHandler);
|
|
491
462
|
try {
|
|
492
463
|
const injectableSteps = session.steps.filter(
|
|
493
464
|
(step) => step.type === "browser.input" && step.injectable !== false
|
|
494
465
|
);
|
|
495
|
-
const allPayloads =
|
|
496
|
-
const payloadsByCategory = payloads.map(
|
|
497
|
-
(ps) => ps.payloads.map((value) => ({ payloadSet: ps, value }))
|
|
498
|
-
);
|
|
499
|
-
const maxLen = Math.max(...payloadsByCategory.map((c) => c.length));
|
|
500
|
-
for (let i = 0; i < maxLen; i++) {
|
|
501
|
-
for (const category of payloadsByCategory) {
|
|
502
|
-
if (i < category.length) {
|
|
503
|
-
allPayloads.push(category[i]);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
466
|
+
const allPayloads = interleavePayloads(payloads);
|
|
507
467
|
const confirmedTypes = /* @__PURE__ */ new Set();
|
|
508
468
|
for (const injectableStep of injectableSteps) {
|
|
469
|
+
let isFirstPayload = true;
|
|
470
|
+
let formPageUrl = null;
|
|
509
471
|
for (const { payloadSet, value } of allPayloads) {
|
|
510
472
|
const stepTypeKey = `${injectableStep.id}::${payloadSet.category}`;
|
|
511
473
|
if (confirmedTypes.has(stepTypeKey)) {
|
|
@@ -517,14 +479,35 @@ var BrowserRunner = class _BrowserRunner {
|
|
|
517
479
|
payloadSet,
|
|
518
480
|
payloadValue: value
|
|
519
481
|
};
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
482
|
+
if (isFirstPayload) {
|
|
483
|
+
await replayWithPayload(
|
|
484
|
+
page,
|
|
485
|
+
session,
|
|
486
|
+
injectableStep,
|
|
487
|
+
value,
|
|
488
|
+
startUrl
|
|
489
|
+
);
|
|
490
|
+
isFirstPayload = false;
|
|
491
|
+
formPageUrl = startUrl;
|
|
492
|
+
} else {
|
|
493
|
+
const cycled = await cyclePayload(
|
|
494
|
+
page,
|
|
495
|
+
session,
|
|
496
|
+
injectableStep,
|
|
497
|
+
value,
|
|
498
|
+
formPageUrl ?? startUrl
|
|
499
|
+
);
|
|
500
|
+
if (!cycled) {
|
|
501
|
+
await replayWithPayload(
|
|
502
|
+
page,
|
|
503
|
+
session,
|
|
504
|
+
injectableStep,
|
|
505
|
+
value,
|
|
506
|
+
startUrl
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const reflectionFinding = await checkReflection(
|
|
528
511
|
page,
|
|
529
512
|
injectableStep,
|
|
530
513
|
payloadSet,
|
|
@@ -550,6 +533,7 @@ var BrowserRunner = class _BrowserRunner {
|
|
|
550
533
|
ctx.options.onStepComplete?.(injectableStep.id, payloadsTested);
|
|
551
534
|
} catch (err) {
|
|
552
535
|
errors.push(`${injectableStep.id}: ${String(err)}`);
|
|
536
|
+
isFirstPayload = true;
|
|
553
537
|
}
|
|
554
538
|
}
|
|
555
539
|
}
|
|
@@ -558,7 +542,10 @@ var BrowserRunner = class _BrowserRunner {
|
|
|
558
542
|
page.off("console", consoleHandler);
|
|
559
543
|
currentPayloadInfo = null;
|
|
560
544
|
await ctx.options.onBeforeClose?.(page);
|
|
561
|
-
await
|
|
545
|
+
await context.close();
|
|
546
|
+
if (ownBrowser) {
|
|
547
|
+
await ownBrowser.close();
|
|
548
|
+
}
|
|
562
549
|
}
|
|
563
550
|
return {
|
|
564
551
|
findings: ctx.findings,
|
|
@@ -568,136 +555,264 @@ var BrowserRunner = class _BrowserRunner {
|
|
|
568
555
|
errors
|
|
569
556
|
};
|
|
570
557
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
case "browser.click":
|
|
593
|
-
if (injected) {
|
|
594
|
-
await Promise.all([
|
|
595
|
-
page.waitForNavigation({
|
|
596
|
-
waitUntil: "domcontentloaded",
|
|
597
|
-
timeout: 5e3
|
|
598
|
-
}).catch(() => {
|
|
599
|
-
}),
|
|
600
|
-
page.click(browserStep.selector, { timeout: 5e3 })
|
|
601
|
-
]);
|
|
602
|
-
} else {
|
|
603
|
-
await page.click(browserStep.selector, { timeout: 5e3 });
|
|
604
|
-
}
|
|
605
|
-
break;
|
|
606
|
-
case "browser.input": {
|
|
607
|
-
const value = step.id === targetStep.id ? payloadValue : browserStep.value;
|
|
608
|
-
await page.fill(browserStep.selector, value, { timeout: 5e3 });
|
|
609
|
-
if (step.id === targetStep.id) {
|
|
610
|
-
injected = true;
|
|
611
|
-
}
|
|
612
|
-
break;
|
|
558
|
+
};
|
|
559
|
+
function createDialogHandler(page, eventFindings, getPayloadInfo) {
|
|
560
|
+
return async (dialog) => {
|
|
561
|
+
const info = getPayloadInfo();
|
|
562
|
+
if (info) {
|
|
563
|
+
const message = dialog.message();
|
|
564
|
+
const dialogType = dialog.type();
|
|
565
|
+
if (dialogType !== "beforeunload") {
|
|
566
|
+
eventFindings.push({
|
|
567
|
+
type: "xss",
|
|
568
|
+
severity: "high",
|
|
569
|
+
title: `XSS Confirmed - ${dialogType}() triggered`,
|
|
570
|
+
description: `JavaScript ${dialogType}() dialog was triggered by payload injection. Message: "${message}"`,
|
|
571
|
+
stepId: info.stepId,
|
|
572
|
+
payload: info.payloadValue,
|
|
573
|
+
url: page.url(),
|
|
574
|
+
evidence: `Dialog type: ${dialogType}, Message: ${message}`,
|
|
575
|
+
metadata: {
|
|
576
|
+
dialogType,
|
|
577
|
+
dialogMessage: message,
|
|
578
|
+
detectionMethod: "dialog"
|
|
613
579
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
try {
|
|
584
|
+
await dialog.dismiss();
|
|
585
|
+
} catch {
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
function createConsoleHandler(eventFindings, getPayloadInfo) {
|
|
590
|
+
return async (msg) => {
|
|
591
|
+
const info = getPayloadInfo();
|
|
592
|
+
if (info && msg.type() === "log") {
|
|
593
|
+
const text = msg.text();
|
|
594
|
+
if (text.includes("vulcn") || text.includes(info.payloadValue)) {
|
|
595
|
+
eventFindings.push({
|
|
596
|
+
type: "xss",
|
|
597
|
+
severity: "high",
|
|
598
|
+
title: "XSS Confirmed - Console Output",
|
|
599
|
+
description: `JavaScript console.log was triggered by payload injection`,
|
|
600
|
+
stepId: info.stepId,
|
|
601
|
+
payload: info.payloadValue,
|
|
602
|
+
url: "",
|
|
603
|
+
evidence: `Console output: ${text}`,
|
|
604
|
+
metadata: {
|
|
605
|
+
consoleType: msg.type(),
|
|
606
|
+
detectionMethod: "console"
|
|
628
607
|
}
|
|
629
|
-
|
|
630
|
-
if (browserStep.selector) {
|
|
631
|
-
await page.locator(browserStep.selector).evaluate((el, pos) => {
|
|
632
|
-
el.scrollTo(pos.x, pos.y);
|
|
633
|
-
}, browserStep.position);
|
|
634
|
-
} else {
|
|
635
|
-
await page.evaluate((pos) => {
|
|
636
|
-
window.scrollTo(pos.x, pos.y);
|
|
637
|
-
}, browserStep.position);
|
|
638
|
-
}
|
|
639
|
-
break;
|
|
640
|
-
case "browser.wait":
|
|
641
|
-
await page.waitForTimeout(browserStep.duration);
|
|
642
|
-
break;
|
|
643
|
-
}
|
|
644
|
-
} catch {
|
|
608
|
+
});
|
|
645
609
|
}
|
|
646
610
|
}
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
async function cyclePayload(page, session, targetStep, payloadValue, formPageUrl) {
|
|
614
|
+
try {
|
|
615
|
+
await page.goBack({ waitUntil: "domcontentloaded", timeout: 5e3 });
|
|
616
|
+
const targetSelector = targetStep.selector;
|
|
617
|
+
const formPresent = await page.waitForSelector(targetSelector, { timeout: 3e3 }).then(() => true).catch(() => false);
|
|
618
|
+
if (!formPresent) {
|
|
619
|
+
await page.goto(formPageUrl, {
|
|
620
|
+
waitUntil: "domcontentloaded",
|
|
621
|
+
timeout: 5e3
|
|
622
|
+
});
|
|
623
|
+
const formPresentAfterNav = await page.waitForSelector(targetSelector, { timeout: 3e3 }).then(() => true).catch(() => false);
|
|
624
|
+
if (!formPresentAfterNav) {
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
await page.fill(targetSelector, payloadValue, { timeout: 5e3 });
|
|
629
|
+
await replayStepsAfter(page, session, targetStep);
|
|
647
630
|
await page.waitForTimeout(500);
|
|
631
|
+
return true;
|
|
632
|
+
} catch {
|
|
633
|
+
return false;
|
|
648
634
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
635
|
+
}
|
|
636
|
+
async function replayWithPayload(page, session, targetStep, payloadValue, startUrl) {
|
|
637
|
+
await page.goto(startUrl, { waitUntil: "domcontentloaded" });
|
|
638
|
+
let injected = false;
|
|
639
|
+
for (const step of session.steps) {
|
|
640
|
+
const browserStep = step;
|
|
641
|
+
try {
|
|
642
|
+
switch (browserStep.type) {
|
|
643
|
+
case "browser.navigate":
|
|
644
|
+
if (injected && browserStep.url.includes("sid=")) {
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
await page.goto(browserStep.url, { waitUntil: "domcontentloaded" });
|
|
648
|
+
break;
|
|
649
|
+
case "browser.click":
|
|
650
|
+
if (injected) {
|
|
651
|
+
await Promise.all([
|
|
652
|
+
page.waitForNavigation({
|
|
653
|
+
waitUntil: "domcontentloaded",
|
|
654
|
+
timeout: 5e3
|
|
655
|
+
}).catch(() => {
|
|
656
|
+
}),
|
|
657
|
+
page.click(browserStep.selector, { timeout: 5e3 })
|
|
658
|
+
]);
|
|
659
|
+
} else {
|
|
660
|
+
await page.click(browserStep.selector, { timeout: 5e3 });
|
|
661
|
+
}
|
|
662
|
+
break;
|
|
663
|
+
case "browser.input": {
|
|
664
|
+
const value = step.id === targetStep.id ? payloadValue : browserStep.value;
|
|
665
|
+
await page.fill(browserStep.selector, value, { timeout: 5e3 });
|
|
666
|
+
if (step.id === targetStep.id) {
|
|
667
|
+
injected = true;
|
|
668
|
+
}
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
case "browser.keypress": {
|
|
672
|
+
const modifiers = browserStep.modifiers ?? [];
|
|
673
|
+
for (const mod of modifiers) {
|
|
674
|
+
await page.keyboard.down(
|
|
675
|
+
mod
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
await page.keyboard.press(browserStep.key);
|
|
679
|
+
for (const mod of modifiers.reverse()) {
|
|
680
|
+
await page.keyboard.up(mod);
|
|
681
|
+
}
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
case "browser.scroll":
|
|
685
|
+
if (browserStep.selector) {
|
|
686
|
+
await page.locator(browserStep.selector).evaluate((el, pos) => {
|
|
687
|
+
el.scrollTo(pos.x, pos.y);
|
|
688
|
+
}, browserStep.position);
|
|
689
|
+
} else {
|
|
690
|
+
await page.evaluate((pos) => {
|
|
691
|
+
window.scrollTo(pos.x, pos.y);
|
|
692
|
+
}, browserStep.position);
|
|
693
|
+
}
|
|
694
|
+
break;
|
|
695
|
+
case "browser.wait":
|
|
696
|
+
await page.waitForTimeout(browserStep.duration);
|
|
697
|
+
break;
|
|
666
698
|
}
|
|
699
|
+
} catch {
|
|
667
700
|
}
|
|
668
|
-
|
|
701
|
+
}
|
|
702
|
+
await page.waitForTimeout(500);
|
|
703
|
+
}
|
|
704
|
+
async function replayStepsAfter(page, session, targetStep) {
|
|
705
|
+
let pastTarget = false;
|
|
706
|
+
for (const step of session.steps) {
|
|
707
|
+
if (step.id === targetStep.id) {
|
|
708
|
+
pastTarget = true;
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
if (!pastTarget) continue;
|
|
712
|
+
const browserStep = step;
|
|
713
|
+
try {
|
|
714
|
+
switch (browserStep.type) {
|
|
715
|
+
case "browser.navigate":
|
|
716
|
+
break;
|
|
717
|
+
case "browser.click":
|
|
718
|
+
await Promise.all([
|
|
719
|
+
page.waitForNavigation({
|
|
720
|
+
waitUntil: "domcontentloaded",
|
|
721
|
+
timeout: 5e3
|
|
722
|
+
}).catch(() => {
|
|
723
|
+
}),
|
|
724
|
+
page.click(browserStep.selector, { timeout: 5e3 })
|
|
725
|
+
]);
|
|
726
|
+
break;
|
|
727
|
+
case "browser.input":
|
|
728
|
+
await page.fill(browserStep.selector, browserStep.value, {
|
|
729
|
+
timeout: 5e3
|
|
730
|
+
});
|
|
731
|
+
break;
|
|
732
|
+
case "browser.keypress": {
|
|
733
|
+
const modifiers = browserStep.modifiers ?? [];
|
|
734
|
+
for (const mod of modifiers) {
|
|
735
|
+
await page.keyboard.down(
|
|
736
|
+
mod
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
await page.keyboard.press(browserStep.key);
|
|
740
|
+
for (const mod of modifiers.reverse()) {
|
|
741
|
+
await page.keyboard.up(mod);
|
|
742
|
+
}
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
case "browser.scroll":
|
|
746
|
+
break;
|
|
747
|
+
// skip scrolls in fast path
|
|
748
|
+
case "browser.wait":
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
} catch {
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
await page.waitForTimeout(500);
|
|
755
|
+
}
|
|
756
|
+
async function checkReflection(page, step, payloadSet, payloadValue) {
|
|
757
|
+
const content = await page.content();
|
|
758
|
+
for (const pattern of payloadSet.detectPatterns) {
|
|
759
|
+
if (pattern.test(content)) {
|
|
669
760
|
return {
|
|
670
761
|
type: payloadSet.category,
|
|
671
|
-
severity:
|
|
672
|
-
title:
|
|
673
|
-
description: `Payload was reflected in page
|
|
762
|
+
severity: getSeverity(payloadSet.category),
|
|
763
|
+
title: `${payloadSet.category.toUpperCase()} vulnerability detected`,
|
|
764
|
+
description: `Payload pattern was reflected in page content`,
|
|
674
765
|
stepId: step.id,
|
|
675
766
|
payload: payloadValue,
|
|
676
|
-
url: page.url()
|
|
767
|
+
url: page.url(),
|
|
768
|
+
evidence: content.match(pattern)?.[0]?.slice(0, 200)
|
|
677
769
|
};
|
|
678
770
|
}
|
|
679
|
-
return void 0;
|
|
680
771
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
772
|
+
if (content.includes(payloadValue)) {
|
|
773
|
+
return {
|
|
774
|
+
type: payloadSet.category,
|
|
775
|
+
severity: "medium",
|
|
776
|
+
title: `Potential ${payloadSet.category.toUpperCase()} - payload reflection`,
|
|
777
|
+
description: `Payload was reflected in page without encoding`,
|
|
778
|
+
stepId: step.id,
|
|
779
|
+
payload: payloadValue,
|
|
780
|
+
url: page.url()
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
return void 0;
|
|
784
|
+
}
|
|
785
|
+
function getSeverity(category) {
|
|
786
|
+
switch (category) {
|
|
787
|
+
case "sqli":
|
|
788
|
+
case "command-injection":
|
|
789
|
+
case "xxe":
|
|
790
|
+
return "critical";
|
|
791
|
+
case "xss":
|
|
792
|
+
case "ssrf":
|
|
793
|
+
case "path-traversal":
|
|
794
|
+
return "high";
|
|
795
|
+
case "open-redirect":
|
|
796
|
+
return "medium";
|
|
797
|
+
default:
|
|
798
|
+
return "medium";
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function interleavePayloads(payloads) {
|
|
802
|
+
const result = [];
|
|
803
|
+
const payloadsByCategory = payloads.map(
|
|
804
|
+
(ps) => ps.payloads.map((value) => ({ payloadSet: ps, value }))
|
|
805
|
+
);
|
|
806
|
+
const maxLen = Math.max(...payloadsByCategory.map((c) => c.length));
|
|
807
|
+
for (let i = 0; i < maxLen; i++) {
|
|
808
|
+
for (const category of payloadsByCategory) {
|
|
809
|
+
if (i < category.length) {
|
|
810
|
+
result.push(category[i]);
|
|
811
|
+
}
|
|
698
812
|
}
|
|
699
813
|
}
|
|
700
|
-
|
|
814
|
+
return result;
|
|
815
|
+
}
|
|
701
816
|
|
|
702
817
|
// src/crawler.ts
|
|
703
818
|
var INJECTABLE_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -734,7 +849,8 @@ async function crawlAndBuildSessions(config, options = {}) {
|
|
|
734
849
|
headless: config.headless ?? true
|
|
735
850
|
});
|
|
736
851
|
const context = await browser.newContext({
|
|
737
|
-
viewport: config.viewport ?? { width: 1280, height: 720 }
|
|
852
|
+
viewport: config.viewport ?? { width: 1280, height: 720 },
|
|
853
|
+
...options.storageState ? { storageState: JSON.parse(options.storageState) } : {}
|
|
738
854
|
});
|
|
739
855
|
try {
|
|
740
856
|
while (queue.length > 0 && visited.size < opts.maxPages) {
|
|
@@ -1049,6 +1165,182 @@ function normalizeUrl(url) {
|
|
|
1049
1165
|
}
|
|
1050
1166
|
}
|
|
1051
1167
|
|
|
1168
|
+
// src/auth.ts
|
|
1169
|
+
async function detectLoginForm(page) {
|
|
1170
|
+
return page.evaluate(() => {
|
|
1171
|
+
function findUsernameInput(container) {
|
|
1172
|
+
const selectors = [
|
|
1173
|
+
'input[autocomplete="username"]',
|
|
1174
|
+
'input[autocomplete="email"]',
|
|
1175
|
+
'input[type="email"]',
|
|
1176
|
+
'input[name*="user" i]',
|
|
1177
|
+
'input[name*="login" i]',
|
|
1178
|
+
'input[name*="email" i]',
|
|
1179
|
+
'input[id*="user" i]',
|
|
1180
|
+
'input[id*="login" i]',
|
|
1181
|
+
'input[id*="email" i]',
|
|
1182
|
+
'input[name*="name" i]',
|
|
1183
|
+
'input[type="text"]'
|
|
1184
|
+
];
|
|
1185
|
+
for (const sel of selectors) {
|
|
1186
|
+
const el = container.querySelector(sel);
|
|
1187
|
+
if (el && el.type !== "password" && el.type !== "hidden") {
|
|
1188
|
+
return el;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return null;
|
|
1192
|
+
}
|
|
1193
|
+
function findSubmitButton(container) {
|
|
1194
|
+
const selectors = [
|
|
1195
|
+
'button[type="submit"]',
|
|
1196
|
+
'input[type="submit"]',
|
|
1197
|
+
"button:not([type])",
|
|
1198
|
+
'button[type="button"]'
|
|
1199
|
+
];
|
|
1200
|
+
for (const sel of selectors) {
|
|
1201
|
+
const el = container.querySelector(sel);
|
|
1202
|
+
if (el) return el;
|
|
1203
|
+
}
|
|
1204
|
+
return null;
|
|
1205
|
+
}
|
|
1206
|
+
function getSelector(el) {
|
|
1207
|
+
if (el.id) return `#${CSS.escape(el.id)}`;
|
|
1208
|
+
if (el.getAttribute("name"))
|
|
1209
|
+
return `${el.tagName.toLowerCase()}[name="${CSS.escape(el.getAttribute("name"))}"]`;
|
|
1210
|
+
if (el.getAttribute("type") && el.tagName === "INPUT")
|
|
1211
|
+
return `input[type="${el.getAttribute("type")}"]`;
|
|
1212
|
+
const parent = el.parentElement;
|
|
1213
|
+
if (!parent) return el.tagName.toLowerCase();
|
|
1214
|
+
const siblings = Array.from(parent.children);
|
|
1215
|
+
const index = siblings.indexOf(el) + 1;
|
|
1216
|
+
return `${parent.tagName.toLowerCase()} > ${el.tagName.toLowerCase()}:nth-child(${index})`;
|
|
1217
|
+
}
|
|
1218
|
+
const forms = document.querySelectorAll("form");
|
|
1219
|
+
for (const form of forms) {
|
|
1220
|
+
const passwordInput2 = form.querySelector(
|
|
1221
|
+
'input[type="password"]'
|
|
1222
|
+
);
|
|
1223
|
+
if (!passwordInput2) continue;
|
|
1224
|
+
const usernameInput = findUsernameInput(form);
|
|
1225
|
+
const submitButton = findSubmitButton(form);
|
|
1226
|
+
return {
|
|
1227
|
+
usernameSelector: usernameInput ? getSelector(usernameInput) : 'input[type="text"]',
|
|
1228
|
+
passwordSelector: getSelector(passwordInput2),
|
|
1229
|
+
submitSelector: submitButton ? getSelector(submitButton) : null,
|
|
1230
|
+
autoDetected: true
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
const passwordInput = document.querySelector(
|
|
1234
|
+
'input[type="password"]'
|
|
1235
|
+
);
|
|
1236
|
+
if (passwordInput) {
|
|
1237
|
+
const usernameInput = findUsernameInput(document.body);
|
|
1238
|
+
const submitButton = findSubmitButton(document.body);
|
|
1239
|
+
return {
|
|
1240
|
+
usernameSelector: usernameInput ? getSelector(usernameInput) : 'input[type="text"]',
|
|
1241
|
+
passwordSelector: getSelector(passwordInput),
|
|
1242
|
+
submitSelector: submitButton ? getSelector(submitButton) : null,
|
|
1243
|
+
autoDetected: true
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
return null;
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
async function performLogin(page, context, credentials, options) {
|
|
1250
|
+
const loginUrl = credentials.loginUrl ?? options.targetUrl;
|
|
1251
|
+
await page.goto(loginUrl, { waitUntil: "domcontentloaded", timeout: 15e3 });
|
|
1252
|
+
let usernameSelector;
|
|
1253
|
+
let passwordSelector;
|
|
1254
|
+
let submitSelector;
|
|
1255
|
+
if (credentials.userSelector && credentials.passSelector) {
|
|
1256
|
+
usernameSelector = credentials.userSelector;
|
|
1257
|
+
passwordSelector = credentials.passSelector;
|
|
1258
|
+
submitSelector = null;
|
|
1259
|
+
} else {
|
|
1260
|
+
const form = await detectLoginForm(page);
|
|
1261
|
+
if (!form) {
|
|
1262
|
+
return {
|
|
1263
|
+
success: false,
|
|
1264
|
+
message: `No login form detected on ${loginUrl}. Use --user-field and --pass-field to specify selectors.`
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
usernameSelector = form.usernameSelector;
|
|
1268
|
+
passwordSelector = form.passwordSelector;
|
|
1269
|
+
submitSelector = form.submitSelector;
|
|
1270
|
+
}
|
|
1271
|
+
try {
|
|
1272
|
+
await page.fill(usernameSelector, credentials.username, { timeout: 5e3 });
|
|
1273
|
+
} catch {
|
|
1274
|
+
return {
|
|
1275
|
+
success: false,
|
|
1276
|
+
message: `Could not find username field: ${usernameSelector}`
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
try {
|
|
1280
|
+
await page.fill(passwordSelector, credentials.password, { timeout: 5e3 });
|
|
1281
|
+
} catch {
|
|
1282
|
+
return {
|
|
1283
|
+
success: false,
|
|
1284
|
+
message: `Could not find password field: ${passwordSelector}`
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
try {
|
|
1288
|
+
if (submitSelector) {
|
|
1289
|
+
await Promise.all([
|
|
1290
|
+
page.waitForNavigation({ waitUntil: "domcontentloaded", timeout: 1e4 }).catch(() => {
|
|
1291
|
+
}),
|
|
1292
|
+
page.click(submitSelector, { timeout: 5e3 })
|
|
1293
|
+
]);
|
|
1294
|
+
} else {
|
|
1295
|
+
await Promise.all([
|
|
1296
|
+
page.waitForNavigation({ waitUntil: "domcontentloaded", timeout: 1e4 }).catch(() => {
|
|
1297
|
+
}),
|
|
1298
|
+
page.press(passwordSelector, "Enter")
|
|
1299
|
+
]);
|
|
1300
|
+
}
|
|
1301
|
+
} catch {
|
|
1302
|
+
return {
|
|
1303
|
+
success: false,
|
|
1304
|
+
message: "Failed to submit login form"
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
await page.waitForTimeout(1e3);
|
|
1308
|
+
const bodyText = await page.textContent("body").catch(() => "");
|
|
1309
|
+
if (options.loggedOutIndicator && bodyText?.includes(options.loggedOutIndicator)) {
|
|
1310
|
+
return {
|
|
1311
|
+
success: false,
|
|
1312
|
+
message: `Login failed \u2014 "${options.loggedOutIndicator}" still visible on page`
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
if (options.loggedInIndicator && !bodyText?.includes(options.loggedInIndicator)) {
|
|
1316
|
+
return {
|
|
1317
|
+
success: false,
|
|
1318
|
+
message: `Login uncertain \u2014 "${options.loggedInIndicator}" not found on page`
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
const storageState = JSON.stringify(await context.storageState());
|
|
1322
|
+
return {
|
|
1323
|
+
success: true,
|
|
1324
|
+
message: "Login successful",
|
|
1325
|
+
storageState
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
async function checkSessionAlive(page, config) {
|
|
1329
|
+
try {
|
|
1330
|
+
const bodyText = await page.textContent("body");
|
|
1331
|
+
if (!bodyText) return true;
|
|
1332
|
+
if (config.loggedOutIndicator && bodyText.includes(config.loggedOutIndicator)) {
|
|
1333
|
+
return false;
|
|
1334
|
+
}
|
|
1335
|
+
if (config.loggedInIndicator && !bodyText.includes(config.loggedInIndicator)) {
|
|
1336
|
+
return false;
|
|
1337
|
+
}
|
|
1338
|
+
return true;
|
|
1339
|
+
} catch {
|
|
1340
|
+
return false;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1052
1344
|
// src/index.ts
|
|
1053
1345
|
var configSchema = import_zod.z.object({
|
|
1054
1346
|
/** Starting URL for recording */
|
|
@@ -1155,9 +1447,12 @@ var index_default = browserDriver;
|
|
|
1155
1447
|
BrowserRunner,
|
|
1156
1448
|
BrowserStepSchema,
|
|
1157
1449
|
checkBrowsers,
|
|
1450
|
+
checkSessionAlive,
|
|
1158
1451
|
configSchema,
|
|
1159
1452
|
crawlAndBuildSessions,
|
|
1453
|
+
detectLoginForm,
|
|
1160
1454
|
installBrowsers,
|
|
1161
|
-
launchBrowser
|
|
1455
|
+
launchBrowser,
|
|
1456
|
+
performLogin
|
|
1162
1457
|
});
|
|
1163
1458
|
//# sourceMappingURL=index.cjs.map
|