Mini Shai-Hulud 蠕虫:分析 TanStack NPM 供应链攻击
JavaScript 生态系统长期以来一直是供应链攻击的重灾区,但最近涉及热门 TanStack 包的事件凸显了这些威胁中一种可怕的演变。最初只是对合法包的劫持,现在已演变成“Mini Shai-Hulud”蠕虫——一种旨在劫持 CI/CD 流水线、窃取开发者密钥并惩罚那些试图撤销其凭据的人的自我传播恶意软件。
这次攻击不仅仅是一个被盗密码的案例;它是对现代包管理器和 CI/CD 工作流中固有信任模型的复杂利用。通过分析 TanStack 事件及随后的社区讨论,我们可以揭示我们在发布和使用开源软件时存在的关键漏洞。
攻击剖析
“Mini Shai-Hulud”蠕虫针对的是开发生命周期的核心。根据报告和 TanStack 的事后分析,攻击者成功劫持了 CI/CD 流水线。该蠕虫通过劫持这些流水线来窃取开发者密钥,然后利用这些被盗的凭据发布合法包的恶意版本。
攻击向量:CI/CD 和 OIDC 令牌
从这次事件中获得的最关键见解之一是“可信发布”(Trusted Publishing)的失效。许多开发者已迁移到可信发布(OIDC 令牌)以避免在 GitHub secrets 中存储长期有效的 NPM 令牌。然而,正如社区成员所指出的,这消除了通常用于限制本地 npm publish 的第二因子认证(2FA)。
如果攻击者获得了 CI 流水线的访问权限或窃取了仓库管理员令牌,他们就可以触发发布事件。由于 OIDC 令牌是由 GitHub Actions runner 自动生成的,攻击者无需物理 2FA 设备即可发布恶意代码。这造成了一个盲点,即信任完全建立在 GitHub 环境内部,使得 CI 流水线成为了高价值目标。
“报复性”有效载荷
这个蠕虫最令人震惊的方面或许是其有效载荷。除了窃取令牌之外,该恶意软件还在受感染的机器上安装了一个“死人开关”(dead-man's switch)。
请在撤销令牌时保持谨慎。看起来有效载荷会在
~/.local/bin/gh-token-monitor.sh安装一个作为 systemd 用户服务 (Linux) / LaunchAgentcom.user.gh-token-monitor(macOS) 的死人开关。它每 60 秒使用被盗令牌轮询api.github.com/user,如果令牌被撤销(HTTP 40x),它将运行rm -rf ~/。
这种报复程度在供应链攻击中非常罕见,它将安全漏洞转化为开发者试图通过撤销被盗凭据来修复情况时可能面临的数据灾难。
生态系统漏洞
TanStack 的沦陷重新引发了关于 NPM 生态系统基本架构的激烈辩论。
生命周期脚本的危险性
该蠕虫利用了 prepare 钩子来执行其有效载荷。这突显了一个反复出现的安全缺陷:生命周期脚本(如 preinstall、postinstall 和 prepare)的默认执行。
批评者认为,在 2026 年启用这些脚本作为默认设置是“纯粹的失职”。由于这些脚本在安装过程中自动运行,单个被劫持的依赖项——甚至是依赖项的依赖项——都可以在无需任何用户显式操作的情况下,在开发者的机器或构建服务器上执行任意代码。
“禁止取消发布”政策
TanStack 的事后分析揭示了恢复过程中的一个令人沮丧的障碍。NPM 的政策规定,如果包已有依赖项,则不允许取消发布(unpublishing)。这意味着即使在检测到入侵后,恶意 tarball 可能仍会存在数小时,直到团队等待 NPM 安全团队手动从服务器端将其移除,这为任何运行 fresh install 的用户留下了漏洞窗口。
如何保护您的项目
社区已提出了一些具体的策略,来降低未来发生类似攻击的风险。
1. 实施依赖项冷却期
避免“零日”恶意发布的有效方法之一是实施最小发布年龄限制。通过将新包版本的安装延迟几天,您可以让社区和安全扫描器在恶意代码进入您的生产环境之前检测并报告它。
根据您的包管理器,配置方式各不相同:
- NPM:
.npmrc$\rightarrow$min-release-age=7(天) - pnpm:
pnpm-workspace.yaml$\rightarrow$minimumReleaseAge: 10080(分钟) - Yarn:
.yarnrc.yml$\rightarrow$npmMinimalAgeGate: 7d - Bun:
bunfig.toml$\rightarrow$minimumReleaseAge = 604800(秒)
2. 禁用生命周期脚本
为了防止在安装过程中自动执行恶意代码,请全局禁用脚本。在您的 .npmrc 中添加:
ignore-scripts=true
3. 加固固化 CI/CD 流水线
为了防止 OIDC 令牌被劫持,请考虑以下架构变更:
隔离:将发布流水线与主开发项目运行在完全隔离的环境中。
分阶段发布:实施一种系统,要求人工通过 2FA 验证才能将构建产物从暂存区提升到最终注册表,从而确保仅靠劫持 CI 流水线不足以发布包。
严格锁定版本:从
package.json中移除使用符号^和~,并锁定到精确版本,以防止自动拉取被劫持的补丁版本。
结论
Mini Shai-Hulud 蠕虫是一个警钟。它表明,当前的 JavaScript 生态系统信任模型——依赖于庞大的传递依赖网和自动化发布——本质上是脆弱的脆弱的。虽然 SLSA 证明(provenance attestation)等工具如 SLSA 证明是向前迈进的一步,但如果生成证明的系统本身被劫持,它们就显得力不从心。真正的安全需要转向显式信任,减少对自动脚本的载荷,减少对自动脚本的依赖,并转向更谨慎的依赖项更新方法。