0sec-cli 0.15.0 → 0.17.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/0sec.js +322 -0
- package/README.md +63 -52
- package/attacks/data-exfiltration/pii-leakage.yaml +27 -0
- package/attacks/encoding-bypass/base64-encoding.yaml +24 -0
- package/attacks/jailbreak/dan-roleplay.yaml +27 -0
- package/attacks/jailbreak/hypothetical-scenario.yaml +25 -0
- package/attacks/jailbreak/multilingual-bypass.yaml +22 -0
- package/attacks/output-manipulation/harmful-content.yaml +25 -0
- package/attacks/prompt-injection/context-manipulation.yaml +32 -0
- package/attacks/prompt-injection/direct-injection.yaml +28 -0
- package/attacks/prompt-injection/indirect-injection.yaml +33 -0
- package/attacks/system-prompt-extraction/direct-ask.yaml +30 -0
- package/attacks/system-prompt-extraction/markdown-exfil.yaml +26 -0
- package/attacks/tool-misuse/ssrf-via-tools.yaml +27 -0
- package/chunks/adapt-loop-ECQG7454.js +18 -0
- package/chunks/adgraph-JLGA6RYI.js +54 -0
- package/chunks/agent/skills/frameworks/entra-id.yaml +63 -0
- package/chunks/agent/skills/frameworks/graphql-introspection.yaml +116 -0
- package/chunks/agent/skills/frameworks/nextjs.yaml +82 -0
- package/chunks/agent/skills/frameworks/python-web.yaml +85 -0
- package/chunks/agent/skills/frameworks/supabase.yaml +83 -0
- package/chunks/agent/skills/frameworks/wordpress-deep.yaml +122 -0
- package/chunks/agent/skills/techniques/ad-attack-paths.yaml +57 -0
- package/chunks/agent/skills/techniques/advisory-disclosure.yaml +59 -0
- package/chunks/agent/skills/techniques/assumption-mining.yaml +53 -0
- package/chunks/agent/skills/techniques/blind-exploitation.yaml +143 -0
- package/chunks/agent/skills/techniques/crypto-misuse.yaml +88 -0
- package/chunks/agent/skills/techniques/cve-poc-adaptation.yaml +82 -0
- package/chunks/agent/skills/techniques/entra-attack-paths.yaml +55 -0
- package/chunks/agent/skills/techniques/http-conformance-diff.yaml +48 -0
- package/chunks/agent/skills/techniques/jwt-attacks.yaml +137 -0
- package/chunks/agent/skills/techniques/kernel-weaponization.yaml +53 -0
- package/chunks/agent/skills/techniques/llm-excessive-agency.yaml +55 -0
- package/chunks/agent/skills/techniques/llm-insecure-output-handling.yaml +56 -0
- package/chunks/agent/skills/techniques/llm-prompt-injection.yaml +54 -0
- package/chunks/agent/skills/techniques/llm-prompt-layer-write.yaml +68 -0
- package/chunks/agent/skills/techniques/llm-rag-poisoning.yaml +57 -0
- package/chunks/agent/skills/techniques/llm-safety-eval.yaml +51 -0
- package/chunks/agent/skills/techniques/npm-ecosystem.yaml +51 -0
- package/chunks/agent/skills/techniques/poc-verification.yaml +55 -0
- package/chunks/agent/skills/techniques/race-condition.yaml +122 -0
- package/chunks/agent/skills/techniques/scoped-fix.yaml +49 -0
- package/chunks/agent/skills/techniques/seedless-depth-review.yaml +59 -0
- package/chunks/agent/skills/techniques/spec-differential.yaml +48 -0
- package/chunks/agent/skills/techniques/variant-hunting.yaml +55 -0
- package/chunks/agent/skills/vulnerabilities/cardano-eutxo-validators.yaml +72 -0
- package/chunks/agent/skills/vulnerabilities/command-injection.yaml +99 -0
- package/chunks/agent/skills/vulnerabilities/deserialization-chains.yaml +106 -0
- package/chunks/agent/skills/vulnerabilities/native-memory-safety.yaml +93 -0
- package/chunks/agent/skills/vulnerabilities/path-traversal.yaml +78 -0
- package/chunks/agent/skills/vulnerabilities/prototype-pollution.yaml +132 -0
- package/chunks/agent/skills/vulnerabilities/request-smuggling.yaml +129 -0
- package/chunks/agent/skills/vulnerabilities/sqli-advanced.yaml +102 -0
- package/chunks/agent/skills/vulnerabilities/ssrf-bypass.yaml +84 -0
- package/chunks/agent/skills/vulnerabilities/ssti-exploitation.yaml +112 -0
- package/chunks/agent/skills/vulnerabilities/structural-sqli.yaml +91 -0
- package/chunks/appsec-catalog-CBWHGTEL.js +24 -0
- package/chunks/artifact-scraper-GJEZMOHF.js +34 -0
- package/chunks/assumption-mining-BVMMUZHB.js +79 -0
- package/chunks/chunk-2CJ776PV.js +2430 -0
- package/chunks/chunk-2RMLOJVB.js +47 -0
- package/chunks/chunk-2SANI5RH.js +4728 -0
- package/chunks/chunk-3MOLBTLS.js +953 -0
- package/chunks/chunk-3QFDYBZQ.js +5108 -0
- package/chunks/chunk-47TQJWDH.js +3729 -0
- package/chunks/chunk-4Y4KQJXE.js +600 -0
- package/chunks/chunk-53G27VPS.js +136 -0
- package/chunks/chunk-57ZENEX2.js +169 -0
- package/chunks/chunk-5G3ZXFBW.js +182 -0
- package/chunks/chunk-6LKRLK2R.js +542 -0
- package/chunks/chunk-7DQEV5QI.js +287 -0
- package/chunks/chunk-A6CLR72I.js +235 -0
- package/chunks/chunk-BKZFDZ23.js +361 -0
- package/chunks/chunk-CLHCDHP4.js +471 -0
- package/chunks/chunk-D6S3IIPG.js +518 -0
- package/chunks/chunk-DN25OQQA.js +1070 -0
- package/chunks/chunk-DW5UWPFY.js +275 -0
- package/chunks/chunk-F3WBKITT.js +687 -0
- package/chunks/chunk-H2FFLZNK.js +3 -0
- package/chunks/chunk-H44E2CRN.js +193 -0
- package/chunks/chunk-HDWV7PGI.js +596 -0
- package/chunks/chunk-HE7LCA7G.js +381 -0
- package/chunks/chunk-HT6P7RY3.js +2597 -0
- package/chunks/chunk-IOL7D5YV.js +515 -0
- package/chunks/chunk-IORC6MQX.js +72828 -0
- package/chunks/chunk-IR537GON.js +49 -0
- package/chunks/chunk-K26SZ37E.js +5735 -0
- package/chunks/chunk-KKGE5RQE.js +1094 -0
- package/chunks/chunk-KLTNTE2Z.js +121 -0
- package/chunks/chunk-KTQDLNSR.js +1732 -0
- package/chunks/chunk-LOHTE223.js +2572 -0
- package/chunks/chunk-LP3HHYQU.js +245 -0
- package/chunks/chunk-MMLDQR4H.js +852 -0
- package/chunks/chunk-MYVT64FN.js +16 -0
- package/chunks/chunk-O462Y7P2.js +33019 -0
- package/chunks/chunk-OEFNRYI2.js +150 -0
- package/chunks/chunk-P6WKNFWX.js +1968 -0
- package/chunks/chunk-QI233I24.js +333 -0
- package/chunks/chunk-QJQKHO7G.js +917 -0
- package/chunks/chunk-QKICO43A.js +808 -0
- package/chunks/chunk-QOKTUOU2.js +102 -0
- package/chunks/chunk-RDZYQQLW.js +596 -0
- package/chunks/chunk-RJWOYEOG.js +271 -0
- package/chunks/chunk-RRMJC3ZE.js +105 -0
- package/chunks/chunk-RWONANDA.js +973 -0
- package/chunks/chunk-SAFFWQW4.js +5921 -0
- package/chunks/chunk-SF4KZ4O3.js +502 -0
- package/chunks/chunk-SNKTC4BP.js +1422 -0
- package/chunks/chunk-SOG2U7B3.js +662 -0
- package/chunks/chunk-SZJCPG2I.js +110 -0
- package/chunks/chunk-UJ4IK5PO.js +2010 -0
- package/chunks/chunk-UM3ZNQIM.js +9329 -0
- package/chunks/chunk-VGRDNSHA.js +3249 -0
- package/chunks/chunk-VQG4FT5D.js +685 -0
- package/chunks/chunk-WKNNZVJS.js +2798 -0
- package/chunks/chunk-WU6AFRAZ.js +1167 -0
- package/chunks/chunk-WVTBZEQO.js +1531 -0
- package/chunks/chunk-YLMN3N25.js +4054 -0
- package/chunks/commands-U3AMV5ZU.js +16795 -0
- package/chunks/corpus-v1.json +403 -0
- package/chunks/cost-ledger-ZCKMBJEN.js +13 -0
- package/chunks/data/appsec-archetypes.json +102 -0
- package/chunks/data/chromium-archetypes.json +204 -0
- package/chunks/data/freebsd-archetypes.json +171 -0
- package/chunks/data/kernel-archetypes.json +611 -0
- package/chunks/db-26NGQFKO.js +16 -0
- package/chunks/disclose-A6AEIEDA.js +132 -0
- package/chunks/dist-FDKALV4R.js +2772 -0
- package/chunks/dist-GL66KMUX.js +74 -0
- package/chunks/dist-JR67XKYO.js +159 -0
- package/chunks/eval-runner-DDLE5RQ3.js +27 -0
- package/chunks/example-manifest.json +95 -0
- package/chunks/exploit-agent-XP23ISN7.js +98 -0
- package/chunks/exploit-autoclimb-E6T7RRSC.js +81 -0
- package/chunks/exploit-climb-2FAUP6XU.js +358 -0
- package/chunks/fix-IG7LYXH3.js +12 -0
- package/chunks/github-issues-MJ6OYOOU.js +157 -0
- package/chunks/harness-HWOYFKFY.js +22 -0
- package/chunks/http-conformance-DU66MZIU.js +11 -0
- package/chunks/http-sender-GWH2IYEA.js +10 -0
- package/chunks/hunt-scan-XPELSPQQ.js +38 -0
- package/chunks/identity-6ZAIIWOR.js +191 -0
- package/chunks/kernel-primitive-TENE3R7T.js +44 -0
- package/chunks/kernel-vm-runner-4F6QSNFY.js +62 -0
- package/chunks/memsafety-scan-J7SECV5W.js +16 -0
- package/chunks/native-loop-NTONUDP7.js +54 -0
- package/chunks/npm-detectors-KB5Y5ZFX.js +69 -0
- package/chunks/npm-dynamic-discovery-AHROOZDB.js +13 -0
- package/chunks/orchestrate-ZUWAUWBC.js +62 -0
- package/chunks/pipeline-FCARFZU3.js +14 -0
- package/chunks/pre-recon-cve-66EB6G4M.js +351 -0
- package/chunks/prepare-MYY743TK.js +13 -0
- package/chunks/process-3Q7QJIOZ.js +13 -0
- package/chunks/replay-runner-RRR4E2A7.js +39 -0
- package/chunks/run-ESNN4V5W.js +32632 -0
- package/chunks/runtime-62ZQAH7H.js +43 -0
- package/chunks/runtime-J7PLXZNM.js +12 -0
- package/chunks/scan-stream-FHI2FYZE.js +96 -0
- package/chunks/scope-BI7BF4ZY.js +18 -0
- package/chunks/session-store-BCFQYDCE.js +28 -0
- package/chunks/source-files-PWRQR6LY.js +12 -0
- package/chunks/specdrift-WCLH6TTQ.js +19 -0
- package/chunks/variant-candidates-6KM6F6MD.js +13 -0
- package/chunks/web-recon-prepass-DPVCBRZA.js +1148 -0
- package/dashboard/assets/0sec-icon-66SreztZ.gif +0 -0
- package/dashboard/assets/bot-yILPnRgD.js +1 -0
- package/dashboard/assets/chevron-down-BeDJ-Vka.js +1 -0
- package/dashboard/assets/circle-alert-BEnMBMT-.js +1 -0
- package/dashboard/assets/client-DRYsuOGl.js +9 -0
- package/dashboard/assets/copy-Bi5vzoF9.js +1 -0
- package/dashboard/assets/desktop-CXTQonNm.js +32 -0
- package/dashboard/assets/desktop-NOekk4QS.css +1 -0
- package/dashboard/assets/dist-CmX-h7a4.js +1 -0
- package/dashboard/assets/dist-DAwYQmf_.js +45 -0
- package/dashboard/assets/findings-page-CACW9nw2.js +5 -0
- package/dashboard/assets/format-PoISqsES.js +1 -0
- package/dashboard/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dashboard/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dashboard/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dashboard/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dashboard/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/dashboard/assets/jsx-runtime-C7oxC63R.js +1 -0
- package/dashboard/assets/live-page-DrgQqQK5.js +1 -0
- package/dashboard/assets/meta-tile-CcskIV_o.js +1 -0
- package/dashboard/assets/operations-BKE8T-vY.css +2 -0
- package/dashboard/assets/operations-D8nUP5_m.js +4 -0
- package/dashboard/assets/operations-app-CQQhG54n.js +2 -0
- package/dashboard/assets/overview-page-DTGS8dPo.js +1 -0
- package/dashboard/assets/page-header-MFVvEtZW.js +1 -0
- package/dashboard/assets/play-Dwo-9xt6.js +1 -0
- package/dashboard/assets/scans-page-ChVtq8Us.js +1 -0
- package/dashboard/assets/search-CiooTLck.js +1 -0
- package/dashboard/assets/siren-CCBwTSuk.js +1 -0
- package/dashboard/assets/table-BYS-nIVA.js +1 -0
- package/dashboard/assets/tabs-DivxbhQy.js +1 -0
- package/dashboard/desktop.html +21 -0
- package/dashboard/index.html +15 -0
- package/package.json +30 -18
- package/bin/0sec.cjs +0 -361
|
@@ -0,0 +1,973 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __0secCreateRequire } from "node:module";
|
|
3
|
+
const require = __0secCreateRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
// packages/core/dist/stages/npm-detectors/dedup.js
|
|
6
|
+
function forkTwinFor(hints, name) {
|
|
7
|
+
return hints?.forkTwins?.[name];
|
|
8
|
+
}
|
|
9
|
+
async function dedupConfirmation(args) {
|
|
10
|
+
const { name, version, cwe, hints, advisoryLookup } = args;
|
|
11
|
+
if (hints?.priorReports?.includes(name)) {
|
|
12
|
+
return {
|
|
13
|
+
novel: false,
|
|
14
|
+
source: "prior-report",
|
|
15
|
+
advisories: [`${name}: previously reported by 0sec for this class`]
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const twin = forkTwinFor(hints, name);
|
|
19
|
+
if (twin) {
|
|
20
|
+
return { novel: false, source: "fork-cve-twin", advisories: [`${name}: ${twin}`] };
|
|
21
|
+
}
|
|
22
|
+
if (!advisoryLookup) {
|
|
23
|
+
return { novel: true, source: "unknown", advisories: [] };
|
|
24
|
+
}
|
|
25
|
+
let advisories;
|
|
26
|
+
try {
|
|
27
|
+
advisories = await advisoryLookup(name, version, cwe);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return {
|
|
30
|
+
novel: true,
|
|
31
|
+
source: "unknown",
|
|
32
|
+
advisories: [`advisory-lookup-failed: ${e instanceof Error ? e.message.split("\n")[0] : String(e)}`]
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (advisories.length > 0) {
|
|
36
|
+
return { novel: false, source: "osv", advisories };
|
|
37
|
+
}
|
|
38
|
+
return { novel: true, source: "novel", advisories: [] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// packages/core/dist/stages/npm-detectors/base.js
|
|
42
|
+
function guardPackage(pkg, guards, now = Date.now()) {
|
|
43
|
+
if (!guards)
|
|
44
|
+
return { allowed: true };
|
|
45
|
+
if (guards.downloadsFloor !== void 0 && pkg.weeklyDownloads !== void 0 && pkg.weeklyDownloads < guards.downloadsFloor) {
|
|
46
|
+
return {
|
|
47
|
+
allowed: false,
|
|
48
|
+
reason: `weeklyDownloads ${pkg.weeklyDownloads} < floor ${guards.downloadsFloor}`
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (guards.maxAgeDays !== void 0 && pkg.lastPublishedAt) {
|
|
52
|
+
const published = Date.parse(pkg.lastPublishedAt);
|
|
53
|
+
if (Number.isFinite(published)) {
|
|
54
|
+
const ageDays = (now - published) / 864e5;
|
|
55
|
+
if (ageDays > guards.maxAgeDays) {
|
|
56
|
+
return { allowed: false, reason: `last publish ${Math.round(ageDays)}d ago > maxAgeDays ${guards.maxAgeDays}` };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { allowed: true };
|
|
61
|
+
}
|
|
62
|
+
async function runDetectorOnPackage(detector, probe, opts = {}) {
|
|
63
|
+
const warnings = [];
|
|
64
|
+
const base = {
|
|
65
|
+
detectorId: detector.id,
|
|
66
|
+
ran: false,
|
|
67
|
+
candidates: 0,
|
|
68
|
+
leads: [],
|
|
69
|
+
warnings
|
|
70
|
+
};
|
|
71
|
+
const guard = guardPackage(probe.pkg, opts.guards, opts.now);
|
|
72
|
+
if (!guard.allowed)
|
|
73
|
+
return { ...base, skipReason: `guard: ${guard.reason}` };
|
|
74
|
+
let applies = false;
|
|
75
|
+
try {
|
|
76
|
+
applies = detector.appliesTo(probe.pkg);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
warnings.push(`appliesTo threw: ${errMsg(e)}`);
|
|
79
|
+
}
|
|
80
|
+
if (!applies)
|
|
81
|
+
return { ...base, skipReason: "appliesTo=false" };
|
|
82
|
+
base.ran = true;
|
|
83
|
+
let candidates = [];
|
|
84
|
+
try {
|
|
85
|
+
candidates = await detector.identifyCandidates(probe);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
warnings.push(`identifyCandidates threw: ${errMsg(e)}`);
|
|
88
|
+
return base;
|
|
89
|
+
}
|
|
90
|
+
base.candidates = candidates.length;
|
|
91
|
+
for (const cand of candidates) {
|
|
92
|
+
let conf;
|
|
93
|
+
try {
|
|
94
|
+
conf = await detector.confirm(cand, probe);
|
|
95
|
+
} catch (e) {
|
|
96
|
+
warnings.push(`confirm(${cand.id}) threw: ${errMsg(e)}`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (!conf.confirmed || !conf.evidence?.observation)
|
|
100
|
+
continue;
|
|
101
|
+
const dedup = await dedupConfirmation({
|
|
102
|
+
name: probe.pkg.name,
|
|
103
|
+
version: probe.pkg.version,
|
|
104
|
+
cwe: detector.cwe,
|
|
105
|
+
hints: detector.dedupHints,
|
|
106
|
+
advisoryLookup: opts.advisoryLookup
|
|
107
|
+
});
|
|
108
|
+
base.leads.push({
|
|
109
|
+
detectorId: detector.id,
|
|
110
|
+
candidateId: cand.id,
|
|
111
|
+
confirmation: {
|
|
112
|
+
...conf,
|
|
113
|
+
severity: conf.severity ?? detector.severityFloor,
|
|
114
|
+
source: conf.source ?? cand.label
|
|
115
|
+
},
|
|
116
|
+
dedup
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return base;
|
|
120
|
+
}
|
|
121
|
+
function errMsg(e) {
|
|
122
|
+
return e instanceof Error ? e.message.split("\n")[0].slice(0, 120) : String(e);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// packages/core/dist/stages/npm-detectors/sspp-fuzz.js
|
|
126
|
+
var NAME_RE = new RegExp("^(" + [
|
|
127
|
+
"merge",
|
|
128
|
+
"mergeWith",
|
|
129
|
+
"mergeAll",
|
|
130
|
+
"mergeDeep",
|
|
131
|
+
"mergeDeepRight",
|
|
132
|
+
"mergeDeepLeft",
|
|
133
|
+
"deepmerge",
|
|
134
|
+
"deepMerge",
|
|
135
|
+
"mergeObjects",
|
|
136
|
+
"mergeAnything",
|
|
137
|
+
"set",
|
|
138
|
+
"setWith",
|
|
139
|
+
"setWithIn",
|
|
140
|
+
"setProperty",
|
|
141
|
+
"setValue",
|
|
142
|
+
"safeSet",
|
|
143
|
+
"deepSet",
|
|
144
|
+
"dset",
|
|
145
|
+
"update",
|
|
146
|
+
"updateWith",
|
|
147
|
+
"unset",
|
|
148
|
+
"defaults",
|
|
149
|
+
"defaultsDeep",
|
|
150
|
+
"defaultsdeep",
|
|
151
|
+
"defu",
|
|
152
|
+
"defuFn",
|
|
153
|
+
"assign",
|
|
154
|
+
"assignIn",
|
|
155
|
+
"assignInWith",
|
|
156
|
+
"assignWith",
|
|
157
|
+
"assignDeep",
|
|
158
|
+
"extend",
|
|
159
|
+
"extendDeep",
|
|
160
|
+
"deepExtend",
|
|
161
|
+
"deepAssign",
|
|
162
|
+
"mixin",
|
|
163
|
+
"mixinDeep",
|
|
164
|
+
"clone",
|
|
165
|
+
"cloneDeep",
|
|
166
|
+
// NB: recursive-WRITE/merge sinks only. Deliberately NOT `parse*` — a
|
|
167
|
+
// validator's `parse`/`parseAsync` (e.g. zod) is a read/validate export,
|
|
168
|
+
// not a merge sink, and matching it both over-flags and (for the async
|
|
169
|
+
// variant) floats a rejected Promise through the sync fuzz loop.
|
|
170
|
+
"expand"
|
|
171
|
+
].join("|") + ")", "i");
|
|
172
|
+
var FRAGMENTS = ["merge", "deep", "assign", "extend", "defaults", "setwith", "mixin", "defu"];
|
|
173
|
+
function nameMatchesPpSink(key) {
|
|
174
|
+
if (typeof key !== "string" || !key)
|
|
175
|
+
return false;
|
|
176
|
+
if (NAME_RE.test(key))
|
|
177
|
+
return true;
|
|
178
|
+
const lk = key.toLowerCase();
|
|
179
|
+
return FRAGMENTS.some((f) => lk.includes(f));
|
|
180
|
+
}
|
|
181
|
+
function isRecord(v) {
|
|
182
|
+
return v !== null && (typeof v === "object" || typeof v === "function");
|
|
183
|
+
}
|
|
184
|
+
function collectFromModule(mod, via, out, seen) {
|
|
185
|
+
if (!isRecord(mod))
|
|
186
|
+
return;
|
|
187
|
+
const push = (key, val, viaLabel) => {
|
|
188
|
+
if (typeof val !== "function")
|
|
189
|
+
return;
|
|
190
|
+
if (!nameMatchesPpSink(key))
|
|
191
|
+
return;
|
|
192
|
+
const fn2 = val;
|
|
193
|
+
if (seen.has(fn2))
|
|
194
|
+
return;
|
|
195
|
+
seen.add(fn2);
|
|
196
|
+
out.push({ id: `${key}@${viaLabel}`, label: `${viaLabel}.${key}`, fn: fn2, via: viaLabel });
|
|
197
|
+
};
|
|
198
|
+
for (const key of Object.keys(mod)) {
|
|
199
|
+
let val;
|
|
200
|
+
try {
|
|
201
|
+
val = mod[key];
|
|
202
|
+
} catch {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
push(key, val, via);
|
|
206
|
+
}
|
|
207
|
+
if (typeof mod === "function" && mod.name)
|
|
208
|
+
push(mod.name, mod, `${via}(default-fn)`);
|
|
209
|
+
for (const nsKey of ["default", "compat", "fp", "object", "lodash"]) {
|
|
210
|
+
let ns;
|
|
211
|
+
try {
|
|
212
|
+
ns = mod[nsKey];
|
|
213
|
+
} catch {
|
|
214
|
+
ns = void 0;
|
|
215
|
+
}
|
|
216
|
+
if (isRecord(ns) && ns !== mod) {
|
|
217
|
+
for (const key of Object.keys(ns)) {
|
|
218
|
+
let val;
|
|
219
|
+
try {
|
|
220
|
+
val = ns[key];
|
|
221
|
+
} catch {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
push(key, val, `${via}.${nsKey}`);
|
|
225
|
+
}
|
|
226
|
+
if (typeof ns === "function" && ns.name)
|
|
227
|
+
push(ns.name, ns, `${via}.${nsKey}(fn)`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function subpathCandidates(probe) {
|
|
232
|
+
const name = probe.pkg.name;
|
|
233
|
+
const subs = /* @__PURE__ */ new Set([name]);
|
|
234
|
+
const pj = probe.load(`${name}/package.json`);
|
|
235
|
+
if (isRecord(pj) && isRecord(pj.exports)) {
|
|
236
|
+
for (const k of Object.keys(pj.exports)) {
|
|
237
|
+
if (k === "." || k === "./package.json")
|
|
238
|
+
continue;
|
|
239
|
+
if (k.startsWith("./") && !k.includes("*"))
|
|
240
|
+
subs.add(name + k.slice(1));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
subs.add(`${name}/compat`);
|
|
244
|
+
return [...subs];
|
|
245
|
+
}
|
|
246
|
+
var markerCounter = 0;
|
|
247
|
+
function freshMarker() {
|
|
248
|
+
markerCounter += 1;
|
|
249
|
+
return `__sspp_${process.pid}_${Date.now().toString(36)}_${markerCounter}__`;
|
|
250
|
+
}
|
|
251
|
+
function pollutionSnapshot(marker) {
|
|
252
|
+
const objHit = {}[marker] !== void 0 || Object.prototype.hasOwnProperty.call(Object.prototype, marker);
|
|
253
|
+
const arrHit = [][marker] !== void 0 || Object.prototype.hasOwnProperty.call(Array.prototype, marker);
|
|
254
|
+
return { objHit, arrHit, any: objHit || arrHit };
|
|
255
|
+
}
|
|
256
|
+
function cleanup(marker) {
|
|
257
|
+
try {
|
|
258
|
+
delete Object.prototype[marker];
|
|
259
|
+
} catch {
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
delete Array.prototype[marker];
|
|
263
|
+
} catch {
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function buildBattery() {
|
|
267
|
+
const battery = [];
|
|
268
|
+
const objStyle = [
|
|
269
|
+
{ tag: "src.__proto__", build: (m) => JSON.parse(`{"__proto__":{"${m}":"polluted"}}`) },
|
|
270
|
+
{ tag: "src.constructor.prototype", build: (m) => JSON.parse(`{"constructor":{"prototype":{"${m}":"polluted"}}}`) },
|
|
271
|
+
{ tag: "src.nested.__proto__", build: (m) => JSON.parse(`{"a":{"b":{"__proto__":{"${m}":"polluted"}}}}`) }
|
|
272
|
+
];
|
|
273
|
+
for (const p of objStyle) {
|
|
274
|
+
battery.push({ label: `obj:${p.tag}:merge(target,src)`, make: (m) => [{}, p.build(m)] });
|
|
275
|
+
battery.push({ label: `obj:${p.tag}:fn(src)`, make: (m) => [p.build(m)] });
|
|
276
|
+
battery.push({ label: `obj:${p.tag}:merge(target,{},src)`, make: (m) => [{}, {}, p.build(m)] });
|
|
277
|
+
}
|
|
278
|
+
const paths = [
|
|
279
|
+
{ tag: "path.str.constructor.prototype", p: (m) => `constructor.prototype.${m}` },
|
|
280
|
+
{ tag: "path.arr.constructor.prototype", p: (m) => ["constructor", "prototype", m] },
|
|
281
|
+
{ tag: "path.str.__proto__", p: (m) => `__proto__.${m}` },
|
|
282
|
+
{ tag: "path.arr.__proto__", p: (m) => ["__proto__", m] },
|
|
283
|
+
{ tag: "path.str.prototype", p: (m) => `prototype.${m}` }
|
|
284
|
+
];
|
|
285
|
+
for (const pp of paths) {
|
|
286
|
+
battery.push({ label: `set:${pp.tag}:set(obj,path,val)`, make: (m) => [{}, pp.p(m), "polluted"] });
|
|
287
|
+
battery.push({ label: `set:${pp.tag}:update(obj,path,fn)`, make: (m) => [{}, pp.p(m), () => "polluted"] });
|
|
288
|
+
}
|
|
289
|
+
return battery;
|
|
290
|
+
}
|
|
291
|
+
var BATTERY = buildBattery();
|
|
292
|
+
function fuzzCandidate(fn2, timeBudgetMs = 4e3) {
|
|
293
|
+
const started = Date.now();
|
|
294
|
+
const hits = [];
|
|
295
|
+
for (const payload of BATTERY) {
|
|
296
|
+
if (Date.now() - started > timeBudgetMs)
|
|
297
|
+
break;
|
|
298
|
+
const marker = freshMarker();
|
|
299
|
+
cleanup(marker);
|
|
300
|
+
try {
|
|
301
|
+
const ret = fn2(...payload.make(marker));
|
|
302
|
+
if (ret !== null && (typeof ret === "object" || typeof ret === "function") && typeof ret.then === "function") {
|
|
303
|
+
void ret.then(void 0, () => {
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
} catch {
|
|
307
|
+
}
|
|
308
|
+
const p = pollutionSnapshot(marker);
|
|
309
|
+
if (p.any)
|
|
310
|
+
hits.push({ payload: payload.label, objProto: p.objHit, arrProto: p.arrHit });
|
|
311
|
+
cleanup(marker);
|
|
312
|
+
}
|
|
313
|
+
return hits;
|
|
314
|
+
}
|
|
315
|
+
var ssppFuzzDetector = {
|
|
316
|
+
id: "sspp-fuzz",
|
|
317
|
+
title: "Server-Side Prototype Pollution (dynamic fuzz)",
|
|
318
|
+
cwe: "CWE-1321",
|
|
319
|
+
category: "prototype-pollution",
|
|
320
|
+
severityFloor: "high",
|
|
321
|
+
description: "LLM-proposes / harness-disposes SSPP miner: name-heuristic candidate recursive-write fns, confirmed ONLY on runtime Object/Array.prototype pollution; optional NODE_OPTIONS gadget escalates to RCE.",
|
|
322
|
+
appliesTo(_pkg) {
|
|
323
|
+
return true;
|
|
324
|
+
},
|
|
325
|
+
identifyCandidates(probe) {
|
|
326
|
+
const out = [];
|
|
327
|
+
const seen = /* @__PURE__ */ new Set();
|
|
328
|
+
for (const id of subpathCandidates(probe)) {
|
|
329
|
+
const mod = probe.load(id);
|
|
330
|
+
if (mod === void 0) {
|
|
331
|
+
probe.note?.(`sspp-fuzz: could not load ${id}`);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
collectFromModule(mod, id, out, seen);
|
|
335
|
+
}
|
|
336
|
+
return out;
|
|
337
|
+
},
|
|
338
|
+
confirm(candidate) {
|
|
339
|
+
const hits = fuzzCandidate(candidate.fn);
|
|
340
|
+
if (hits.length === 0) {
|
|
341
|
+
return { confirmed: false, evidence: { observation: "" } };
|
|
342
|
+
}
|
|
343
|
+
const first = hits[0];
|
|
344
|
+
const which = first.objProto ? "Object.prototype" : "Array.prototype";
|
|
345
|
+
return {
|
|
346
|
+
confirmed: true,
|
|
347
|
+
severity: "high",
|
|
348
|
+
source: candidate.label,
|
|
349
|
+
evidence: {
|
|
350
|
+
observation: `${which} polluted at runtime via ${candidate.label} (payload ${first.payload})`,
|
|
351
|
+
payload: first.payload,
|
|
352
|
+
analysis: [
|
|
353
|
+
`Function ${candidate.via} recursively writes attacker-controlled keys without guarding`,
|
|
354
|
+
`__proto__ / constructor.prototype. ${hits.length} payload(s) fired; a truly empty {} then`,
|
|
355
|
+
`inherits the planted marker. SSPP/High; escalate to RCE only if a NODE_OPTIONS gadget`,
|
|
356
|
+
`fires in a spawned Node subprocess (sandbox-gated escalation, assume-FP otherwise).`
|
|
357
|
+
].join(" "),
|
|
358
|
+
escalation: { kind: "NODE_OPTIONS\u2192RCE", achieved: false, note: "gadget not attempted in confirm (sandbox-gated)" }
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
},
|
|
362
|
+
dedupHints: {
|
|
363
|
+
// 0sec prior filing for this exact class.
|
|
364
|
+
priorReports: ["es-toolkit"],
|
|
365
|
+
// Fork-twin the prototype learned the hard way: npm audit gave a false
|
|
366
|
+
// all-clear because the advisory lives under the maintained fork's name.
|
|
367
|
+
forkTwins: {
|
|
368
|
+
radash: "CVE-2025-48054 / GHSA-2xv9-ghh9-xc69 (disclosed in fork `radashi`; no GHSA under `radash` \u2192 live advisory lookup misses it)"
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// packages/core/dist/stages/npm-detectors/read-unstable.js
|
|
374
|
+
var EVIL = "__0sec_toctou_forbidden__";
|
|
375
|
+
function makePhasedField(clean, evil = EVIL) {
|
|
376
|
+
let phase = "validate";
|
|
377
|
+
return {
|
|
378
|
+
clean,
|
|
379
|
+
evil,
|
|
380
|
+
install(target, prop) {
|
|
381
|
+
Object.defineProperty(target, prop, {
|
|
382
|
+
enumerable: true,
|
|
383
|
+
configurable: true,
|
|
384
|
+
get() {
|
|
385
|
+
return phase === "validate" ? clean : evil;
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
},
|
|
389
|
+
useNow() {
|
|
390
|
+
phase = "use";
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function confirmReadUnstable(candidate) {
|
|
395
|
+
const field = makePhasedField(candidate.allowed[0] ?? "allowed-1");
|
|
396
|
+
let accepted = false;
|
|
397
|
+
let appValue;
|
|
398
|
+
try {
|
|
399
|
+
const res = candidate.probe(field);
|
|
400
|
+
accepted = res.accepted;
|
|
401
|
+
field.useNow();
|
|
402
|
+
appValue = res.readAppValue();
|
|
403
|
+
} catch {
|
|
404
|
+
return { confirmed: false, evidence: { observation: "" } };
|
|
405
|
+
}
|
|
406
|
+
const bypass = accepted && appValue === field.evil;
|
|
407
|
+
if (!bypass)
|
|
408
|
+
return { confirmed: false, evidence: { observation: "" } };
|
|
409
|
+
return {
|
|
410
|
+
confirmed: true,
|
|
411
|
+
severity: "high",
|
|
412
|
+
source: candidate.label,
|
|
413
|
+
evidence: {
|
|
414
|
+
observation: `validator ACCEPTED input yet app-observed ${candidate.field}="${String(appValue)}" (\u2209 allowlist [${candidate.allowed.join(",")}])`,
|
|
415
|
+
payload: `getter on '${candidate.field}' returns '${field.clean}' at validate-time, '${field.evil}' at use-time`,
|
|
416
|
+
analysis: [
|
|
417
|
+
`${candidate.label} validates then returns/keeps the SAME live object, so a getter-backed`,
|
|
418
|
+
`(or Proxy / mutable-derived) property that changes between the validator's read and the`,
|
|
419
|
+
`app's read yields an unvalidated value the app trusts. TOCTOU allowlist bypass (CWE-367).`
|
|
420
|
+
].join(" ")
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
var ALLOWED = ["allowed-1", "allowed-2"];
|
|
425
|
+
function fn(mod, key) {
|
|
426
|
+
const v = mod[key];
|
|
427
|
+
return typeof v === "function" ? v : void 0;
|
|
428
|
+
}
|
|
429
|
+
var ADAPTERS = [
|
|
430
|
+
// superstruct: validate(input, Schema) → [err, output]; output === input.
|
|
431
|
+
{
|
|
432
|
+
id: "superstruct",
|
|
433
|
+
matches: (name, mod) => name === "superstruct" || !!fn(mod, "object") && !!fn(mod, "enums") && !!fn(mod, "validate"),
|
|
434
|
+
candidates(name, mod) {
|
|
435
|
+
const object = fn(mod, "object");
|
|
436
|
+
const enums = fn(mod, "enums");
|
|
437
|
+
const validate = fn(mod, "validate");
|
|
438
|
+
const assert = fn(mod, "assert");
|
|
439
|
+
if (!object || !enums || !validate)
|
|
440
|
+
return [];
|
|
441
|
+
const Schema = object({ role: enums(ALLOWED) });
|
|
442
|
+
const out = [
|
|
443
|
+
{
|
|
444
|
+
id: `${name}.validate`,
|
|
445
|
+
label: `${name}.validate`,
|
|
446
|
+
field: "role",
|
|
447
|
+
allowed: ALLOWED,
|
|
448
|
+
probe(field) {
|
|
449
|
+
const input = {};
|
|
450
|
+
field.install(input, "role");
|
|
451
|
+
const res = validate(input, Schema);
|
|
452
|
+
const err = Array.isArray(res) ? res[0] : void 0;
|
|
453
|
+
const output = Array.isArray(res) ? res[1] : input;
|
|
454
|
+
return { accepted: !err, readAppValue: () => (output ?? input)["role"] };
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
];
|
|
458
|
+
if (assert) {
|
|
459
|
+
out.push({
|
|
460
|
+
id: `${name}.assert`,
|
|
461
|
+
label: `${name}.assert`,
|
|
462
|
+
field: "role",
|
|
463
|
+
allowed: ALLOWED,
|
|
464
|
+
probe(field) {
|
|
465
|
+
const input = {};
|
|
466
|
+
field.install(input, "role");
|
|
467
|
+
let ok = false;
|
|
468
|
+
try {
|
|
469
|
+
assert(input, Schema);
|
|
470
|
+
ok = true;
|
|
471
|
+
} catch {
|
|
472
|
+
ok = false;
|
|
473
|
+
}
|
|
474
|
+
return { accepted: ok, readAppValue: () => input["role"] };
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
return out;
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
// class-validator: validates class INSTANCES; getter accessors are idiomatic.
|
|
482
|
+
{
|
|
483
|
+
id: "class-validator",
|
|
484
|
+
matches: (name, mod) => name === "class-validator" || !!fn(mod, "IsIn") && !!fn(mod, "validateSync"),
|
|
485
|
+
candidates(name, mod) {
|
|
486
|
+
const IsIn = fn(mod, "IsIn");
|
|
487
|
+
const validateSync = fn(mod, "validateSync");
|
|
488
|
+
if (!IsIn || !validateSync)
|
|
489
|
+
return [];
|
|
490
|
+
return [
|
|
491
|
+
{
|
|
492
|
+
id: `${name}.validateSync`,
|
|
493
|
+
label: `${name}.validateSync`,
|
|
494
|
+
field: "role",
|
|
495
|
+
allowed: ALLOWED,
|
|
496
|
+
probe(field) {
|
|
497
|
+
class UserDto {
|
|
498
|
+
}
|
|
499
|
+
const decorate = IsIn(ALLOWED);
|
|
500
|
+
decorate(UserDto.prototype, "role");
|
|
501
|
+
const u = new UserDto();
|
|
502
|
+
field.install(u, "role");
|
|
503
|
+
const errors = validateSync(u);
|
|
504
|
+
return { accepted: Array.isArray(errors) && errors.length === 0, readAppValue: () => u["role"] };
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
];
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
// joi (control): materialises a fresh value → SAFE. Kept so the detector
|
|
511
|
+
// proves it does NOT false-positive on read-stable validators.
|
|
512
|
+
{
|
|
513
|
+
id: "joi",
|
|
514
|
+
matches: (name, mod) => name === "joi" || !!fn(mod, "object") && !!fn(mod, "string"),
|
|
515
|
+
candidates(name, mod) {
|
|
516
|
+
const object = fn(mod, "object");
|
|
517
|
+
const string = fn(mod, "string");
|
|
518
|
+
if (!object || !string)
|
|
519
|
+
return [];
|
|
520
|
+
return [
|
|
521
|
+
{
|
|
522
|
+
id: `${name}.validate`,
|
|
523
|
+
label: `${name}.validate`,
|
|
524
|
+
field: "role",
|
|
525
|
+
allowed: ALLOWED,
|
|
526
|
+
probe(field) {
|
|
527
|
+
const input = {};
|
|
528
|
+
field.install(input, "role");
|
|
529
|
+
const schema = object({ role: string().valid(...ALLOWED) });
|
|
530
|
+
const res = schema.validate(input);
|
|
531
|
+
return { accepted: !res.error, readAppValue: () => res.value?.["role"] };
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
];
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
// zod (control): safeParse materialises → SAFE.
|
|
538
|
+
{
|
|
539
|
+
id: "zod",
|
|
540
|
+
matches: (name, mod) => name === "zod" || !!fn(mod, "z") || !!fn(mod, "object"),
|
|
541
|
+
candidates(name, mod) {
|
|
542
|
+
const z = mod["z"] ?? mod;
|
|
543
|
+
const object = fn(z, "object");
|
|
544
|
+
const zenum = fn(z, "enum");
|
|
545
|
+
if (!object || !zenum)
|
|
546
|
+
return [];
|
|
547
|
+
return [
|
|
548
|
+
{
|
|
549
|
+
id: `${name}.safeParse`,
|
|
550
|
+
label: `${name}.safeParse`,
|
|
551
|
+
field: "role",
|
|
552
|
+
allowed: ALLOWED,
|
|
553
|
+
probe(field) {
|
|
554
|
+
const input = {};
|
|
555
|
+
field.install(input, "role");
|
|
556
|
+
const schema = object({ role: zenum(ALLOWED) });
|
|
557
|
+
const res = schema.safeParse(input);
|
|
558
|
+
return { accepted: res.success, readAppValue: () => res.data?.["role"] };
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
];
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
];
|
|
565
|
+
var readUnstableDetector = {
|
|
566
|
+
id: "read-unstable",
|
|
567
|
+
title: "Validation Read-Stability TOCTOU",
|
|
568
|
+
cwe: "CWE-367",
|
|
569
|
+
category: "toctou",
|
|
570
|
+
severityFloor: "high",
|
|
571
|
+
description: "Confirms the validate-and-return-live-object read-stability class: a phased getter (allowed at validate-time, forbidden at use-time) that the validator accepts but the app then reads as forbidden. Materialising validators (joi/zod) are the negative controls.",
|
|
572
|
+
appliesTo(_pkg) {
|
|
573
|
+
return true;
|
|
574
|
+
},
|
|
575
|
+
identifyCandidates(probe) {
|
|
576
|
+
const mod = probe.load(probe.pkg.name);
|
|
577
|
+
if (mod === void 0 || typeof mod !== "object" && typeof mod !== "function") {
|
|
578
|
+
probe.note?.(`read-unstable: could not load ${probe.pkg.name}`);
|
|
579
|
+
return [];
|
|
580
|
+
}
|
|
581
|
+
const modRec = mod;
|
|
582
|
+
const out = [];
|
|
583
|
+
const seen = /* @__PURE__ */ new Set();
|
|
584
|
+
for (const adapter of ADAPTERS) {
|
|
585
|
+
let matched = false;
|
|
586
|
+
try {
|
|
587
|
+
matched = adapter.matches(probe.pkg.name, modRec);
|
|
588
|
+
} catch {
|
|
589
|
+
matched = false;
|
|
590
|
+
}
|
|
591
|
+
if (!matched)
|
|
592
|
+
continue;
|
|
593
|
+
try {
|
|
594
|
+
for (const c of adapter.candidates(probe.pkg.name, modRec)) {
|
|
595
|
+
const fp = `${adapter.id}:${c.field}:${c.allowed.join(",")}`;
|
|
596
|
+
if (seen.has(fp)) {
|
|
597
|
+
probe.note?.(`read-unstable: collapsed duplicate candidate ${c.id} (fingerprint ${fp})`);
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
seen.add(fp);
|
|
601
|
+
out.push(c);
|
|
602
|
+
}
|
|
603
|
+
} catch (e) {
|
|
604
|
+
probe.note?.(`read-unstable: adapter ${adapter.id} failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return out;
|
|
608
|
+
},
|
|
609
|
+
confirm(candidate) {
|
|
610
|
+
return confirmReadUnstable(candidate);
|
|
611
|
+
},
|
|
612
|
+
dedupHints: {
|
|
613
|
+
priorReports: ["class-validator", "superstruct"]
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
// packages/core/dist/stages/npm-detectors/parser-diff.js
|
|
618
|
+
function parsePart(s) {
|
|
619
|
+
if (s === "")
|
|
620
|
+
return null;
|
|
621
|
+
let val;
|
|
622
|
+
let m;
|
|
623
|
+
if (m = /^0[xX]([0-9a-fA-F]+)$/.exec(s))
|
|
624
|
+
val = parseInt(m[1], 16);
|
|
625
|
+
else if (/^0[0-7]*$/.exec(s))
|
|
626
|
+
val = parseInt(s, 8);
|
|
627
|
+
else if (/^0[0-7]*[89]/.exec(s))
|
|
628
|
+
return null;
|
|
629
|
+
else if (/^[0-9]+$/.exec(s))
|
|
630
|
+
val = parseInt(s, 10);
|
|
631
|
+
else
|
|
632
|
+
return null;
|
|
633
|
+
if (!Number.isFinite(val) || val < 0)
|
|
634
|
+
return null;
|
|
635
|
+
return val;
|
|
636
|
+
}
|
|
637
|
+
function inetAton(input) {
|
|
638
|
+
if (typeof input !== "string")
|
|
639
|
+
return { ok: false };
|
|
640
|
+
const parts = input.split(".");
|
|
641
|
+
if (parts.length < 1 || parts.length > 4)
|
|
642
|
+
return { ok: false };
|
|
643
|
+
const nums = parts.map(parsePart);
|
|
644
|
+
if (nums.some((n2) => n2 === null))
|
|
645
|
+
return { ok: false };
|
|
646
|
+
const n = nums.length;
|
|
647
|
+
const v = nums;
|
|
648
|
+
let long;
|
|
649
|
+
if (n === 1) {
|
|
650
|
+
if (v[0] > 4294967295)
|
|
651
|
+
return { ok: false };
|
|
652
|
+
long = v[0] >>> 0;
|
|
653
|
+
} else if (n === 2) {
|
|
654
|
+
if (v[0] > 255 || v[1] > 16777215)
|
|
655
|
+
return { ok: false };
|
|
656
|
+
long = (v[0] << 24 | v[1]) >>> 0;
|
|
657
|
+
} else if (n === 3) {
|
|
658
|
+
if (v[0] > 255 || v[1] > 255 || v[2] > 65535)
|
|
659
|
+
return { ok: false };
|
|
660
|
+
long = (v[0] << 24 | v[1] << 16 | v[2]) >>> 0;
|
|
661
|
+
} else {
|
|
662
|
+
if (v.some((x) => x > 255))
|
|
663
|
+
return { ok: false };
|
|
664
|
+
long = (v[0] << 24 | v[1] << 16 | v[2] << 8 | v[3]) >>> 0;
|
|
665
|
+
}
|
|
666
|
+
const dotted = [long >>> 24, long >>> 16 & 255, long >>> 8 & 255, long & 255].join(".");
|
|
667
|
+
return { ok: true, long, dotted, cls: classify(long) };
|
|
668
|
+
}
|
|
669
|
+
function classify(long) {
|
|
670
|
+
const a = long >>> 24;
|
|
671
|
+
const b = long >>> 16 & 255;
|
|
672
|
+
if (a === 127)
|
|
673
|
+
return "loopback";
|
|
674
|
+
if (a === 10)
|
|
675
|
+
return "private";
|
|
676
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
677
|
+
return "private";
|
|
678
|
+
if (a === 192 && b === 168)
|
|
679
|
+
return "private";
|
|
680
|
+
if (a === 169 && b === 254)
|
|
681
|
+
return long === 2852039166 ? "metadata(169.254.169.254)" : "linklocal";
|
|
682
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
683
|
+
return "cgnat";
|
|
684
|
+
if (a === 0)
|
|
685
|
+
return "this-network";
|
|
686
|
+
return "PUBLIC";
|
|
687
|
+
}
|
|
688
|
+
function isUnsafe(cls) {
|
|
689
|
+
return !!cls && cls !== "PUBLIC";
|
|
690
|
+
}
|
|
691
|
+
var PARSER_DIFF_CORPUS = [
|
|
692
|
+
// octal / hex / decimal encodings of loopback + metadata
|
|
693
|
+
"0177.0.0.1",
|
|
694
|
+
"0177.00.00.01",
|
|
695
|
+
"0x7f.0.0.1",
|
|
696
|
+
"0x7f000001",
|
|
697
|
+
"0x7f.1",
|
|
698
|
+
"2130706433",
|
|
699
|
+
"017700000001",
|
|
700
|
+
"127.1",
|
|
701
|
+
"127.0.1",
|
|
702
|
+
// metadata (169.254.169.254) in many encodings
|
|
703
|
+
"169.254.169.254",
|
|
704
|
+
"0251.0376.0251.0376",
|
|
705
|
+
"0xa9.0xfe.0xa9.0xfe",
|
|
706
|
+
"169.254.43518",
|
|
707
|
+
"169.16689150",
|
|
708
|
+
"2852039166",
|
|
709
|
+
// private
|
|
710
|
+
"0300.0250.0.01",
|
|
711
|
+
"010.0.0.1",
|
|
712
|
+
"192.168.257",
|
|
713
|
+
// whitespace-wrapped loopback (clients trim, getaddrinfo does not)
|
|
714
|
+
" 127.0.0.1",
|
|
715
|
+
"127.0.0.1 ",
|
|
716
|
+
"0177.0.0.1 "
|
|
717
|
+
];
|
|
718
|
+
function scanDivergences(candidate, corpus = PARSER_DIFF_CORPUS) {
|
|
719
|
+
const hits = [];
|
|
720
|
+
for (const input of corpus) {
|
|
721
|
+
const oracle = inetAton(input);
|
|
722
|
+
if (!oracle.ok || !isUnsafe(oracle.cls))
|
|
723
|
+
continue;
|
|
724
|
+
let verdict;
|
|
725
|
+
try {
|
|
726
|
+
verdict = candidate.filterVerdict(input);
|
|
727
|
+
} catch {
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if (verdict.treatedAsSafe) {
|
|
731
|
+
hits.push({ input, socketTarget: oracle.dotted, cls: oracle.cls, note: verdict.note });
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return hits;
|
|
735
|
+
}
|
|
736
|
+
function confirmParserDiff(candidate) {
|
|
737
|
+
const hits = scanDivergences(candidate);
|
|
738
|
+
if (hits.length === 0)
|
|
739
|
+
return { confirmed: false, evidence: { observation: "" } };
|
|
740
|
+
const h = hits[0];
|
|
741
|
+
return {
|
|
742
|
+
confirmed: true,
|
|
743
|
+
severity: "high",
|
|
744
|
+
source: candidate.label,
|
|
745
|
+
evidence: {
|
|
746
|
+
observation: `oracle resolves ${JSON.stringify(h.input)} to ${h.socketTarget} [${h.cls}] but ${candidate.label} treats it as safe (${h.note})`,
|
|
747
|
+
payload: h.input,
|
|
748
|
+
analysis: [
|
|
749
|
+
`${candidate.label} disagrees with glibc getaddrinfo (inet_aton) semantics on ${hits.length}`,
|
|
750
|
+
`input(s). A real socket connect() targets the internal address while the filter classifies`,
|
|
751
|
+
`it public \u2192 SSRF allowlist bypass (CWE-918). Sample: ${hits.slice(0, 3).map((x) => `${x.input}\u2192${x.socketTarget}`).join(", ")}.`
|
|
752
|
+
].join(" ")
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
function safe(f) {
|
|
757
|
+
try {
|
|
758
|
+
return f();
|
|
759
|
+
} catch {
|
|
760
|
+
return void 0;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function fnOf(mod, key) {
|
|
764
|
+
const v = mod[key];
|
|
765
|
+
return typeof v === "function" ? v : void 0;
|
|
766
|
+
}
|
|
767
|
+
var ADAPTERS2 = [
|
|
768
|
+
// `ip`: isPrivate / isPublic / isLoopback booleans.
|
|
769
|
+
{
|
|
770
|
+
id: "ip",
|
|
771
|
+
matches: (name, mod) => name === "ip" || !!fnOf(mod, "isPrivate") && !!fnOf(mod, "isPublic"),
|
|
772
|
+
candidate(name, mod) {
|
|
773
|
+
const isPublic = fnOf(mod, "isPublic");
|
|
774
|
+
const isPrivate = fnOf(mod, "isPrivate");
|
|
775
|
+
const isLoopback = fnOf(mod, "isLoopback");
|
|
776
|
+
if (!isPublic && !isPrivate)
|
|
777
|
+
return void 0;
|
|
778
|
+
return {
|
|
779
|
+
id: `${name}.isPublic`,
|
|
780
|
+
label: `${name}.isPublic`,
|
|
781
|
+
field: "",
|
|
782
|
+
filterVerdict(input) {
|
|
783
|
+
const pub = safe(() => isPublic?.(input));
|
|
784
|
+
if (pub === true)
|
|
785
|
+
return { treatedAsSafe: true, note: "ip.isPublic=true" };
|
|
786
|
+
const priv = safe(() => isPrivate?.(input));
|
|
787
|
+
const lo = safe(() => isLoopback?.(input));
|
|
788
|
+
if (priv === false && lo === false)
|
|
789
|
+
return { treatedAsSafe: true, note: "ip.isPrivate=false&isLoopback=false" };
|
|
790
|
+
return { treatedAsSafe: false, note: "blocked" };
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
},
|
|
795
|
+
// `ipaddr.js`: isValid + parse().range(); invalid ⇒ filter skips ⇒ bypass.
|
|
796
|
+
{
|
|
797
|
+
id: "ipaddr.js",
|
|
798
|
+
matches: (name, mod) => name === "ipaddr.js" || !!fnOf(mod, "isValid") && !!fnOf(mod, "parse"),
|
|
799
|
+
candidate(name, mod) {
|
|
800
|
+
const isValid = fnOf(mod, "isValid");
|
|
801
|
+
const parse = fnOf(mod, "parse");
|
|
802
|
+
if (!isValid || !parse)
|
|
803
|
+
return void 0;
|
|
804
|
+
const SAFE_RANGES = /loopback|private|linkLocal|carrierGradeNat|unspecified/;
|
|
805
|
+
return {
|
|
806
|
+
id: `${name}.range`,
|
|
807
|
+
label: `${name}.range`,
|
|
808
|
+
field: "",
|
|
809
|
+
filterVerdict(input) {
|
|
810
|
+
const valid = safe(() => isValid(input));
|
|
811
|
+
if (valid !== true)
|
|
812
|
+
return { treatedAsSafe: true, note: "ipaddr.isValid=false(filter-skips)" };
|
|
813
|
+
const range = safe(() => {
|
|
814
|
+
const p = parse(input);
|
|
815
|
+
return p.range ? p.range() : void 0;
|
|
816
|
+
});
|
|
817
|
+
if (typeof range === "string" && !SAFE_RANGES.test(range)) {
|
|
818
|
+
return { treatedAsSafe: true, note: `ipaddr.range=${range}(unsafe-as-safe)` };
|
|
819
|
+
}
|
|
820
|
+
return { treatedAsSafe: false, note: "blocked" };
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
},
|
|
825
|
+
// NB: an `is-ip`-style recognizer (`isIP(host) → boolean`) is deliberately NOT
|
|
826
|
+
// an adapter. `isIP("0177.0.0.1") === false` is CORRECT library behaviour —
|
|
827
|
+
// that string is not a canonical IP — and a bare `false` says nothing about a
|
|
828
|
+
// downstream connect()/resolve() ever happening. The old adapter ASSERTED
|
|
829
|
+
// "not-recognized ⇒ passed to DNS ⇒ SSRF", which fired on is-ip itself and on
|
|
830
|
+
// every package that re-exports `isIP` (e.g. class-validator) with no observed
|
|
831
|
+
// consequence — an assume-FP violation. A recognizer that never NORMALISES or
|
|
832
|
+
// RE-EMITS a different address cannot produce an observable parser divergence,
|
|
833
|
+
// so it has no confirm here. Only filters that make a connect/resolve decision
|
|
834
|
+
// (`ip`, `ipaddr.js`) or re-emit a different base (`cidr-tools`) get adapters.
|
|
835
|
+
//
|
|
836
|
+
// `cidr-tools`: normalizeCidr mis-normalization — the confirmed win. If the
|
|
837
|
+
// filter normalises the input to a base whose oracle class is public while the
|
|
838
|
+
// input's true target is internal, the allowlist is bypassed.
|
|
839
|
+
{
|
|
840
|
+
id: "cidr-tools",
|
|
841
|
+
matches: (name, mod) => name === "cidr-tools" || !!fnOf(mod, "normalizeCidr"),
|
|
842
|
+
candidate(name, mod) {
|
|
843
|
+
const normalizeCidr = fnOf(mod, "normalizeCidr");
|
|
844
|
+
if (!normalizeCidr)
|
|
845
|
+
return void 0;
|
|
846
|
+
return {
|
|
847
|
+
id: `${name}.normalizeCidr`,
|
|
848
|
+
label: `${name}.normalizeCidr`,
|
|
849
|
+
field: "",
|
|
850
|
+
filterVerdict(input) {
|
|
851
|
+
const norm = safe(() => normalizeCidr(input));
|
|
852
|
+
const normStr = Array.isArray(norm) ? String(norm[0] ?? "") : String(norm ?? "");
|
|
853
|
+
if (!normStr)
|
|
854
|
+
return { treatedAsSafe: true, note: "normalizeCidr=\u2205(filter-skips)" };
|
|
855
|
+
const base = normStr.split("/")[0].trim();
|
|
856
|
+
const baseOracle = inetAton(base);
|
|
857
|
+
if (baseOracle.ok && !isUnsafe(baseOracle.cls)) {
|
|
858
|
+
return { treatedAsSafe: true, note: `normalizeCidr\u2192${normStr}(base ${baseOracle.cls})` };
|
|
859
|
+
}
|
|
860
|
+
return { treatedAsSafe: false, note: "blocked" };
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
];
|
|
866
|
+
var parserDiffDetector = {
|
|
867
|
+
id: "parser-diff",
|
|
868
|
+
title: "SSRF Filter Parser Confusion (differential)",
|
|
869
|
+
cwe: "CWE-918",
|
|
870
|
+
category: "ssrf",
|
|
871
|
+
severityFloor: "high",
|
|
872
|
+
description: "Differential IP/host parser confusion: compares a package SSRF filter against a glibc inet_aton oracle; confirmed when the oracle resolves an input to an internal target the filter treats as public.",
|
|
873
|
+
appliesTo(_pkg) {
|
|
874
|
+
return true;
|
|
875
|
+
},
|
|
876
|
+
identifyCandidates(probe) {
|
|
877
|
+
const mod = probe.load(probe.pkg.name);
|
|
878
|
+
if (mod === void 0 || typeof mod !== "object" && typeof mod !== "function") {
|
|
879
|
+
probe.note?.(`parser-diff: could not load ${probe.pkg.name}`);
|
|
880
|
+
return [];
|
|
881
|
+
}
|
|
882
|
+
const modRec = mod;
|
|
883
|
+
const out = [];
|
|
884
|
+
for (const adapter of ADAPTERS2) {
|
|
885
|
+
let matched = false;
|
|
886
|
+
try {
|
|
887
|
+
matched = adapter.matches(probe.pkg.name, modRec);
|
|
888
|
+
} catch {
|
|
889
|
+
matched = false;
|
|
890
|
+
}
|
|
891
|
+
if (!matched)
|
|
892
|
+
continue;
|
|
893
|
+
const c = safe(() => adapter.candidate(probe.pkg.name, modRec));
|
|
894
|
+
if (c)
|
|
895
|
+
out.push(c);
|
|
896
|
+
}
|
|
897
|
+
return out;
|
|
898
|
+
},
|
|
899
|
+
confirm(candidate) {
|
|
900
|
+
return confirmParserDiff(candidate);
|
|
901
|
+
},
|
|
902
|
+
dedupHints: {
|
|
903
|
+
// parser-diff's OWN founding seed: `cidr-tools`
|
|
904
|
+
// `normalizeCidr("0177.0.0.1") → "177.0.0.1"` drops the leading-zero octal
|
|
905
|
+
// and re-emits a PUBLIC-looking base while glibc resolves the input to
|
|
906
|
+
// 127.0.0.1 (loopback) — the exact mis-normalization the prototype was
|
|
907
|
+
// ported from. OSV carries no named advisory for it, so a live lookup
|
|
908
|
+
// returns "novel" and the flywheel would otherwise re-surface its own seed
|
|
909
|
+
// as a fresh find. Route it as prior/known.
|
|
910
|
+
priorReports: ["cidr-tools"]
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
// packages/core/dist/stages/npm-detectors/registry.js
|
|
915
|
+
function asAny(d) {
|
|
916
|
+
return d;
|
|
917
|
+
}
|
|
918
|
+
var DETECTOR_REGISTRY = Object.freeze([
|
|
919
|
+
asAny(ssppFuzzDetector),
|
|
920
|
+
asAny(readUnstableDetector),
|
|
921
|
+
asAny(parserDiffDetector)
|
|
922
|
+
]);
|
|
923
|
+
var DETECTOR_REGISTRY_BY_ID = Object.freeze(DETECTOR_REGISTRY.reduce((acc, d) => {
|
|
924
|
+
if (acc[d.id])
|
|
925
|
+
throw new Error(`Duplicate detector id "${d.id}" in DETECTOR_REGISTRY`);
|
|
926
|
+
acc[d.id] = d;
|
|
927
|
+
return acc;
|
|
928
|
+
}, {}));
|
|
929
|
+
function listDetectorIds() {
|
|
930
|
+
return DETECTOR_REGISTRY.map((d) => d.id);
|
|
931
|
+
}
|
|
932
|
+
function getDetectorById(id) {
|
|
933
|
+
return DETECTOR_REGISTRY_BY_ID[id];
|
|
934
|
+
}
|
|
935
|
+
function resolveDetectors(ids) {
|
|
936
|
+
if (!ids || ids.length === 0)
|
|
937
|
+
return { detectors: [...DETECTOR_REGISTRY], unknown: [] };
|
|
938
|
+
const detectors = [];
|
|
939
|
+
const unknown = [];
|
|
940
|
+
for (const id of ids) {
|
|
941
|
+
const d = getDetectorById(id);
|
|
942
|
+
if (d)
|
|
943
|
+
detectors.push(d);
|
|
944
|
+
else
|
|
945
|
+
unknown.push(id);
|
|
946
|
+
}
|
|
947
|
+
return { detectors, unknown };
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
export {
|
|
951
|
+
dedupConfirmation,
|
|
952
|
+
guardPackage,
|
|
953
|
+
runDetectorOnPackage,
|
|
954
|
+
nameMatchesPpSink,
|
|
955
|
+
pollutionSnapshot,
|
|
956
|
+
fuzzCandidate,
|
|
957
|
+
ssppFuzzDetector,
|
|
958
|
+
makePhasedField,
|
|
959
|
+
confirmReadUnstable,
|
|
960
|
+
readUnstableDetector,
|
|
961
|
+
inetAton,
|
|
962
|
+
classify,
|
|
963
|
+
isUnsafe,
|
|
964
|
+
PARSER_DIFF_CORPUS,
|
|
965
|
+
scanDivergences,
|
|
966
|
+
confirmParserDiff,
|
|
967
|
+
parserDiffDetector,
|
|
968
|
+
DETECTOR_REGISTRY,
|
|
969
|
+
DETECTOR_REGISTRY_BY_ID,
|
|
970
|
+
listDetectorIds,
|
|
971
|
+
getDetectorById,
|
|
972
|
+
resolveDetectors
|
|
973
|
+
};
|