返回所有文章
发布于 · 作者 Renaud Deraison

没有人发布新版本

Wordfence 8 月 9 日关于 BdThemes 遭入侵的通报,描述的是一场没有供应的供应链攻击。没有发布、没有安装脚本、没有抢注名称、插件仓库里也没有任何文件被改动。攻击者拿到了厂商存储桶的写入权限,改掉了一份 35 万个站点在每次加载后台页面时都会抓取的 JSON 数据。对开发者而言,这件事真正提出的问题是:跑着你的编码智能体的那台机器,被允许写入什么,又写给谁。

你手上的每一项供应链管控,检查的都是被发布出来的东西。这一次什么都没发布,却照样触及了 35 万个站点。

2026 年 8 月 7 日,Wordfence 开始接到通报:一些装了 BdThemes 插件的 WordPress 站点,长出了没有人创建过的管理员账号。名单一共七个插件:Element Pack Addons for Elementor、Prime Slider、Pixel Gallery、Ultimate Post Kit、Ultimate Store Kit、Live Copy Paste 和 Smart Admin Assistant。光是 Element Pack 就有超过 10 万次有效安装,七个加起来超过 35 万。

到了 8 月 8 日,厂商已清理自家 API,WordPress.org 则在调查期间下架了全部七个插件。Wordfence 第二天发出 关于这次入侵的 PSA,而它最核心的发现是一个否定句:没有人动过插件的代码。

什么都没有被发布

BdThemes 的插件里带着一个叫 Biggopti 的内部组件,活儿既小又无聊:从厂商的远程 API 抓取推广横幅,画在 WordPress 的后台仪表盘上。横幅数据是一个放在 DigitalOcean Spaces 存储桶里的静态 JSON 文件,由 Cloudflare 后面提供。

攻击者拿到了那个存储桶的写入权限。整场入侵就只有这些。

Biggopti 把 JSON 响应里的 display_id 字段取出来,未经转义就丢进一个 HTML 的 id 属性,这是一个 Wordfence 评为 CVSS 5.4、中等 的跨站脚本漏洞,源头可追到 2026 年 3 月 1 日:某位开发者写了一次,然后从一个插件复制到下一个。于是,一个带着 onanimationstart 处理器的 display_id 就是 JavaScript,而它会在每一位已登录管理员的浏览器里、在每一次 wp-admin 页面加载时、在每一个装了该插件的站点上执行。

载荷把这个座位用得很好。一个叫 w2.js 的脚本向命令与控制服务器,也就是攻击者用来告诉载荷哪些站点值得下手的那台机器,索取锁定目标的指令,接着骑在真正管理员自己的会话和 nonce 上,通过 WordPress 的 REST API 创建隐藏的管理员账号。有一个变种会从站点的主机名推导出可预测的用户名。一个假插件,常见名字像是 wp-smart-thumbnails,在 emer-run.php 送上一个 web shell,也就是攻击者只要加载一个普通网址就能操控的脚本。而 must-use plugins,也就是 WordPress 每次请求都会加载、又不会显示在插件页面上的那些,带着一道魔术登录后门,以及一个通过改写后台显示的用户总数来掩盖新账号的模块。

被投毒记录上的时间戳,把最早可能的起点放在 2026 年 6 月 23 日,比任何人察觉早了七周。

把这件事对照一个严谨团队会跑的那些管控。版本锁定:版本从未改变。锁文件与完整性哈希:它们吻合,因为它们覆盖的文件根本没被动过。安装脚本审查:根本没有安装。抢注名称检测:名字对、厂商对、仓库条目也对。版本之间差异的代码审查:没有差异,因为没有版本。证书与 CDN:两者都健康得很。Cloudflare 和 TLS 证明了那份 JSON 是从它本来就该来的存储桶、原封不动地送达站点的,而这正是它好用的地方。

所有人都盯着的那条路WordPress.org七个插件,35 万+次有效安装管控检查的东西版本、哈希、安装脚本、两次发布之间的差异判定:干净而且判得没错。仓库里没有任何一个文件曾被修改过。没有人盯着的那条路厂商的存储桶DigitalOcean Spaces,在 Cloudflare 后面有人能往里面写Biggopti 抓取横幅display_id 进了 HTML 的id 属性,未经转义onanimationstart 执行它在管理员本人的会话里用 REST 建隐藏管理员账号、emer-run.php 的 web shell、mu-plugins 里的 magic-login最早的被投毒记录:2026 年 6 月 23 日Wordfence 接到通报:2026 年 8 月 7 日在这七周里,对已发布产物的每一次检查都通过了,而且通过得没有错。
通往一个装了该插件的站点的两条路。发布的那条路,也就是每一项供应链管控都会检查的那条,从未改变。运行期的那条路从一个别人可以写入的存储桶抓下一份 JSON 文件,而那份文件是以 JavaScript 的样子抵达管理员浏览器的。

事件从一组能写入的凭据开始

