Back to Blog
high SEVERITY8 min read

adm-zip 0.6.0 Preserves SUID Bits From ZIPs: CVE-2026-102282

The `adm-zip` dependency resolved to 0.6.0 in this project's dependency tree, a version affected by CVE-2026-102282: during extraction it applies the Unix permission bits stored in each ZIP entry's external file attributes verbatim, including the setuid (`04000`), setgid (`02000`), and sticky bits. An attacker who controls an archive passed to `extractAllTo()` or `extractEntryTo()` can therefore have the extractor create a setuid binary owned by whatever user the extraction process runs as. The

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published October 3, 2026•Reviewed October 3, 2026

Answer Summary

`adm-zip` 0.6.0 (npm) is affected by CVE-2026-102282; the upstream affected range was not published in the advisory data available here, but 0.6.0 is the version this project resolved and the one flagged. When an untrusted ZIP is handed to `extractAllTo()` or `extractEntryTo()`, adm-zip copies the Unix mode out of each entry's external file attributes field without masking it, so a crafted entry can be written to disk as a setuid or setgid executable — if extraction runs as root, that is a root-owned `4755` binary any local user can execute for a privilege escalation. The fix is to upgrade `adm-zip` to 0.6.1, which strips the setuid/setgid/sticky bits before applying permissions. No CWE identifier was supplied with this advisory.

Vulnerability at a Glance

cweN/A
fixUpgrade `adm-zip` from 0.6.0 to 0.6.1, which masks special mode bits before writing extracted files
riskA crafted archive can cause the extractor to create a setuid/setgid executable owned by the extracting user, giving a local attacker that user's privileges
languageJavaScript (Node.js)
root causeadm-zip 0.6.0 applies the raw Unix mode from a ZIP entry's external file attributes without masking off `04000`/`02000`/`01000`
vulnerabilityPreservation of SUID/SGID permission bits during ZIP extraction, leading to local privilege escalation

Summary

The adm-zip dependency resolved to 0.6.0 in this project's dependency tree, a version affected by CVE-2026-102282: during extraction it applies the Unix permission bits stored in each ZIP entry's external file attributes verbatim, including the setuid (04000), setgid (02000), and sticky bits. An attacker who controls an archive passed to extractAllTo() or extractEntryTo() can therefore have the extractor create a setuid binary owned by whatever user the extraction process runs as. The fix is a version bump to adm-zip 0.6.1, which masks those special mode bits off before writing files.

Introduction

adm-zip is one of the most widely used pure-JavaScript ZIP libraries on npm, and almost every use of it ends in the same two calls: new AdmZip(buffer).extractAllTo(target, overwrite) or zip.extractEntryTo(entry, target, maintainEntryPath, overwrite). Both of those calls do more than write bytes — they also try to be faithful to the archive, and part of being faithful is restoring the file mode that the archive author recorded.

That mode does not come from your code. It comes from the external file attributes field in the ZIP central directory. On Unix-created archives, the high 16 bits of that 32-bit field hold the st_mode value of the original file, which is why unzip can preserve 0755 on a shell script. adm-zip reads the same field (exposed on a ZipEntry as header.attr, shifted down by 16 bits) and hands the result to the filesystem as the mode for the file it creates.

The problem in 0.6.0 is that st_mode is not just read/write/execute. The same field carries 04000 (setuid), 02000 (setgid), and 01000 (sticky). A permission value of 0o104755 in that field means "regular file, setuid, rwxr-xr-x" — and 0.6.0 will happily reproduce it. CVE-2026-102282 is exactly that: the extractor treats an attacker-controlled integer as a trustworthy permission mask, with no filtering of the bits that change who a program runs as.

If you write code that unpacks archives, container layers, plugin bundles, or build caches, this is the class of bug worth internalising: archive metadata is attacker input, and permission metadata is the most dangerous kind.

Affected Versions

Affected unknown — the upstream range was not published in the advisory data available; adm-zip 0.6.0 is the version resolved in this project and the one flagged
Fixed in unknown as a published range — the upgrade in this change targets 0.6.1, which the advisory references as carrying the fix
Ecosystem npm
CVE / GHSA CVE-2026-102282 / GHSA not assigned
CWE unknown

The Vulnerability Explained

What the change actually contains

This fix is a dependency resolution change only. The lockfile entry for adm-zip moved one patch version:

"adm-zip": {
-   "version": "0.6.0",
+   "version": "0.6.1",
    "license": "MIT",
    "engines": { "node": ">=14.0" }
}

(Resolved URL and integrity metadata were updated alongside it and are omitted here.) No first-party source changed, because the flaw lives entirely inside the library's extraction routine.

The vulnerable pattern

Inside adm-zip 0.6.0, the extraction path for a regular file is approximately:

