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:
entry.header.attris 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.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 explicitchmodafter the fact overrides it, so0o4755survives even on a process running withumask 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 >>> 16is a number the archive author chose; anything derived from it needs a mask before it reacheschmod. 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
0o7000off, not just0o4000. 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& 0o0777for 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.1looks 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-zipasZipEntry.header.attrand consumed duringextractAllTo()/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 explicitchmodbypasses 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-zipdependency 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,