5月11日,一个蠕虫在npm上发布了84个恶意版本,覆盖42个TanStack包。这些版本带着有效的来源证明,从TanStack自己的发布流水线里推出去的。两个半小时后,一个Dependabot的合并请求把其中两个版本拉进了一个小型航空数据项目,一次合并就把维护者的发布令牌变成了另外110个恶意版本,用时95分钟。

整个过程没有钓鱼密码,没有一步需要人参与,除了那一次点击。

打开网易新闻 查看精彩图片

上游:一个fork的PR埋下了毒缓存

攻击从UTC上午开始。一个改名的TanStack/router fork在10:49打开了PR #7378,标题是"WIP: simplify history build"。

TanStack的bundle-size.yml工作流跑在pull_request_target上,这意味着它在基础仓库的上下文里执行。它检出了fork的合并引用并构建,攻击者的代码就这样跑了起来。11:29,那段代码把一个1.1 GB的pnpm-store缓存存到了发布工作流之后会查找的那个确切键名下。11:31,PR被强制推空、关闭,分支删除。

八小时后,一次正常的合并触发了release.yml。它恢复了那个被污染的缓存。攻击者的二进制文件读取了Runner.Worker进程的/proc//mem,取出了为id-token: write铸造的OIDC令牌,直接把包发到了注册表。工作流自己的发布步骤因为测试失败被跳过了,但恶意软件照发不误。

TanStack自己没发现。一位StepSecurity的研究员在19:46打开了TanStack/router#7383,标题是"Several npm latest releases were compromised",距离第一次发布大约26分钟。21:19,@tan_stack发布了公告。

下游:Dependabot的例行升级成了传播链

21:48,公告发出29分钟后,Dependabot打开了squawk PR #246,"Bump the dev-dependencies group with 13 updates"。13个更新里有两个是@tanstack/router-cli 1.166.40 → 1.166.49和@tanstack/router-plugin 1.167.32 → 1.167.41。两个都被污染了。

Dependabot没有自动合并。它只是提议,由人来审查并在22:12合并,距离PR打开24分钟。事件报告用一句话概括了剩下的部分:发布工作流在NPM_TOKEN在作用域内的情况下跑了npm ci,恶意的prepare脚本外泄了令牌,并用它发布了令牌能访问的每个包的5个恶意版本。

那个令牌是"一个单一且权限过宽的经典npm令牌"。它触及了全部22个@squawk/*包和三个无关的个人包。第一个恶意版本@​squawk/mcp@0.9.1在22:17发出,最后一个@​squawk/mcp@0.9.5在23:52发出。95分钟,110个版本,每个包的latest都指向蠕虫。维护者是从npm的通知邮件里知道的,时间是00:04。

值得注意的是21:03和22:12这两个时间点:下游合并发生时,每一个坏的TanStack版本都已经被弃用了。弃用只是一个警告,npm仍然会安装它。

蠕虫只想要一样东西

StepSecurity反混淆了载荷,一个2.3 MB的混淆JavaScript文件。

它在安装时运行。被污染的TanStack版本添加了一个隐藏的optionalDependency,指向一个孤立的git提交。npm把git依赖当作tarball获取,并在安装期间运行它的prepare脚本。一个分组升级的diff把它藏了起来:它只说"router-cli 1.166.40 → 1.166.49",别的什么都没有。

它想要一样东西:一个不需要第二因素的发布令牌。第一步是搜索带bypass_2fa: true的经典npm令牌。在CI里,它用GitHub OIDC令牌换取每个包的发布令牌。

拿到发布凭证后,它向npm的搜索查询维护者控制的所有包,然后为每一个发布一个被感染的tarball。发布是每个版本一个HTTP请求,没有人工步骤,没有冷却时间。这就是为什么95分钟足够发出110个版本。

它还伪装成Dependabot。它的死信投递提交使用一个伪造的作者名"claude"(不是Anthropic),提交信息是chore: update dependencies,分支名像dependabot/github_actions/format/fremen,然后是sandworm、harkonnen、atreides。StepSecurity说它"模仿了Dependabot的分支命名惯例"。一个机器人提议了版本,一条流水线安装了它,蠕虫回报的方式是穿上机器人的制服。

为什么有效的来源证明没有帮上忙

这些包带着有效的SLSA来源证明。它们确实是从TanStack的发布流水线里出来的,用的是真实的凭证。来源证明能证明"这个包来自它声称的地方",但证明不了"那个地方没有被攻破"。

厂商统计整个生态里超过160个包受影响,包括Mistral的。这个蠕虫被称为Mini Shai-Hulud。

应对是好的。两位维护者都在一天内发布了带时间戳的事后复盘,下游流水线在三天内重建。