
CVE-2026-102282: LPE no adm-zip via preservação de SUID/SGID durante a extração de arquivos (corrigido na 0.6.1)
Atribuição incorreta de permissões no pacote npm adm-zip. A extração pode manter os bits setuid, setgid e sticky de um zip não confiável quando keepOriginalPermission está habilitado, permitindo que um executável setuid pertencente ao root seja plantado no disco a partir de um arquivo fornecido pelo atacante: escalação local de privilégios.
| Pacote | adm-zip (npm) |
| Ecossistema | Node.js |
| CVE | CVE-2026-102282 |
| Advisory | GHSA-j5f4-cc29-5x44 |
| CWE | CWE-732 Atribuição Incorreta de Permissões para 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 |
| Afetado | < 0.6.1 (última versão afetada conhecida: <= 0.6.0) |
| Corrigido | 0.6.1 (11 de setembro de 2026) |
| Publicado | Advisory do mantenedor em 11 de setembro de 2026; GitHub Advisory Database em 29 de setembro de 2026 |
Este repositório contém o write-up, um verificador de versão de dependência, um teste comportamental seguro e uma prova de conceito autocontida construída em torno de um binário verificador benigno que apenas imprime seu contexto de credenciais.
Todas as versões anteriores à 0.6.1 são afetadas, incluindo a 0.6.0.
| Intervalo | Status |
|---|---|
>= 0 e < 0.6.1 | Afetado |
<= 0.6.0 | Última versão afetada conhecida |
>= 0.6.1 | Corrigido |
O bug é alcançável apenas quando todas estas condições são verdadeiras:
keepOriginalPermission=true para extractAllTo(), extractAllToAsync() ou extractEntryTo().A extração padrão deixa a flag desativada e não é afetada. A extração como usuário sem privilégios ainda pode aplicar os bits de modo, mas o arquivo resultante pertence a esse mesmo usuário, portanto não cruza uma fronteira de privilégios.
Os bits de modo Unix ficam nos 16 bits superiores dos atributos externos de arquivo de uma entrada zip. O adm-zip copia esses bits para o caminho extraído.
| Local | Papel |
|---|---|
headers/entryHeader.js — fileAttr | Lê o modo Unix dos atributos externos |
adm-zip.js — extractAllTo, extractAllToAsync, extractEntryTo | Usa fileAttr quando keepOriginalPermission é true |
util/utils.js — chmodSync dentro do helper de escrita de arquivo | Aplica esse modo ao caminho recém-criado |
Extração de diretórios em adm-zip.js | Aplica o mesmo modo aos diretórios extraídos |
Na 0.6.0 e anteriores, fileAttr mascara o atributo deslocado com 0xfff (0o7777). Essa máscara mantém os três bits especiais junto com os bits rwx usuais, e nada no caminho de escrita limpa 0o7000 antes do chmod:
// headers/entryHeader.js (<= 0.6.0)
get fileAttr() {
return (_attr || 0) >> 16 & 0xfff;
}
0o4000)0o2000)0o1000)A extração passa o modo controlado pelo arquivo diretamente para o sistema de arquivos:
// 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);
Observe a assinatura de chamada da 0.6.x: extractAllTo(targetPath, overwrite, keepOriginalPermission) recebe um booleano literal como terceiro argumento.
Passar um objeto de opções como { keepOriginalPermission: true } é
silenciosamente convertido para false, o que mascara a vulnerabilidade em um teste em vez de
demonstrá-la. Ambos os scripts de PoC neste repositório passam true diretamente.
A correção é o commit 6a63c339 (v0.6.1). fileAttr agora mantém apenas os nove bits rwx:
get fileAttr() {
return (_attr || 0) >> 16 & 0o777;
}
keepOriginalPermission=true, a extração lê esse modo através de fileAttr e o passa para chmod.0o7777, setuid, setgid e sticky sobrevivem.chmod for executado como root, o novo arquivo pertence ao root e pode carregar setuid. Uma execução posterior por um usuário menos privilegiado executa o código do proprietário do arquivo como root.Com a flag deixada em seu valor padrão, a extração não aplica o modo do arquivo, e os bits especiais não são gravados.
Tudo fica em poc/. O payload incluído é
verify.c, um programa benigno que imprime seu uid/gid real e
efetivo e, em seu modo padrão, abre um shell com a identidade efetiva. Ele não persiste, não se oculta nem faz nada além de demonstrar o
contexto de credenciais.
| Arquivo | Propósito |
|---|---|
poc/verify.c | Payload verificador setuid benigno |
poc/build-archive.js | Empacota um arquivo em um zip cuja entrada carrega o modo 0o4755 |
poc/extract.js | Extrai um arquivo com keepOriginalPermission=true, como faria um app afetado |
poc/run-demo.sh | Execução ponta a ponta: build, extração como root, execução como o usuário 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
Saída esperada contra a 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
Saída esperada contra a 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
Ressalvas para a demo: