Sitelet https://github.com/vpdb/server/pull/553
Skip to content

chore(deps): update dependency adm-zip to v0.6.1 [security] - #553

Open
renovate[bot] wants to merge 1 commit into
masterfrom
renovate/npm-adm-zip-vulnerability
Open

renovate[bot] wants to merge 1 commit into
masterfrom
renovate/npm-adm-zip-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Aug 26, 2026 •

Copy link
Copy Markdown

This PR contains the following updates:

Package Change Age Confidence
adm-zip 0.4.16 → 0.6.1 age confidence

adm-zip: Crafted ZIP file triggers 4GB memory allocation

CVE-2026-39244 / GHSA-xcpc-8h2w-3j85

More information

Details

adm-zip before 0.5.18 is vulnerable to denial of service via a crafted ZIP file with a manipulated uncompressed size header field. In zipEntry.js line 103, Buffer.alloc(_centralHeader.size) allocates memory based on the declared uncompressed size from the ZIP central directory header without validating it against the actual compressed data size or imposing any upper bound. The size value is read directly from the binary header at entryHeader.js line 266 with no bounds check. An attacker can craft a ~120-byte ZIP file that declares ~4GB uncompressed size, causing a memory allocation amplification ratio of over 33 million to 1. The allocation occurs before CRC validation, so the malicious payload cannot be rejected early. All extraction and read methods are affected: readFile(), readAsText(), extractEntryTo(), extractAllTo(), extractAllToAsync(), test(), and entry.getData(). Any application accepting untrusted ZIP files via adm-zip is vulnerable to immediate process crash.

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


adm-zip: Uncontrolled memory allocation via the declared uncompressed size (DoS)

CVE-2026-77301 / GHSA-7q85-xj36-vmfc

More information

Details

Summary

adm-zip allocates an entry's output buffer from the declared uncompressed size (central-directory size field) before validating it against the actual data. A tiny crafted ZIP that declares a huge uncompressed size forces a multi-gigabyte allocation from a few bytes.

Impact

On adm-zip 0.5.17 (latest), Node 24, a 105-byte ZIP with one stored entry declaring size = 1,774,399,200 makes new AdmZip(buf).getEntries()[0].getData() commit ~1.8 GB of resident memory in ~4.4 s before throwing Error: ADM-ZIP: CRC32 checksum failed, roughly 16 million times the input size. Because the buffer is committed before any validation, on a memory-constrained host (containers, serverless, small VMs) the allocation OOM-kills the process before the CRC check (uncatchable), and concurrent requests can exhaust memory even on larger hosts. Any service that reads entries from untrusted ZIPs is exposed to a remote denial of service.

Steps to reproduce

Attachments are not supported in the advisory form, so the 105-byte PoC (sha256 980d34356fbb248fe527b9d0ac3eabc5c99393a374014be6199523de16709386) is inlined as base64 in this self-contained reproducer:

const AdmZip = require('adm-zip');
// 105-byte crafted ZIP, base64-inlined
// sha256 980d34356fbb248fe527b9d0ac3eabc5c99393a374014be6199523de16709386
const b64 = "UEsDBBQAAAAAAAAAAAAAAAAABQAAAAUAAAABAAAAYWhlbGxvUEsBAhQAFAAAAAAAAAAAAAAAAAAFAAAA4C7DaQEAAAAAAAAAAAAAAAAAAAAAAGFQSwUGAAAAAAEAAQAvAAAAJAAAAAAA";
const buf = Buffer.from(b64, "base64");          // 105 bytes
const zip = new AdmZip(buf);
zip.getEntries()[0].getData();   // commits ~1.8 GB, then throws "ADM-ZIP: CRC32 checksum failed"

The single entry declares uncompressed size = 1,774,399,200 with a compressed size of 5. getData() allocates the full declared size before the CRC check runs, so the memory is committed regardless of the (tiny) actual payload.

Root cause

zipEntry.js does Buffer.alloc(<declared uncompressed size>) before checking the declared size against the compressed size / available bytes.

Suggested fix

Validate the declared uncompressed size against the compressed size and a configurable maximum before allocating (yauzl, for example, requires the caller to bound this); reject or stream when the declared size is implausible relative to the input. Happy to send a patch.

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


adm-zip extraction preserves SUID/SGID bits from untrusted ZIPs -> local privilege escalation

CVE-2026-102282 / GHSA-j5f4-cc29-5x44

More information

Details

Summary

