新加坡网络安全局(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.mjsMath_Symbol.jsmath_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-scriptsnpm ci --ignore-scripts),确实需要构建脚本的包单独处理;构建与运行用不同的低权限用户;给 SSH 私钥加口令;关掉 agent forwarding;生产机上不放能直推生产的密钥。

把构建和运行分开,本身就是一道防线

这类事故里损失最惨的,通常是「一台机器什么都干」:既是构建机、又是生产机、还存着通往其他机器的密钥。把构建放到一台独立实例上,让它没有生产凭证、也没有到生产的免密通道,投毒包最多毁掉一台可随时重建的机器。用 PM2 在 VPS 上部署 Node.js 应用里有进程与用户隔离的做法,VPS 备份策略里有让「重建」真正可行的前提条件。SharkCloud 的实例按小时计费、可随时重开,多开一台专门做构建的机器,成本远低于一次凭证泄露。