从厂商那一侧读这份通报,攻击链就缩成一步。有人取得了一个存储桶的写入权限。之后的一切都是后果。

上游遭入侵的事件,一直以这个形状出现。 Red Hat 的 npm scope 蠕虫、 真的属于 Microsoft 的那个仓库、扩展市场、容器镜像仓库:在每一件里,被写成报道的是载荷,而真正要紧的,是有人手上多了一组能覆盖别人按时抓取之产物的凭据。

这样一组凭据住在哪里?住在开发者的环境里。shell 里的 DIGITALOCEAN_ACCESS_TOKEN~/.config 里的 doctl 配置、~/.docker/config.json 里的镜像仓库登录、一份 AWS 配置、~/.git-credentials 里的 GitHub 令牌、一把能 push 的 SSH 密钥。在笔记本上、在终端里、在项目旁边。而在 2026 年,项目旁边也正是你跑编码智能体的地方。

这个博客的多数文章,讲的都是把那个智能体反过来对付你的方法。一份 执行了命令的缺陷报告。一个 改写了智能体配置的网页。一条 没有人看得见的评论。一项 用你的身份执行的技能一场 Black Hat 演讲里的三种框架绕过。把整份清单当成已知,然后假设某个星期二,其中一种落到了你头上。

那个星期二会有多糟,取决于一个你今天就能回答、而且不必知道哪一种注入会得手的问题:这台机器被允许写入什么,又写给谁?在笔记本上,答案是所有你被允许写入的东西,而只要你有在发布任何东西,那就包括好几十万个陌生人不看一眼就抓下来的那份产物。

令牌不在那台机器里

Bromure Agentic Coding 在 Apple Silicon 上的一台一次性 Linux VM 里跑智能体,而它流量的每一个字节都会穿过主机上的一层代理, 在智能体运行的那个盒子之外。Bromure 把凭据安排在那道边界的外围,而不是放进里面。

把你的 DigitalOcean 个人访问令牌放进 Credentials 面板,Bromure 会把真值留在 Mac 上。落进 VM 的是个假的:一个 DIGITALOCEAN_ACCESS_TOKEN 环境变量,以及一份让 doctl 不必 doctl auth init 就能工作的 ~/.config/doctl/config.yaml,里面装的是占位值。当 VM 对 api.digitalocean.com 发出请求时,主机上的代理会在链路上换成真的令牌,只针对那个目的地,别的一概不换。

在那台 VM 里运行的任何东西都可以读环境变量、grep 点文件、走遍整块磁盘:智能体本身、一个依赖包、一个构建步骤、一行从被投毒的 issue 里跑出来的 shell 命令。它们收集到的是 brm_…。同一套安排也覆盖其余的部分:GitHub、GitLab 和 Bitbucket 的令牌,以假 base64 blob 形式写在 ~/.docker/config.json 里的容器镜像仓库登录,一份带着一次性客户端证书的合成 kubeconfig,Linear,各家数据库的 HTTPS 端点,以及你在 Other API keys 底下另外加的任何东西。AWS 还更进一步:主机会用真正的密钥以 SigV4 重新签名每一个请求,所以绕过代理换来的是一个 InvalidSignatureException,而不是一次未经授权的写入。

动词由 Mac 上的 Guardrails 决定

这起事件只需要一个操作:覆盖存储桶里的一个对象。Guardrails 是主机代理内部的一套策略引擎,每种资源都有自己的模式:Off、Block destructive 或 Read-only。把 DigitalOcean 设成 Read-only,每一次对 api.digitalocean.com 的变更都会换回一个硬邦邦的 403,而智能体会把它当成一次普通的 API 失败报告出来。Bromure 是在 macOS 里做这个决定的。VM 里没有设置可以改、没有环境变量可以取消、也没有文件可以编辑。

要发布,先问过

每一条凭据都有 Require approval to use。把它打开,每一次假值→真值的替换,都会在真值上链路之前于主机端弹出一个同意对话框,而 SSH 那条路还有带时限的授权:五分钟、一小时、或这次会话的剩余时间。推一次发布是你本来就打算做的事,代价是点一下。而一次你没有发起过的写入,会在你没有预期的时刻,变成一个你没有预期的对话框。

镜像仓库和 git 也是同样待遇

Guardrails 会针对镜像仓库自己的主机名,用方法来给容器镜像仓库的流量分类:GETHEAD 是 pull,PUTPOST 是 push,DELETE 是破坏性的。GitHub、GitLab 和 Bitbucket 在 REST API 和走 HTTPS 的 git 上都被覆盖,在那里 git-receive-pack 算作写入,在 Read-only 模式下会被拦掉,而 fetch 永远放行。一个读得到你整张依赖图、却什么都推不出去的配置,只是两个开关。

环境是一份你自己写的清单

