新加坡网络安全局(CSA)的通告 AD-2026-009 于 2026 年 8 月 6 日发布,9 月 7 日仍在更新——一个月过去还在滚动更新,本身就说明事件没有结束。规模:npm 上超过 1,300 个包版本被投毒,这些包合计每月约 20 亿次下载。
被投毒的是缓存类基础包,多数人是间接依赖
通告点名的典型版本包括 keyv 6.0.0、cacheable 2.5.1、cacheable-request 13.0.20、flat-cache 6.1.24、file-entry-cache 11.1.6。注意这些包的共同点:几乎没人会直接安装它们,它们躺在 ESLint、各类 HTTP 客户端和构建工具的依赖树深处。也就是说,「我没装过这些包」不是一个有效的判断,必须真的去查依赖树。
它偷什么:清单里就有 SSH 私钥
恶意软件名为 ChainDrop,是 Shai-Hulud 家族的自传播变种。它窃取并外传的内容包括:云服务凭证、GitHub 令牌、SSH 私钥、Kubernetes 配置、Terraform 凭证,外传目标域名为 npm-cache[.]com(这个名字显然是照着 npm 官方基础设施起的)。拿到这些之后,它继续用被盗的发布者凭证去污染更多包——这就是它能滚到 1,300 多个版本的原因。
微软威胁情报在 2026 年 8 月 4 日的分析里补充了执行细节:涉及 400 多个包、跨多个互不相关的发布者,载荷是重度混淆的 Bun 版 JavaScript,通过 npm 的 preinstall 生命周期钩子在安装完成之前就执行。CSA 通告点名的钩子文件是 setup.mjs、Math_Symbol.js、math_init.js。
最初的入口不是什么高深的零日:一封伪装成 npm 安全告警的钓鱼邮件骗走了一个维护者的账号。
为什么服务器管理员必须把这当成自己的事
关键在于 preinstall 钩子是以你的用户身份运行的。它能读到的东西,就是你能读到的东西。而在一台既跑生产又顺手 npm install 的机器上,同一个用户目录下通常同时躺着:
~/.ssh/id_ed25519——很可能还是一把能免密登录其他服务器的密钥- 云厂商 CLI 的凭证缓存、
.env里的数据库密码和 API key ~/.docker/config.json、~/.kube/config、~/.terraform.d/
换句话说,供应链攻击在开发者笔记本上偷的是代码权限,在服务器上偷的是整片基础设施的钥匙。
现在就做的四件事
- 查依赖树:
npm ls keyv cacheable cacheable-request flat-cache file-entry-cache。也可以直接grep -n "keyv\|flat-cache\|file-entry-cache" package-lock.json看锁定的具体版本号。 - 命中就按已失陷处理。CSA 的建议很直接:移除受影响版本、把系统当作已被入侵、装过恶意包的机器重建、轮换全部暴露的凭证、检查云与代码托管环境有没有异常访问。轮换的范围包括 SSH 密钥、GitHub token、云 API key,而不只是改个密码。
- 查历史外连:有没有连过
npm-cache[.]com。这条能给出「有没有真的被执行过」的直接证据。 - 改掉习惯:CI 与生产环境的安装一律加
--ignore-scripts(npm ci --ignore-scripts),确实需要构建脚本的包单独处理;构建与运行用不同的低权限用户;给 SSH 私钥加口令;关掉 agent forwarding;生产机上不放能直推生产的密钥。
把构建和运行分开,本身就是一道防线
这类事故里损失最惨的,通常是「一台机器什么都干」:既是构建机、又是生产机、还存着通往其他机器的密钥。把构建放到一台独立实例上,让它没有生产凭证、也没有到生产的免密通道,投毒包最多毁掉一台可随时重建的机器。用 PM2 在 VPS 上部署 Node.js 应用里有进程与用户隔离的做法,VPS 备份策略里有让「重建」真正可行的前提条件。SharkCloud 的实例按小时计费、可随时重开,多开一台专门做构建的机器,成本远低于一次凭证泄露。