adm-zip extraction preserves SUID/SGID bits from untrusted ZIPs -> local privilege escalation
## 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: ```js // 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: ```js // adm-zip.js:726-727 (extractEntryTo, and identically in extractAllTo) const fileAttr = keepOriginalPermission ? entry.header.fileAttr : undefined; filetools.writeFileTo(target, content, overwrite, fileAttr); ``` ```js // util/utils.js:94 self.fs.chmodSync(path, attr || 0o666); ``` No `& 0o777`, no
- Products
- adm-zip
- Affected versions
- Not specified
- Fixed versions
- 0.6.1
- CVEs
- CVE-2026-102282
- Confirmed fixed CVEs
- Not specified
- Updated in this feed
- 28 Oct 2026, 09:04 UTC · Revision 1
Source evidence
adm-zip extraction preserves SUID/SGID bits from untrusted ZIPs -> local privilege escalation
core-osv · Original sourcePublished 29 Sept 2026, 18:24 UTC
## 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: ```js // 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: ```js // adm-zip.js:726-727 (extractEntryTo, and identically in extractAllTo) const fileAttr = keepOriginalPermission ? entry.header.fileAttr : undefined; filetools.writeFileTo(target, content, overwrite, fileAttr); ``` ```js // util/utils.js:94 self.fs.chmodSync(path, attr || 0o666); ``` No `& 0o777`, no
osv:GHSA-j5f4-cc29-5x44
adm-zip
Fixed versions: 0.6.1
Complete source record
{
"id": "1c0d76af4f798db45b3ec1a68f3ae563ccf33f9da14d3f8bbca42ccd00c94565",
"url": "https://osv.dev/vulnerability/GHSA-j5f4-cc29-5x44",
"method": "structured",
"payload": {
"kind": "advisory",
"title": "adm-zip extraction preserves SUID/SGID bits from untrusted ZIPs -> local privilege escalation",
"summary": "## 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: ```js // 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: ```js // adm-zip.js:726-727 (extractEntryTo, and identically in extractAllTo) const fileAttr = keepOriginalPermission ? entry.header.fileAttr : undefined; filetools.writeFileTo(target, content, overwrite, fileAttr); ``` ```js // util/utils.js:94 self.fs.chmodSync(path, attr || 0o666); ``` No `& 0o777`, no",
"evidence": "osv:GHSA-j5f4-cc29-5x44",
"products": [
{
"name": {
"value": "adm-zip",
"evidence": "/affected/0/package/name"
},
"prerequisites": [],
"fixed_versions": [
{
"value": "0.6.1",
"evidence": "/affected/0/ranges"
}
],
"affected_versions": []
}
],
"severity": "high",
"structured": {
"id": "GHSA-j5f4-cc29-5x44",
"aliases": [
"CVE-2026-102282",
"GHSA-j5f4-cc29-5x44"
],
"metrics": [
{
"type": "CVSS_V3",
"score": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N"
}
],
"affected": [
{
"ranges": [
{
"type": "SEMVER",
"events": [
{
"introduced": "0"
},
{
"fixed": "0.6.1"
}
]
}
],
"package": {
"name": "adm-zip",
"purl": "pkg:npm/adm-zip",
"ecosystem": "npm"
},
"database_specific": {
"source": "https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-j5f4-cc29-5x44/GHSA-j5f4-cc29-5x44.json",
"last_known_affected_version_range": "<= 0.6.0"
}
}
],
"modified": "2026-10-08T10:56:40.691028011Z",
"provider": "osv",
"references": [
"https://github.com/cthackers/adm-zip/security/advisories/GHSA-j5f4-cc29-5x44",
"https://github.com/cthackers/adm-zip/commit/6a63c339b83c52915483efacda517660a7a7bf87",
"https://github.com/cthackers/adm-zip",
"https://github.com/cthackers/adm-zip/releases/tag/v0.6.1"
],
"provider_metadata": {
"cwe_ids": [
"CWE-732"
],
"severity": "HIGH",
"github_reviewed": true,
"nvd_published_at": null,
"github_reviewed_at": "2026-09-29T18:24:57Z"
}
},
"related_cves": [
{
"value": "CVE-2026-102282",
"evidence": "/aliases"
}
],
"security_relevant": true,
"severity_evidence": "/database_specific/severity"
},
"source_id": "core-osv",
"document_id": 99128,
"published_at": "2026-09-29T18:24:57+00:00"
}