adm-zip applies the Unix permission bits stored in a zip entry directly to the extracted file via fs.chmodSync() when keepOriginalPermission=true is passed to extractAllTo()/extractEntryTo() — and it never filters the setuid/setgid/sticky bits out of those bits. A zip crafted by an attacker can therefore produce an extracted binary with mode 04755. When extraction runs as root (the default posture in Docker builds, CI runners, and privileged install steps — the exact environments where this flag is used), the resulting root-owned setuid file is executed later by a lesser-privileged user, turning the attacker's code into a root execution.

Details

The mode a zip entry wants is read back from the external file attributes in the header, and the mask used keeps every special bit:

// headers/entryHeader.js:187
get fileAttr() {
    return (_attr || 0) >> 16 & 0xfff;
}

0xfff is 0o7777 — it preserves setuid (0o4000), setgid (0o2000) and the sticky bit (0o1000) along with the rwx bits. Shifting by 16 is the standard Unix convention for where zip stores the mode; the mask is the problem.

When the flag is on, that value goes straight to the write:

// adm-zip.js:726-727 (extractEntryTo, and identically in extractAllTo)
const fileAttr = keepOriginalPermission ? entry.header.fileAttr : undefined;
filetools.writeFileTo(target, content, overwrite, fileAttr);
// util/utils.js:94
self.fs.chmodSync(path, attr || 0o666);

No & 0o777, no stripping of 0o7000. Attacker-controlled bytes in the zip decide the final mode of a file the library creates on disk. Directory entries are affected too (adm-zip.js:855), so a setgid bit on a directory entry also carries over and gives new files inside it group inheritance.

PoC

Tested against adm-zip@0.6.0 (latest as of 2026-08-01), Node 22, Linux.

  1. Craft a zip with a setuid binary using standard tooling (this is the
    realistic attacker path — no adm-zip APIs involved in creating it):
python3 -c "
import zipfile
zi = zipfile.ZipInfo('pysuidbin')
zi.external_attr = 0o4755 << 16
with zipfile.ZipFile('evil.zip', 'w') as z:
    z.writestr(zi, '#!/bin/sh\nid\n')
"
  1. Extract with the flag enabled:
const AdmZip = require('adm-zip');
new AdmZip('evil.zip').extractAllTo('/tmp/out', true, true);

const fs = require('fs');
const st = fs.statSync('/tmp/out/pysuidbin');
console.log((st.mode & 0o7777).toString(8));
// => 4755   (setuid bit set — the file is root-owned if the extractor runs as root)
  1. Control — same zip, default extraction (keepOriginalPermission=false):
    mode comes out 0666, no setuid. The flag is the enabler.

Alternative supply path, if the zip is built in-process with adm-zip's own API:

const zip = new AdmZip();
zip.addFile('suidbin', Buffer.from('#!/bin/sh\nid\n'), '', 0o4755);
zip.writeZip('evil.zip');
new AdmZip('evil.zip').extractAllTo('/tmp/out', true, true);
// same result: stat mode & 0o7777 === 0o4755
Impact

Privilege escalation.
The vulnerability class is CWE-732 (incorrect permission assignment): permission bits taken from untrusted input are applied with no filtering.

Realistic chain:

  1. Attacker supplies a zip (upload endpoint, fetched dependency archive, artifact in a build script — no special access needed to produce the file).
  2. A pipeline or service extracts it as root with keepOriginalPermission=true. Docker builds run as root by default and CI/install steps commonly do too; this flag is specifically the tooling used in permission-preserving deploy flows.
  3. The root-owned setuid file leaves the build, typically preserved by cp -a/rsync mode-bit propagation, into the runtime environment.
  4. An unprivileged app user or service account executes it (the standard build-as-root/run-as-user model) — the attacker's code runs as root.

Who is impacted: applications and pipelines that extract untrusted archives with keepOriginalPermission=true while running as root.
Default-usage deployments (flag off) are not affected; non-root extraction results in a harmless self-owned setuid file.
Severity: Medium

Suggested fix, one line in the getter:

get fileAttr() {
    return (_attr >> 16) & 0o777;
}

Severity

  • CVSS Score: 7.1 / 10 (High)
  • Vector String: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


adm-zip: Decompression-bomb protection (fix for CVE-2026-39244) can be bypassed by declaring uncompressed size as 0

GHSA-rcw4-f5rp-g42v

More information

Details

Affected package: adm-zip (npm)
Affected version: 0.6.0

Summary

The fix shipped for CVE-2026-39244 (methods/inflater.js) caps zlib's decompression output via maxOutputLength: expectedLength, where expectedLength is read directly from the ZIP entry's attacker-controlled "uncompressed size" header field (CENLEN/LOCLEN). This cap is only applied when expectedLength > 0:

