当 npm install 变成入口:冷静看待 ChainDrop
冷静梳理 Shai-Hulud npm 事件:发生了什么,provenance 的边界在哪里,以及团队如何检查 CI secrets 而不把下载量误当受害者数量。
依赖安装本来应该很无聊。ChainDrop 这一轮围绕 Shai-Hulud 的 npm 供应链事件中,危险恰好出现在这个日常步骤:CI runner 或开发者电脑解析到被污染的 npm 包,包在安装阶段执行 lifecycle script,而这些环境里通常放着发布令牌、云访问权限和构建密钥。

8 月 4 日到 9 日发生了什么
Aikido、Socket、SafeDep、JFrog、Unit 42、Wiz、Endor Labs 等团队都发布了分析,指出这是一场与 Keyv 和 Cacheable 包族有关的 npm 供应链攻击。Aikido 报告称,最早可见的集群从 [email protected] 的发布路径被滥用开始,并牵涉 flat-cache、file-entry-cache、cacheable-request、cacheable、@cacheable/memory、cache-manager 等包。The Hacker News 后来把这一周概括为具有蠕虫式扩散特征的活动,影响范围已超出最初命名空间。
不同来源给出的数量不完全一致,这是因为研究人员发布文章时事件仍在变化。JFrog 提到 400 多个受影响包,SafeDep 和 Unit 42 在不同时间给出更高的包或版本计数,Endor Labs 强调某些依赖路径每周下载量巨大。这些数字不能相加,也不能直接当作受害者数量。它们描述的是包、版本或下载规模,不等于每次下载都执行了恶意代码。
为什么它不是普通恶意包事件
很多 npm 恶意事件来自拼写相似的假包或几乎没人用的小包。ChainDrop 更麻烦,因为入口包含许多团队会间接获得的 transitive dependencies。开发者可能从未主动选择 keyv,却通过框架、缓存层、linter、构建工具或服务端库把它带进项目。此时 lockfile 比人的记忆更可靠:危险版本可能藏在你以为自己安装的包下面好几层。
这次活动还击中了常见的信任信号。研究人员称,一些恶意版本通过看似合法的 release workflow 发布,并可能显示有效 provenance。Provenance 仍然有价值,但它回答的是窄问题:哪个 workflow、以哪个身份、在什么仓库上下文中生成了包。它不能证明 maintainer 账号没有被控制,不能证明 release commit 经过真实审查,也不能证明安装脚本适合在你的 runner 上执行。
安装阶段的攻击链
公开报告描述的链条并不复杂。被污染的版本加入 preinstall 之类的生命周期命令,启动 node setup.mjs。随后脚本加载混淆后的 JavaScript,研究人员提到过 Math_Symbol.js、math_init.js 等文件名,并在部分流程中使用 Bun 作为运行时。Bun 本身不是被攻破的组件,只是 payload 使用的工具。
恶意代码进入 CI 或开发机后,会寻找 npm 和 GitHub tokens、云凭据、Vault 和 Kubernetes 材料、SSH keys、.env、Terraform state、Docker 与 Helm 配置,以及 GitHub Actions runner 上短暂出现的 secrets。构建环境特别敏感,因为短生命周期凭据和 OIDC 派生 token 可能正好在 job 期间出现在环境变量或内存中。
蠕虫式扩散来自发布权限。如果 payload 拿到 npm publishing tokens,就可能发布受害者维护的其他包的感染版本;如果拿到 GitHub access,报告中还描述了仓库改动和持久化表面,例如 VS Code tasks 与 Claude Code settings 相关文件。并非每个文件都会自动执行,但它们说明攻击者想把开发自动化变成下一步入口。
如何判断自己是否暴露
先看证据,而不是看口号。按照 Aikido、SafeDep、Socket、JFrog、Unit 42 等来源列出的精确包名和版本,检查 package-lock.json、npm-shrinkwrap.json、pnpm-lock.yaml、yarn.lock、Bun lockfile、内部镜像和依赖扫描记录。不要只看直接依赖;这次问题的核心就是传递依赖。
然后确认相关时间窗口内是否真的执行过 install。CI 日志、构建时间、package-manager cache 创建时间、容器层历史和开发机 shell history,可以区分“lockfile 里出现过”和“代码已经运行过”。如果受影响版本只停留在 lockfile,处理可能限于 pin、更新和记录;如果它在有 secrets 的环境里运行过,就应把该环境视为已暴露。
不要用“升级到 latest”结束处理。升级只能避免未来安装已知坏版本,不能拿回已经复制走的 token。应隔离 runner 或工作站,保留必要日志,重建临时 runner,检查缓存、环境变量、开发工具和仓库改动。撤销 npm 与 GitHub tokens,检查 maintainers、2FA 和权限,再轮换云密钥、Vault、Kubernetes、SSH 与部署 secrets。
长期做法是把安装脚本当成特权代码执行。能用 --ignore-scripts 的地方就用;确实需要 lifecycle scripts 的包要显式批准;测试 npm 12 相关控制以及 pnpm、Yarn、Bun 的对应策略;为自动安装设置最小发布年龄;保持 runner 可重建;把 install、test、publish 分离。ChainDrop 不是开源失败的证明,而是提醒我们:npm install 不是下载,它常常是在钥匙旁边执行代码。
Comments
Sign in to comment.
No comments yet.