
CVE-2026-102282: LPE en adm-zip mediante la preservación de SUID/SGID durante la extracción de archivos (corregido en 0.6.1)
Asignación incorrecta de permisos en el paquete npm adm-zip. La extracción puede conservar los bits setuid, setgid y sticky de un zip no confiable cuando keepOriginalPermission está habilitado, lo que permite plantar en disco un ejecutable setuid propiedad de root a partir de un archivo proporcionado por un atacante: escalada de privilegios local.
| Paquete | adm-zip (npm) |
| Ecosistema | Node.js |
| CVE | CVE-2026-102282 |
| Aviso | GHSA-j5f4-cc29-5x44 |
| CWE | CWE-732 Asignación incorrecta de permisos para un recurso crítico |
| CVSS v3.1 | 7.1 Alta — CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N |
| Afectadas | < 0.6.1 (última afectada conocida: <= 0.6.0) |
| Corregida | 0.6.1 (11 de septiembre de 2026) |
| Publicado | Aviso del mantenedor el 11 de septiembre de 2026; GitHub Advisory Database el 29 de septiembre de 2026 |
Este repositorio contiene el análisis, un verificador de versiones de dependencias, una prueba de comportamiento segura y una prueba de concepto autocontenida construida en torno a un binario verificador benigno que solo imprime su contexto de credenciales.
Todas las versiones anteriores a la 0.6.1 están afectadas, incluida la 0.6.0.
| Rango | Estado |
|---|---|
>= 0 y < 0.6.1 | Afectada |
<= 0.6.0 | Última afectada conocida |
>= 0.6.1 | Corregida |
El fallo solo es alcanzable cuando todas estas condiciones se cumplen:
keepOriginalPermission=true a extractAllTo(), extractAllToAsync() o extractEntryTo().La extracción predeterminada deja el flag desactivado y no se ve afectada. La extracción como usuario sin privilegios todavía puede aplicar los bits de modo, pero el archivo resultante es propiedad de ese mismo usuario, por lo que no cruza una frontera de privilegios.
Los bits de modo de Unix residen en los 16 bits altos de los atributos de archivo externos de una entrada zip. adm-zip copia esos bits en la ruta extraída.
| Ubicación | Rol |
|---|---|
headers/entryHeader.js — fileAttr | Lee el modo Unix de los atributos externos |
adm-zip.js — extractAllTo, extractAllToAsync, extractEntryTo | Usa fileAttr cuando keepOriginalPermission es true |
util/utils.js — chmodSync dentro del helper de escritura de archivos | Aplica ese modo a la ruta recién creada |
adm-zip.js extracción de directorios | Aplica el mismo modo a los directorios extraídos |
En 0.6.0 y anteriores, fileAttr enmascara el atributo desplazado con 0xfff (0o7777). Esa máscara conserva los tres bits especiales junto con los bits rwx habituales, y nada en la ruta de escritura limpia 0o7000 antes de chmod:
// headers/entryHeader.js (<= 0.6.0)
get fileAttr() {
return (_attr || 0) >> 16 & 0xfff;
}
0o4000)0o2000)0o1000)La extracción pasa el modo controlado por el archivo directamente al sistema de archivos:
// 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);
Nótese la firma de llamada de 0.6.x: extractAllTo(targetPath, overwrite, keepOriginalPermission) toma un booleano literal como tercer argumento.
Pasar un objeto de opciones como { keepOriginalPermission: true } se convierte
silenciosamente a false, lo que enmascara la vulnerabilidad en una prueba en lugar de
demostrarla. Ambos scripts de PoC en este repositorio pasan true directamente.
La corrección es el commit 6a63c339 (v0.6.1). fileAttr ahora conserva solo los nueve bits rwx:
get fileAttr() {
return (_attr || 0) >> 16 & 0o777;
}
keepOriginalPermission=true, la extracción lee ese modo a través de fileAttr y lo pasa a chmod.0o7777, setuid, setgid y sticky sobreviven.chmod se ejecuta como root, el nuevo archivo es propiedad de root y puede llevar setuid. Una ejecución posterior por parte de un usuario con menos privilegios ejecuta el código del propietario del archivo como root.Con el flag en su valor predeterminado, la extracción no aplica el modo del archivo y los bits especiales no se escriben.
Todo vive bajo poc/. El payload incluido es
verify.c, un programa benigno que imprime su uid/gid real y
efectivo y, en su modo predeterminado, abre un shell con la identidad
efectiva. No persiste, no se oculta ni hace nada más allá de demostrar el
contexto de credenciales.
| Archivo | Propósito |
|---|---|
poc/verify.c | Payload verificador setuid benigno |
poc/build-archive.js | Empaqueta un archivo en un zip cuya entrada lleva el modo 0o4755 |
poc/extract.js | Extrae un archivo con keepOriginalPermission=true, como lo haría una aplicación afectada |
poc/run-demo.sh | Ejecución de extremo a extremo: compilar, extraer como root, ejecutar como el usuario invocador |
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
Salida esperada contra 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
Salida esperada contra 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
Advertencias para la demo: