adm-zip: Duplicate ZIP entry names: getEntry() and extractAllTo() resolve to different contentGHSA-p634-w6r4-rjp2
MEDIUMFix: cthackers/adm-zip@05101d4GHSA-p634-w6r4-rjp2 is a medium-severity (CVSS 5.9) CWE-436 vulnerability in adm-zip. A fix is available for adm-zip — see the affected versions and patch details below.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
How urgent is this, really
GHSA-p634-w6r4-rjp2 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.
Where this sits among everything scored
Of 394,803 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.
Real-World Exposure
How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.
adm-zipnpmDescription
Summary
A ZIP file can contain two entries with the identical name. adm-zip keeps both in its internal entry list, but its name-lookup table only retains the last one written. getEntry(name) and extractAllTo() walk these two different internal structures, so they can each resolve a duplicate name to a different entry. An application that validates a named entry's contents via getEntry() before trusting an archive, then extracts the whole archive, can end up approving one file's content while a different file's bytes are what actually land on disk under that name.
Details
zipFile.js:58-83retains both entries inentryListbut overwritesentryTable[name]with only the last one written.adm-zip.js:83-95,658-663usesentryTableforgetEntry()lookups — returns the last duplicate.adm-zip.js:769-914iteratesentryListfor extraction — writes the first duplicate (sync, default overwrite policy).
PoC
const AdmZip = require('adm-zip');
const z = new AdmZip({ noSort: true });
z.addFile('a.txt', Buffer.from('FIRST'));
z.addFile('b.txt', Buffer.from('SECOND'));
const raw = Buffer.from(z.toBuffer());
// rename the a.txt entry to b.txt directly in the raw bytes
for (let at = raw.indexOf('a.txt'); at >= 0; at = raw.indexOf('a.txt', at + 5)) {
raw.write('b.txt', at);
}
const parsed = new AdmZip(raw, { noSort: true });
const validated = parsed.getEntry('b.txt').getData().toString();
parsed.extractAllTo(outDir, false);
// validated === "SECOND", but the file written to disk === "FIRST"
Reproduced on the pinned commit (2b4d84087d45344643e0183756e19191d52815cc)
Impact
An application that checks a named entry's content before trusting an untrusted ZIP, then extracts it, can be made to approve different bytes than what actually gets written to disk — the classic check/use split that this kind of validate-then-extract pattern relies on.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 📦npm | adm-zip | all versions | 0.6.1npm install adm-zip@0.6.1 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for adm-zip, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update adm-zip to 0.6.1 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-p634-w6r4-rjp2 is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
Frequently Asked Questions
Is GHSA-p634-w6r4-rjp2 in your dependencies?
Find it across npm, including transitive dependencies.