导语:你想运行 tsc,环境却先挑了别的程序
项目脚本里的一句 tsc 看上去很清楚:运行 TypeScript 编译器。可是在 pnpm 生成的 POSIX bin 垫片里,找到编译器之前还要解析符号链接、计算目录、判断运行环境。若垫片以裸命令名调用辅助程序,脚本所继承的 PATH 会决定谁先被运行。被放在前面的恰是项目依赖提供 bin 的 node_modules/.bin。
【事实】pnpm 12.4.2 发布说明于 2026-09-15说明:依赖可借可执行文件接管其他包的 POSIX bin 垫片,更新后的修复要求重新安装依赖以更换已有垫片。修复不是安装脚本漏洞的同义词,也不是任何旧版实例已被入侵的证据。
背景与趋势:PATH 是便利功能,也是执行权分配表
执行项目脚本时,包管理器把 node_modules/.bin 放入 PATH 前列,让开发者不必写完整路径。依赖包的 bin 字段于是获得了"供项目脚本按名称找到"的能力,这本来是正常行为。问题在于包管理器自己的内部垫片也把这份搜索顺序用于找可信辅助程序。
【事实】Issue #14837于 2026-09-11 指出,垫片头部原以裸名称解析 readlink、dirname、sed、uname。在 pnpm run 上下文中,依赖的 bin 有机会抢占同名系统工具;辅助命令先于目标包的主程序执行,并可影响目标路径。该 Issue 中的原理说明并不表示所有依赖都提供这些名称。
【推断】风险评估应先确定项目是否实际在 POSIX 垫片上运行构建/测试工具,是否引入未知或低可信的依赖 bin,以及执行身份能读写哪些凭据、缓存与制品。只扫描 scripts.postinstall 不能覆盖这条运行时链路。
技术原理:两个合法机制叠加出了错误信任来源
项目脚本 → 前置 node_modules/.bin 的 PATH → 某包的命令垫片
→ 裸名称辅助工具由 PATH 选中 → 得到目标路径 → 启动目标
这里没有把文本拼进 shell 命令的注入步骤。攻击者潜在影响的是"程序名字对应哪个可执行文件";搜索顺序使一个依赖的 bin 有机会成为另一个依赖垫片的辅助程序。即使最终目标仍是正确的 tsc,辅助程序也已经先执行过;若它还能改变返回的路径,实际运行目标也可能偏离预期。
【事实】已合并 PR #14845于 2026-09-13 把 readlink、sed、uname 改用 POSIX command -p 的系统默认搜索路径,并把 dirname 替换为 shell 参数展开。其回归测试放置同名诱饵命令在调用者 PATH 前,核验垫片不会选择错误辅助程序或错误目标。command -p 并不是"在所有平台写死 /usr/bin"。
风险影响与证据边界
【事实】官方描述的是依赖可抢占其他包的 POSIX 垫片。它需要项目使用相关垫片、依赖 bin 在搜索路径前列且实际触发命令;并非无需用户运行任何构建/测试命令的网络 RCE。官方没有为本问题在上述材料中列出 CVE、统一 CVSS 或在野攻击数量。
【事实】发布说明还明确说 Cygwin、MSYS2 和 WSL 中 cygpath/wslpath 的路径转换仍通过 PATH,依赖可继续重定向命令;Issue #14866单独跟踪。不能用"升级 12.4.2"证明这些环境已完全安全。
【推断】在高权限 CI Runner 上,意外先执行依赖 bin 可能威胁构建输出、仓库工作区或可用的短期凭据;影响依赖运行环境,不能把"可能"写成"已发生窃密"。
无害案例:只比较选中哪个字符串
在内存中模拟两条路径的优先级,不创建可执行文件、不运行 shell:
search = ["project/.bin", "system/bin"]
names = {"project/.bin/readlink": "dependency",
"system/bin/readlink": "trusted-helper"}
chosen = next(names[f"{d}/readlink"] for d in search if f"{d}/readlink" in names)
assert chosen == "dependency"
assert names["system/bin/readlink"] == "trusted-helper"
类比模型只解释搜索顺序,不是 pnpm 的源码 PoC;真实 command -p 使用系统默认路径,不能从示例的 system/bin 猜出平台实际目录。隔离测试可进一步审查生成垫片头部和依赖声明的 bin 清单,避免针对生产机器做攻击验证。
开发与安全团队怎么做
【建议·P0】核对 pnpm 版本、项目实际生成的垫片及 CI/本机环境;12 系列升级到 12.4.2或经过确认的更高版本,并依发布说明重装依赖、验证旧垫片已更新。不要只用 pnpm --version 代替生成文件核验。对 WSL 等环境另行收紧依赖来源及 Runner 权限。
【建议·P1】针对新增或变更的依赖检查 bin 名称是否与构建工具、系统辅助程序重名。验证脚本运行时 PATH 的构成,避免在共享 Runner 中把不可信目录作为内部工具的可信搜索目录。凭据只在必要阶段注入,并控制制品写入权限。
【建议·P2】把"依赖 bin 名称审计 + 垫片诱饵 PATH 负向测试 + 运行环境隔离"写入 CI 基线;对生成的脚本搜索更多裸名称外部辅助程序,避免只围绕四个已修复名字做一次性整改。
总结
PATH 不是单纯的环境变量,它决定"程序名"到底映射到谁。项目依赖可以占有自己的命令名,但不能让它决定包管理器内部辅助程序的身份。pnpm 12.4.2 修复了主要 POSIX 路径的这道边界;发布说明要求的重装与兼容环境的剩余风险,同样属于处置的一部分。