// illustrative reconstruction of the 0.6.0 behaviour
const mode = entry.header.attr >>> 16;      // attacker-controlled: from the ZIP central directory
fs.writeFileSync(targetPath, content);
if (mode) {
  fs.chmodSync(targetPath, mode);           // ← applied with no mask
}

The problematic line is the chmodSync() call. Two things make it dangerous:

  1. entry.header.attr is attacker data. It is a field in the archive, not a derived value. Any ZIP writer — including a 30-line script — can set it to an arbitrary 32-bit integer.
  2. chmodSync() ignores the process umask. A restrictive umask is the usual backstop against overly permissive extracted files, but it only applies to the mode passed at creation time. An explicit chmod after the fact overrides it, so 0o4755 survives even on a process running with umask 077.

Attack scenario against this code path

Consider a service that accepts an uploaded ZIP — a theme bundle, a plugin, a CI artifact, a data import — and unpacks it:

const AdmZip = require("adm-zip");
new AdmZip(uploadedBuffer).extractAllTo("/opt/app/plugins", true);

The attacker builds an archive containing one entry, helper, whose payload is a tiny C binary that does setuid(0); execl("/bin/sh", ...), and sets that entry's external file attributes to 0o104755 << 16. They upload it.

On extraction, adm-zip writes helper and then calls chmodSync(path, 0o4755). If the extracting process runs as root — which is the default in a great many container images, systemd units, installer scripts, and self-hosted CI runners — the result is a root-owned, setuid, world-executable binary on disk. Any local account, any other container process sharing that mount, or any subsequent low-privilege stage of the same build pipeline can now execute it and become root. The attacker never needed to exploit the service's own logic; they only needed it to unpack a file.

The setgid variant is just as useful and works without root: an entry with mode 0o2755 lands as a setgid executable in whatever group the extracting user belongs to, which is often a shared app or docker group. That is a lateral-movement primitive rather than a direct root, but it is the same bug.

Real-world impact

The severity is rated high because the capability granted is persistent and execution-independent. Unlike a transient RCE, a planted setuid binary sits on disk until someone notices it. It also survives the request that created it, meaning the attack window is "whenever a local attacker gets around to it". Detection is poor in practice: file-integrity monitoring that watches for content changes in a plugin directory will see a legitimate-looking upload, and the only anomalous signal is the mode bits.

The Fix

Before and after

The project's resolved adm-zip version changed from 0.6.0 to 0.6.1. In prose: one patch-level dependency bump, no API changes, no call-site edits required.

Upstream, 0.6.1 sanitises the mode before it reaches the filesystem:

// 0.6.0: raw archive mode applied
const mode = entry.header.attr >>> 16;
fs.chmodSync(targetPath, mode);

// 0.6.1: special bits stripped first
const mode = (entry.header.attr >>> 16) & 0o0777;
fs.chmodSync(targetPath, mode);

Why this specific change resolves it

Masking with 0o0777 keeps the behaviour users actually want from an extractor — executable scripts stay executable, read-only files stay read-only — while discarding the three bits that have security meaning beyond file access:

  • 04000 (setuid): the bit that makes a binary run as its owner rather than its invoker. Removing it is what defeats the root-shell scenario.
  • 02000 (setgid): the group equivalent, and on directories the bit that forces group inheritance for everything created inside.
  • 01000 (sticky): less dangerous, but it alters deletion semantics in shared directories and has no legitimate reason to arrive from an untrusted archive.

The mask is applied at the point of use rather than at parse time, which matters: entry.header.attr stays faithful to the archive for anyone inspecting metadata, while the filesystem never sees the dangerous bits. That is the right boundary — sanitise at the sink, not by rewriting the data model.

Verification caveat on this change

This pull request was produced from a dependency-tree match. The scanner confirmed that adm-zip 0.6.0 was resolved in the project and that CVE-2026-102282 applies to it; it did not confirm that first-party code reaches extractAllTo() or extractEntryTo(), and no automated test run was possible against the repository. Treat the bump as a safe, low-risk patch upgrade, and confirm reachability separately before deciding how urgently the deployed version needs rotating. If extraction does happen, check whether it runs as root — that single fact is the difference between "annoying" and "root on the host".

Key Takeaways

  • The Unix mode in a ZIP entry's external file attributes is attacker input, not metadata you can trust. entry.header.attr >>> 16 is a number the archive author chose; anything derived from it needs a mask before it reaches chmod.
  • fs.chmodSync() defeats your umask. Hardening the process umask does not protect against an extractor that explicitly sets permissions after creating the file, which is exactly what adm-zip 0.6.0 does.
  • Mask 0o7000 off, not just 0o4000. The setgid bit on an extracted directory silently changes group ownership for every file written into it afterwards, and the sticky bit changes deletion rules. adm-zip 0.6.1 masks with & 0o0777 for this reason.
  • Extraction privilege is the real amplifier here. The same crafted archive is a nuisance when unpacked as an unprivileged service account and a root compromise when unpacked by a root-running container entrypoint or CI runner — audit who calls extractAllTo(), not just whether it is called.
  • Patch-level bumps on archive libraries deserve attention. 0.6.0 → 0.6.1 looks cosmetic in a lockfile diff, but on a library whose entire job is turning untrusted bytes into files, patch releases are frequently permission- or path-safety fixes.