const option = version >= 15 && expectedLength > 0 ? { maxOutputLength: expectedLength } : {};
return zlib.inflateRawSync(inbuf, option);

If an attacker sets the declared uncompressed-size field to exactly 0, this condition is false, option becomes {}, and no output cap is passed to zlib at all. Node then falls back to zlib's own internal default limit (several GB), so a small, highly-compressible payload can still be decompressed to a very large size in memory -- the same class of resource-exhaustion issue the original CVE addressed, just triggered differently.

Steps to Reproduce
  1. Build a ZIP archive containing one DEFLATE-compressed entry whose real content is highly redundant (e.g. several MB of a repeated byte, achieving close to the ~1032:1 theoretical raw-DEFLATE compression ratio).
  2. Patch the entry's declared uncompressed-size fields (both the local file header copy and the central directory copy, 4-byte little-endian values) to 0. The compressed bytes and CRC32 are left untouched -- CRC validation still passes because CRC is computed over the real decompressed output, not the declared size.
  3. Load the archive with new AdmZip(buffer) and call .getEntries()[0].getData() (or readFile/readAsText/extractAllTo/etc. -- all share the same code path).
  4. Observe: decompression succeeds and returns the full-size buffer with no size restriction applied, whereas the same real data with an honest (but undersized) declared value correctly throws Cannot create a Buffer larger than N bytes.
Proof of Concept

Attached script demonstrates a controlled A/B comparison using the identical real payload in both cases -- only the declared-size header field differs:

  • Control (declared size = 1024 bytes, deliberately smaller than the true 4MB output): correctly throws, proving the cap mechanism works when expectedLength > 0.
  • Bypass (declared size = 0, identical real payload): succeeds and returns the full 4,194,304-byte buffer with zero restriction.

Verified reproducible across 3 independent runs.

Impact

Any application that calls adm-zip's read/extract methods on an untrusted ZIP file (upload handlers, CI artifact extraction, email attachment scanning, etc.) can be made to allocate an amount of memory bounded only by zlib's own internal default rather than any limit the application or adm-zip intends -- a small (tens-of-MB) upload can trigger multi-GB memory consumption, risking process crash/OOM.

Suggested Fix

