那份文档点名了一个没有人拥有的包
研究人员扫描了 6,214 个企业域名,寻找 llms.txt,也就是各家公司如今为 AI 智能体发布的那种机器可读文件,并在这些文件里找到 237 个被点名、却从来没有人注册过的包和域名。他们把其中几个注册了下来。某个 Fortune 500 网络内部的第一个编码智能体,在不到四分钟内就装上了其中一个。攻击者没有碰过任何维护者、任何注册表、任何仓库。Bromure Agentic Coding 的年龄门槛默认就开着,设在两天,而攻击者取走的名字,就结构而言必然是一次全新的发布。
一家厂商把某个自己从未发布过的包的安装命令,写进了自己的文档里。那个名字就空在 那儿,谁都能拿走。编码智能体读了文档,相信了它,然后执行了安装。
过去这两年的某个时候,你的开发工具开始随附一个新文件。它就放在 robots.txt 旁
边,叫做 llms.txt,里面装着一份写给机器而不是写给人看的产品纯文本摘要。这个 API
做什么。哪些端点重要。要开始用得先装哪个包。编码智能体会去抓它,而且因为它来自厂
商自己的域名,智能体就把它当成板上钉钉的事实。
一位名叫 Alon Hertz、以 PANDEX 之名工作的研究者,去看了这些文件到底叫智能体做什
么。他的方法只有三步,没有任何漏洞利用:从企业目录拉下 llms.txt 和
llms-full.txt,在沙箱里拿真正的智能体去跑它们、看智能体伸手去拿什么,然后把跑出
来的每一个包名和主机名,拿去 npm、PyPI 以及各域名注册机构比对。他在
whatwouldai.do 上说明了这套工具,而网站上那句标语,最后
成了整件事的结论:智能体会对任何东西动手。
这次扫描覆盖 6,214 个运行中的域名,来自 Fortune 500 企业、大型科技公司、金融科技 和国防承包商,以及超过 8,000 份这类文件。其中有 120 份文件里,那个叫智能体去安装 的东西根本不存在。在整个语料库中,有 237 个被点名的包名和主机名是空的:在 npm、 PyPI、RubyGems、NuGet、crates.io 和 Packagist 上可以随手注册,或在 Vercel、 Render、Fly 和 Netlify 上是空着的子域名。任何人只要付出注册一个账号的代价,就能拿 走它们。
于是研究人员挑了几个,用那些名字发布了无害的包,里面放一个安装时会回家报到的信 标,然后等着。
第一个回调在不到四分钟内就到了。第二个在一小时之内。接下来的一段时间里,他们 记录到来自数十个组织的安装,其中好几个是 Fortune 500 的环境,驱动它们的是跑在 Anthropic 的 Claude、OpenAI 的 Codex 和 Nous Research 的 Hermes 上的智能体。Bruce Schneier 在 9 月 4 日接手了这份研究, 并且用它应得的方式来读:数据与可执行代码之间的界线正在崩塌。那篇帖子引用的一位研 究者用的字更少。信任模型坏了。智能体把厂商文档当成基准事实,而且不会质疑它。
Clerk 为一个从未出货的包写了文档
最清楚的例子来自一家身份验证厂商,它的文档告诉读者去执行:
npx clerk-next-fix-auth-protection
Clerk 从来没有以这个名字发布过独立的包。也许那条命令是个简写,也许它指向某个内部
的东西,也许是写文档的人弄错了。原因不重要,因为对一个智能体来说,那个名字读起来
就是真的。别人把它注册了。安全通告数据库现在把那个包列为 MAL-2026-11069,而它做
的事正是你会为侦察而打造的那种:npm 的安装钩子触发,把执行安装的账号名、机器的主
机名、工作目录和一个时间戳送了出去。
那是某个人构建环境的一张地图,在那个人的网络内部,由那个人自己的编码智能体收集, 而智能体当时正照着那个人自己的厂商文档在走。
为什么平常那套建议会直接指向载荷
每一份供应链指南都叫你拿包名去对照官方文档。这个建议在这里失效,因为那个坏名字正 是从官方文档里来的。
把我们七月写过的问题反过来,你就得到这一 个。在那里,智能体幻想出一个看起来合理的包名,攻击者去注册它惯常会编出来的那些名 字,所以脆弱的那一步是模型的猜测。在这里模型什么都没猜。它跑去找厂商,读厂商说了 什么,照着做,而正是那个一丝不苟的顺序把它送进去的。
注册表被入侵也是另一种形状。上个月当 一条发布流程把某个真实包的十个下毒版本推了出去 时,得先有东西坏掉:一条相信了评论的流程。在这里什么都没坏。一位技术写作者敲了一 条跟已发布产物不完全对得上的命令,这是软件业里最寻常的失误,而名字与产物之间的那 道缝隙,变成了网上任何人都能捡起来用的执行原语。
接住这件事的那一层,已经开着了
Bromure Agentic Coding 让每个编码智能体跑在你 Mac 上一台硬件虚拟化的 Linux VM 里, 而智能体抓取的每一个包,都会先经过那道边界主机这一侧的代理服务器,在任何一个字节 抵达 VM 之前。那里套用了好几层。就这个故事而言,其中一层自己就能把事情办完,而且 那正是一个新工作区已经开着的那一层。
年龄门槛让一个刚被取走的名字变成非事件。 工作区会拒绝比某条界线更新的包版本,
出厂设置是两天。代理服务器通过改写途经的注册表版本清单来执行这件事:它移除比界线
更新的版本,并把 latest 重新指向幸存下来的最新版。今天早上才第一次发布的包,没有
任何一个版本老得足以熬过这道筛子,所以从智能体的角度看,那个名字底下根本没有东西
可装。如果智能体直接指名要一个太新的版本,抓取会以 HTTP 451 回来,正文写着这个包
真实的年龄以及所要求的最小值,而 npm 会原封不动地把那段文字打印出来。
带着这一层再读一次这场攻击。整套手法都倚赖一个名字在攻击者选定的时刻,从未发布变 成已发布。那个转变就是一次全新的发布。攻击者唯一避不开的性质,正是默认策略所盯 住的那一个。
安装脚本剥除,拿掉的正是载荷所用的那个机制。 被观察到的那个包,是在 npm 的
preinstall、install、postinstall 与 prepare 钩子里做事的。开启脚本剥除后,
代理服务器会在传输途中把每个 npm tarball 解压,从 package.json 删掉那四个键,把
dist.integrity 与 dist.shasum 从元数据里抹掉,好让 npm 自己的哈希校验仍然过得
去,然后重新压缩。包装得起来;信标却没有地方可以跑。真的需要钩子的包,例如
better-sqlite3 这类原生绑定的编译器,可以放进每个工作区各自的允许清单,保留它们
的钩子。
信誉过滤认得这个包的名字。 MAL-2026-11069 是一条恶意软件通告。把 socket.dev
选为工作区的包过滤器之后,它的受害包拦截会在任何严重程度的恶意软件与仿冒名称信号
上触发;免费的 OSV 检查则不需要 API 密钥,就覆盖全部八个被拦截的生态系统。当某个
信誉来源给不出判断时,不论是因为速率限制、网络中断,还是因为它不覆盖那个生态系
统,Bromure 都会朝关闭的一侧失败,而不是放行这次抓取。下载会暂停下来问你,逐个
包、逐个版本地问,所以一个有办法诱发查询失败的攻击者,什么也拿不到。
它收集到的侦察信息属于那台 VM。 关于这份研究的每一个标题都在说同一件事:智能 体正在企业网络上安装来历不明的代码。信标的收获是执行安装的用户名、机器的主机 名,以及工作目录。在一个 Bromure 工作区里,这三个字段描述的是你 Mac 上的一台 Linux 客户机,而不是一台加入域、带着值得放进目标清单的内部名称的构建主机。待在那 台 VM 里的凭证都是诱饵,真正的值只存在于主机端代理服务器的内存里。要是敌意代码真 的想把其中一个带去一台它并非为之铸造的主机,代理服务器会挡下那个请求,并让 VM 在 半空中暂停。
默认拒绝回答了这份研究的另外一半。 那些没人认领的名字里,有不少是主机名而不是 包:过期的域名,以及 Vercel、Render、Fly 与 Netlify 上的免费子域名,就坐在智能体会 去抓的文档里。工作区的出站防火墙是一张带有 Unmatched traffic 设置的有序规则 表;把它设成 Deny,VM 就只会对你列出的那些主机开连接,其他一概不通,任何协议 都一样。有两个组件在客户机之外执行这件事:虚拟网络交换机按目的地 IP 地址匹配每一 条流,主机端代理服务器再按 TLS 服务器名匹配一次。一个瞄准某人上周二才注册的域名的 信标,出不了这台机器。规则的修改不需要重启,就会送达正在跑的会话。
每一次尝试都会得到一行记录。 在 Activity only 这个追踪级别上,代理服务器会
为每一个离开 VM 的请求写下一条元数据记录,带着时间戳、主机、端口、方法、路径、状
态、延迟与字节数,而且完全不存储任何请求体。
bromure-cli trace hostnames my-workspace 会把这个工作区联系过的每一个不同主机都
打印出来,并附上次数。你会在事情发生的当天,在自己的机器上,看到一个你手下工程师
没有人认得的回调域名,而不是几个月后在别人的数据集里读到它。
已经开着的东西
一个新工作区出厂时,年龄门槛就是开着的,最小值两天,而这场攻击撞上的正是这个设
置。如果你的工作等得起,把它往上调,在步进器上最高可到 90 天;至于你自己团队当
天发布、当小时就要安装的那些包,用 Exempt packages,写成 npm:our-package
的形式。
今天值得打开的东西
在工作区的 Supply Chain 面板里,开启安装脚本剥除,并挑一个包过滤器。在 Guardrails 里,把 Unmatched traffic 设成 Deny,并列出这份工作需要的 主机。在 Tracing 里,Activity only 会保留联系过的主机轨迹,内容一点都不 留。上面每一项修改,都会应用到已经在跑的会话。
没有人会去校对一份写给机器看的文件
PANDEX 的扫描器还在找另一样东西。除了无人认领的包与域名之外,它也在找那些写给智能
体、而不是写给读者看的指示:埋在文件里的指令,有时还用 Unicode 的把戏与零宽字符藏
起来。厂商的构建流水线生出 llms.txt,你的智能体把它吃下去,而在这两家公司里,都
没有人读过在它们之间传递的是什么。
Bromure 把抓回来的页面当成不可信的输入。智能体从外面拉进来的内容,不论是文件内
容、网页、issue 的正文,还是命令输出,都会以 tool_result 区段的形式抵达模型。开
启源码检测器之后,一个跑在你 Mac 上的 PromptGuard 模型会在模型据此行动之前,于传输
途中为这些区段打分,而你来决定它是记录、询问,还是拦截。没有任何内容会被送到别处
去扫描。
我们花了十年学会验证产物:为包签名、钉住哈希、为构建做证明。这份研究讲的是这一切 之前的那一步,在那里,一个智能体决定要去要哪一个产物,而它依据的是一份没有任何签 名、由一位赶时间的技术写作者写出来的文档。文档已经变成一种可执行的输入,而各个注 册表里满是空着的名字,就等着有人注意到某家厂商提过它们。
拿 grep 扫一遍你自己的 llms.txt,找出那些你从来没有发布过的名字。那是一个上午的
工作量,而且能把你从别人的目标清单上拿下来。然后给你的智能体一台机器,在那上面,
四分钟前才发布的包装不起来,而且你看得见它去过的每一个地方。
安装 Bromure Agentic Coding,把安装这条路径放到主机上。