How Orbis AppSec Detected This

  • Source: the Unix permission bits in the external file attributes field of an untrusted ZIP entry, surfaced by adm-zip as ZipEntry.header.attr and consumed during extractAllTo() / extractEntryTo().
  • Sink: the mode argument passed to fs.chmodSync() by the library's file-extraction routine, applied after the entry contents are written.
  • Missing control: no mask stripping the setuid, setgid, and sticky bits (0o7000) from the archive-supplied mode before it was applied, and no reliance on the process umask (which an explicit chmod bypasses anyway).
  • CWE: unknown — no CWE identifier was supplied with CVE-2026-102282 in the advisory data available for this finding.
  • Fix: upgrade the resolved adm-zip dependency from 0.6.0 to 0.6.1, the release that masks special mode bits off before applying extracted file permissions. Advisory: CVE-2026-102282.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

CVE-2026-102282 is a small bug with an outsized consequence: adm-zip 0.6.0 trusted a 16-bit integer from an untrusted archive enough to hand it straight to chmod,

Prevention and further reading

Frequently Asked Questions

Does adm-zip 0.6.1 change the signature of `extractAllTo()` or `extractEntryTo()`?

No. 0.6.1 is a patch release on the same 0.6 line — the call signatures, the `overwrite`/`keepOriginalPermission` style options, and the declared `node >= 14.0` engine constraint are unchanged. The only behavioural difference is that special mode bits from the archive are no longer applied.

Am I safe if my service extracts uploaded ZIPs as a non-root user?

The impact is much lower, but not zero. A setgid entry can still be written with a shared group the extracting user belongs to, and a setuid binary owned by the service account is a useful lateral-movement target for any other local user who can execute it.

The pull request only touched the dependency lockfile — does that mean my code actually calls the vulnerable path?

Not necessarily. The scanner found `adm-zip` 0.6.0 resolved in the dependency tree and matched it against CVE-2026-102282; it did not prove that your code or a transitive dependency reaches `extractAllTo()`. The bump to 0.6.1 is safe either way, but reachability should be confirmed before prioritising it as an active exposure.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #2

Related Articles

high

requestInput() Type Confusion: NaN and Object Bypass in JavaScript

The `requestInput()` utility function lacked validation on its `type` parameter and failed to handle `NaN` results from float conversions, creating a type confusion weakness. An attacker could supply malformed inputs that propagate unhandled `NaN` values or unexpected object types through the type system. The fix adds explicit guards against `NaN` type parameters and rejects non-primitive type values.

critical

No Rate Limit on /api/uploads/presign Enables DoS

The `/api/uploads/presign` endpoint accepted unlimited concurrent requests to generate storage presigned URLs, giving an attacker a free lever to exhaust storage-provider quotas and server resources. The fix adds an `express-rate-limit` middleware capping each client to 30 requests per minute on that route.

high

CVE-2026-54673: builder-util-runtime Leaks Auth Headers on Redirect

electron-updater and electron-builder rely on builder-util-runtime to fetch update manifests and artifacts over HTTP. A flaw in that shared HTTP executor allowed credential headers attached to the original update-feed request to be re-sent after a redirect, exposing them to any host the redirect pointed to. The project fixes this by upgrading builder-util-runtime to 9.7.0 and collapsing a duplicate, older copy of the package that electron-updater had pinned on its own.

high

image-size 1.2.1 DoS: Zero-Valued Dimensions in Image Buffer Parser

A high-severity denial-of-service vulnerability in image-size 1.2.1 allows attackers to crash Node.js services using malicious image buffers with zero-valued dimensions. The fix removes the vulnerable `queue` dependency and tightens dimension validation in version 2.0.3.

high

linkify-it 5.0.1 mailto: Link Parsing Causes DoS

linkify-it versions up to 5.0.1 can be forced into excessive processing time when autolinking a specially crafted mailto: link, allowing a remote attacker to degrade or stall the parsing thread. Upgrading to linkify-it 5.0.2 closes the issue; any application that runs linkify-it (directly or via markdown-it) against untrusted text should update immediately.

high

undici 8.10.0 Cache Poisoning: CVE-2026-85152 Auth Bypass

undici, the HTTP client used by Node.js's `fetch()` implementation, shipped a caching layer that did not properly isolate cached responses by origin, letting a response poisoned on one origin be served to requests for another. This created a path to cross-origin authentication bypass, tracked as CVE-2026-85152 and fixed in undici 8.10.2.