npm 包 adm-zip 中存在不正确的权限分配。当启用 keepOriginalPermission 时,解压过程会保留来自不可信 zip 的 setuid、setgid 和 sticky 位,从而允许从攻击者提供的归档文件中将一个 root 所有的 setuid 可执行文件植入磁盘:本地权限提升。
| 包 | adm-zip (npm) |
| 生态系统 | Node.js |
| CVE | CVE-2026-102282 |
| 公告 | GHSA-j5f4-cc29-5x44 |
| CWE | CWE-732 关键资源的不正确权限分配 |
| 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(2026 年 9 月 11 日) |
| 发布时间 | 维护者公告 2026 年 9 月 11 日;GitHub Advisory Database 2026 年 9 月 29 日 |
本仓库包含一份分析报告、一个依赖版本检查器、一个安全的行为测试,以及一个围绕良性验证器二进制文件构建的自包含概念验证,该二进制文件仅打印其凭据上下文。
0.6.1 之前的每个版本都受影响,包括 0.6.0。
| 范围 | 状态 |
|---|---|
>= 0 且 < 0.6.1 | 受影响 |
<= 0.6.0 | 最后已知受影响版本 |
>= 0.6.1 | 已修复 |
该漏洞仅在以下所有条件均成立时才可触发:
keepOriginalPermission=true 传递给 extractAllTo()、extractAllToAsync() 或 extractEntryTo()。默认解压不启用该标志,因此不受影响。以非特权用户身份解压仍可应用这些模式位,但生成的文件归该用户所有,因此不会跨越权限边界。
Unix 模式位位于 zip 条目外部文件属性的高 16 位中。adm-zip 会将这些位复制到解压路径上。
| 位置 | 作用 |
|---|---|
headers/entryHeader.js — fileAttr | 从外部属性中读取 Unix 模式 |
adm-zip.js — extractAllTo、extractAllToAsync、extractEntryTo | 当 keepOriginalPermission 为 true 时使用 fileAttr |
util/utils.js — 文件写入辅助函数中的 chmodSync | 将该模式应用于刚创建的路径 |
adm-zip.js 目录解压 | 将相同模式应用于解压出的目录 |
在 0.6.0 及更早版本中,fileAttr 使用 0xfff(0o7777)对移位后的属性进行掩码。该掩码保留了三个特殊位以及通常的 rwx 位,并且写入路径中没有任何内容在 chmod 之前清除 0o7000:
// 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,并在其默认模式下以有效身份打开一个 shell。它不会持久化、隐藏或做任何超出演示凭据上下文的事情。
| 文件 | 用途 |
|---|---|
poc/verify.c | 良性 setuid 验证器载荷 |
poc/build-archive.js | 将文件打包为条目携带模式 0o4755 的 zip |
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
演示的注意事项:
nosuid 挂载的文件系统上;脚本在检测到该挂载选项时会发出警告。sudo 运行,以便第 5 步有一个非 root 账户可降权到)。scripts/behavior-test.js 直接验证已安装的 adm-zip 副本,无需 root 也无需载荷:它构建一个携带 0o4755 的内存 zip 条目,使用 keepOriginalPermission=true 解压它,并检查 setuid 位是否保留。
npm install [email protected] && node scripts/behavior-test.js # -> exit 1, VULNERABLE
npm install [email protected] && node scripts/behavior-test.js # -> exit 0, not vulnerable
对于以 root 身份使用 keepOriginalPermission=true 解压不可信 zip 的应用程序和流水线,会造成本地权限提升。
这种组合出现在 Docker 镜像构建(默认 root)、CI 作业以及有意保留权限的安装或部署步骤中。该文件可以通过保留模式的复制(cp -a、rsync)离开构建环境,并在之后由运行时用户执行。
在已发布的 CVSS 向量中,主机的机密性和完整性均为高。可用性不计入评分。攻击是本地性的:攻击者需要一个 zip 被特权进程解压,然后以较低权限执行结果。
将 adm-zip 升级到 0.6.1 或更高版本。
npm install adm-zip@^0.6.1
确认解析到的副本,包括传递依赖:
npm ls adm-zip
node scripts/check-version.js .
在每一个副本都升级之前:
keepOriginalPermission=true 解压不可信归档。npm audit、npm ls adm-zip 或本仓库的检查器(见下文)可识别项目或锁文件中易受攻击的副本。scripts/behavior-test.js 可回答给定环境是否仍携带易受攻击的解压行为。find /path/extracted -type f -perm /7000。在来自不可信归档且经过易受攻击版本的解压目录树中发现的任何命中都应视为恶意。检查项目、adm-zip 安装或包本身的 package.json:
node scripts/check-version.js /path/to/project
node scripts/check-version.js /path/to/project/node_modules/adm-zip/package.json
node scripts/check-version.js --self-test
该脚本会查看 node_modules/adm-zip/package.json,如果存在,还会查看 package-lock.json 或 npm-shrinkwrap.json 中的 adm-zip 条目(包括 packages 映射和顶层 dependencies 部分)。退出码 0 表示它找到的每个副本都是 >= 0.6.1。退出码 1 表示安装了或锁定了易受攻击的副本,或无法解析某个版本(该工具采用失败关闭策略)。退出码 2 表示未找到 adm-zip。
示例:
VULNERABLE 0.6.0 install node_modules/adm-zip/package.json
fixed in 0.6.1
已知限制:不解析 yarn.lock 和 pnpm-lock.yaml,不遍历 lockfile-v1 dependencies 树中的嵌套副本,已安装的嵌套 node_modules/**/node_modules/adm-zip 副本只能通过锁文件看到。运行 npm ls adm-zip 以获得完整的依赖树覆盖。
与维护者协调披露;在本仓库发布之前已在上游 0.6.1 中修补并发布。公告记录为 GHSA-j5f4-cc29-5x44。本 PoC 仅为防御性理解和修复而发布:请仅针对你拥有或获授权测试的系统使用它。