没有人发布新版本
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 是从它本来就该来的存储桶、原封不动地送达站点的,而这正是它好用的地方。
事件从一组能写入的凭据开始
从厂商那一侧读这份通报,攻击链就缩成一步。有人取得了一个存储桶的写入权限。之后的一切都是后果。
上游遭入侵的事件,一直以这个形状出现。 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
上的其他一切都没有挂载、无法触及、也无法枚举。凭据集合就是你加进那个配置的那些,所以一个你拿来写前端的配置里,根本没有任何集群凭据可供寻找,不管藏得多好都一样。
运行期的抓取也一样穿过代理
现在换到这个故事的下游那一侧,也就是你是那 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 没有写入权的日志里。
一份被投毒的数据,说到底也只是抵达一台机器的文本。当它以 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,给智能体一台对你发布的任何东西都不握有密钥的机器。