Apply maxOutputLength unconditionally (e.g. defaulting to a sane absolute ceiling, or always passing the declared size regardless of whether it's 0), and/or add an independent compression-ratio check that doesn't rely solely on the attacker-supplied size field.

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Release Notes

cthackers/adm-zip (adm-zip)

v0.6.1

Compare Source

Full Changelog: cthackers/adm-zip@v0.6.0...v0.6.1

  • Updated dev dependencies
  • Fixed uncaught crash in async decompression on malformed DEFLATE data
  • Fixed addLocalFolder following symlinks out of the archived folder
  • Stripped setuid/setgid/sticky bits from extracted file permissions
  • Enforced the decompression size cap on the async path and for size 0
  • Rejected archives with duplicate entry names
  • Blocked extraction from writing through symlinks inside the target
  • Routed malformed-header parse errors through the async callback
  • Rejected zip entries whose declared data extent runs past the buffer
  • Fixed addLocalFolderPromise hanging on empty folders and swallowing errors
  • Fixed addLocalFolderAsync2 mangling local paths on Windows

v0.6.0

Compare Source

==================

Security

  • Fixed CVE-2026-39244: a crafted archive declaring a huge uncompressed size could force an unbounded Buffer.alloc (memory exhaustion / DoS) before any validation. Allocation is now bounded by the data actually present — STORED output is sized from the real bytes, DEFLATED output is grown by the inflater and capped at the declared size (#​568)
  • Hardened the internal entry-name lookup table against object injection: entry names come from untrusted archives, and a name such as __proto__ previously resolved to Object.prototype, crashing addFile and hiding the entry from getEntry/readFile. The table is now prototype-less

Bug fixes

  • Fixed a regression (0.5.15) that rejected valid archives using a data descriptor (general-purpose bit 3). The payload is now validated against the authoritative central-directory CRC instead of requiring/parsing the trailing descriptor (#​548, #​533, #​554)
  • Fixed extractAllTo/extractAllToAsync not restoring directory permissions with keepOriginalPermission; directory modes are applied after their contents are written, deepest path first, and no longer lock the extractor out of a restrictive directory (#​530)
  • Fixed infinite recursion in addLocalFolder when a folder contains a symlink pointing back to an ancestor (e.g. workspace node_modules); the walk now tracks resolved real paths and skips already-visited directories (#​541)
  • Fixed an uncaught exception (ERR_INVALID_ARG_TYPE) that crashed the process when writeFileToAsync could not open the target file (bad permissions, invalid filename, exhausted file descriptors); write failures are now reported through the callback and write errors are no longer silently swallowed (#​470, #​459, #​402)
  • Fixed directory entries reporting an empty name (e.g. a/b/c/ now returns c) (#​466)
  • Fixed extractEntryTo flattening subdirectories when maintainEntryPath is false; the structure below the extracted directory is now preserved instead of collapsing (and overwriting) files by basename (#​306)
  • Fixed a failed utimes aborting extraction; setting the modification time is now best-effort and never fails extraction of already-written content (#​379)
  • Fixed test() always returning false for any archive containing a file (it indexed the entries array with an entry object instead of reading the entry); it now correctly verifies each entry's CRC

Performance

  • Faster entry sorting when writing archives with many entries: names are decoded once instead of on every comparison (about 6× faster sort for large archives)

Added

  • Bundled TypeScript type definitions (types.d.ts), so @types/adm-zip is no longer required

Notes

  • Behavior change: extractEntryTo(dir, target, /* maintainEntryPath */ false) now preserves subdirectories beneath the extracted directory rather than flattening them
  • Behavior change: extraction no longer fails when the modification time cannot be set

v0.5.18

Compare Source

What's Changed
New Contributors

Full Changelog: cthackers/adm-zip@v0.5.17...v0.5.18

v0.5.17

Compare Source

What's Changed

New Contributors

Full Changelog: cthackers/adm-zip@v0.5.16...v0.5.17

v0.5.16

Compare Source

What's Changed

New Contributors

Full Changelog: cthackers/adm-zip@v0.5.15...v0.5.16

v0.5.15

Compare Source

What's Changed

New Contributors

Full Changelog: cthackers/adm-zip@v0.5.14...v0.5.15

v0.5.14

Compare Source

Fixed an issue introduced on version 0.5.13 requiring a new mandatory parameter on the inflater on nodejs version >= 15

v0.5.13

Compare Source

  • Fixed extractAllToAsync callback @​5saviahv
  • Fixed issue with "toAsyncBuffer" where after that command all entries are gone @​5saviahv
  • Minor fixes (tests, typos etc) @​5saviahv
  • Added a an option to specificy the maximum expectedLength of the file to protect against zip bombs or limit memory usage @​undefined-moe
  • Add check for invalid large disk entries @​criyle

v0.5.12

Compare Source

Fixed extraction error

v0.5.11

Compare Source

  • Add support for Info-Zip password check spec for ZipCrypto @​lukemalcolm
  • Extraction of password protected zip entries @​Santa77
  • Fixed unnecessary scanning a local file headers (except in the case of corrupted archives) @​likev
  • Added GitHub actions @​kibertoad
  • Fixed cases when extra data was lost @​yfdyh000
  • Fixed throw empty error in extractAllToAsync on operation done @​Autokaka

v0.5.10

Compare Source

v0.5.9

Compare Source

v0.5.8

Compare Source

v0.5.7

Compare Source

v0.5.6: .

Compare Source

v0.5.5

Compare Source

v0.5.4

Compare Source

==================

  • Fixed relative paths
  • Added zipcrypto encryption
  • Lower verMade for macOS when generating zip file

v0.5.3

Compare Source

==================

  • Fixed filemode when unzipping

v0.5.2

Compare Source

==================

  • Fixed path traversal issue (GHSL-2020-198)

v0.5.1

Compare Source

==================

  • Incremented version (cthackers)
  • Fixed outFileName (cthackers)

v0.5.0

Compare Source

==================

  • Added extra parameter to extractEntryTo so target filename can be renamed (cthackers)
  • Updated dev dependency (cthackers)
  • modified addLocalFolder method (5saviahv)
  • modified addLocalFile method (5saviahv)
  • Deflate needs min V2.0 (5saviahv)
  • Node v6 (5saviahv)
  • Added ZipCrypto decrypting ability (5saviahv)
  • LICENSE filename in package.json (5saviahv)
  • add multibyte-encoded comment with byte length instead of character length (Kosuke Suzuki)
  • Bump lodash from 4.17.15 to 4.17.19 (dependabot[bot])
  • now it works in browser (Emiliano Necciari)

Configuration

📅 Schedule: (UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate
renovate Bot force-pushed the renovate/npm-adm-zip-vulnerability branch from f71eeb0 to a3d970f Compare September 18, 2026 23:12
@renovate renovate Bot changed the title chore(deps): update dependency adm-zip to v0.6.0 [security] chore(deps): update dependency adm-zip to v0.6.1 [security] Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants