← 返回所有文章
发布于 · 作者 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 会针对镜像仓库自己的主机名,用方法来给容器镜像仓库的流量分类:GET 和 HEAD 是 pull,PUT 和 POST 是 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 里的 .npmrc 和 pip.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.md、AGENTS.md 这类指令文件的形式抵达智能体时,Prompt Injection 面板会用一个本地模型在设备上为它评分,而且什么都不会离开 Mac。选 Ask me what to do,请求会暂停并显示被标记的那一段;选 Block unilaterally,智能体会拿到一个硬邦邦的 451。当你想让这台机器彻底消失,Erase home 会重置 /home/ubuntu,而 Reset to base 会从基础镜像重新克隆一份工作区的系统盘。

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

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

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