一个 Bromure 配置看得到的,是你分享给它的那些 Mac 文件夹,最多八个,各自挂载在 /home/ubuntu 底下。你 Mac 上的其他一切都没有挂载、无法触及、也无法枚举。凭据集合就是你加进那个配置的那些,所以一个你拿来写前端的配置里,根本没有任何集群凭据可供寻找,不管藏得多好都一样。

在开发者的笔记本上DIGITALOCEAN_ACCESS_TOKEN=dop_v1_…~/.config/doctl, ~/.docker/config.json,~/.git-credentials, ~/.aws, ~/.ssh智能体被策反,一次就够一个 issue、一个网页、一份 README、一行日志现在它读得到你读得到的东西往存储桶发一个 PUT你的用户自动抓取的那份产物从此就是他们的了在一个 Bromure 配置里DIGITALOCEAN_ACCESS_TOKEN=brm_…每一个点文件都在,而它们里面的每一个秘密都是占位值真值住在 Mac 上由主机代理在链路上替换,只给 api.digitalocean.com,别处都没有Guardrails:Read-only → 403写入的准驳在 macOS 里裁决,而Require approval 会先挡一道对话框
同一组写入凭据,放在两个地方。在笔记本上,它是真文件里的一个真令牌,离你的用户会抓取的那份产物只差一个被策反的智能体。在 Bromure 配置里,真令牌从不进入 VM,而它想执行的那个操作,由主机来裁决。

运行期的抓取也一样穿过代理

现在换到这个故事的下游那一侧,也就是你是那 35 万之一的那一侧。你在那里能抓到什么,取决于你把检查放在哪里。

Bromure 的 Supply Chain 面板会筛查跨 npm、PyPI、Cargo、RubyGems、Maven、NuGet、Go 模块和 Packagist 的包抓取:一道默认开启、会拒绝最近两天内发布之物的时效门槛,OSV 查询,把 socket.dev 或 Delpi 当作过滤提供方,会改写 tarball 并修正仓库元数据哈希好让 npm 依然验得过的安装脚本剥除,以及在被锁文件钉住的 tarball 原封不动通过之前的一道询问。VM 里的 .npmrcpip.conf 可以把这些规则收紧,但不能放宽。

这些规则之所以值回票价,在于 Bromure 执行它们的位置。主机上的代理是把规则套在请求上,而不是套在包上,所以一个构建步骤、一个 CLI、一台 MCP 服务器或应用程序本身在运行期从厂商存储桶拉下的 JSON 文件,穿过的是跟 tarball 同一道边界。VM 对网络说的每一句话,都会穿过它。

这会改变那七周的样子。把 Session trace 设在 Activity,Bromure 就会为 VM 发出的每一个请求记下主机、状态、延迟、替换报告和任何泄漏警告。设成 Everything,它连响应体也一起留下,覆盖每一个主机,用与你配置里秘密相同的钥匙串密钥加密,并可在 Trace Inspector 里读。你的构建所依赖的某份数据开始给出不一样答案的那一天,就是一份你能打开来看的差异,在 Mac 上,在一份 VM 没有写入权的日志里。

在包管理器里做的检查看得到:tarball、版本、锁文件只在你安装东西的时候才跑GET banner.json → 厂商的存储桶不是安装,所以也不算一起事件七周,没有留下任何记录在主机代理里做的检查看得到:VM 发出的每一个请求时效门槛、OSV、过滤、脚本剥除GET banner.json → 厂商的存储桶主机、状态、延迟、替换报告在 Everything 级别下还有响应体
检查发生在哪里,决定了它能看见什么。包管理器里的管控只检查被发布出来的东西;主机代理里的管控检查 VM 发出的每一个请求,包括带着这次载荷的那次运行期抓取。

一份被投毒的数据,说到底也只是抵达一台机器的文本。当它以 tool_result 的形式、以文件的形式、以一个抓下来的网页的形式,或以 CLAUDE.mdAGENTS.md 这类指令文件的形式抵达智能体时,Prompt Injection 面板会用一个本地模型在设备上为它评分,而且什么都不会离开 Mac。选 Ask me what to do,请求会暂停并显示被标记的那一段;选 Block unilaterally,智能体会拿到一个硬邦邦的 451。当你想让这台机器彻底消失,Erase home 会重置 /home/ubuntu,而 Reset to base 会从基础镜像重新克隆一份工作区的系统盘。

BdThemes 会把转义修好,存储桶会换上更严的密钥,而这个故事的下一个版本,会带着另一家厂商和另一份数据出现。留得住的是底下那套安排。你的开发环境握着一把凭据,每一组都能改写某个成千上万人按时抓取的东西,而同一个环境如今还跑着一个整天在读陌生人文本的智能体。

这里的多数文章问的是:你的智能体应该被允许读什么。也请回答另一个问题,那个在笔记本上让人不安、在配置里却很容易的问题:它被允许写入什么,又写给多少人?

安装 Bromure Agentic Coding,给智能体一台对你发布的任何东西都不握有密钥的机器。