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

那份文档点名了一个没有人拥有的包

研究人员扫描了 6,214 个企业域名,寻找 llms.txt,也就是各家公司如今为 AI 智能体发布的那种机器可读文件,并在这些文件里找到 237 个被点名、却从来没有人注册过的包和域名。他们把其中几个注册了下来。某个 Fortune 500 网络内部的第一个编码智能体,在不到四分钟内就装上了其中一个。攻击者没有碰过任何维护者、任何注册表、任何仓库。Bromure Agentic Coding 的年龄门槛默认就开着,设在两天,而攻击者取走的名字,就结构而言必然是一次全新的发布。

一家厂商把某个自己从未发布过的包的安装命令,写进了自己的文档里。那个名字就空在 那儿,谁都能拿走。编码智能体读了文档,相信了它,然后执行了安装。

过去这两年的某个时候,你的开发工具开始随附一个新文件。它就放在 robots.txt 旁 边,叫做 llms.txt,里面装着一份写给机器而不是写给人看的产品纯文本摘要。这个 API 做什么。哪些端点重要。要开始用得先装哪个包。编码智能体会去抓它,而且因为它来自厂 商自己的域名,智能体就把它当成板上钉钉的事实。

一位名叫 Alon Hertz、以 PANDEX 之名工作的研究者,去看了这些文件到底叫智能体做什 么。他的方法只有三步,没有任何漏洞利用:从企业目录拉下 llms.txtllms-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 的安装钩子触发,把执行安装的账号名、机器的主 机名、工作目录和一个时间戳送了出去。

那是某个人构建环境的一张地图,在那个人的网络内部,由那个人自己的编码智能体收集, 而智能体当时正照着那个人自己的厂商文档在走。

五个寻常步骤,链条上没有任何入侵1 · 厂商发布vendor.com/llms.txt写给智能体看,不是给人看npx clerk-next-fix-auth-protection2 · 名字是空的从来没有包以这个名字发布过120 个文件里有 237 个3 · 有人把它注册了一次普通的 npm publishMAL-2026-11069载荷藏在 install 钩子里4 · 智能体读了文档,并且相信它Claude、Codex 与 Hermes 都被观察到执行了安装命令来自厂商自己的域名,没有什么好怀疑的没有凭证被窃 · 没有仓库被入侵 · 没有维护者被钓鱼5 · 钩子回家报到用户名 · 主机名工作目录 · 时间戳一张构建环境的地图攻击者唯一真正做的事,是读了一份受害者的厂商刻意发布出来的文件。
这条链里没有任何入侵。厂商发布一份点名了安装命令的机器可读文档;那个包从来没有发布过,所以名字在注册表上一直空着;攻击者把它注册下来;智能体把文档当成基准事实抓下来并执行安装;安装钩子把环境信息回报出去。每一环都是一次正确执行的正常操作。

为什么平常那套建议会直接指向载荷

每一份供应链指南都叫你拿包名去对照官方文档。这个建议在这里失效,因为那个坏名字正 是从官方文档里来的。

把我们七月写过的问题反过来,你就得到这一 个。在那里,智能体幻想出一个看起来合理的包名,攻击者去注册它惯常会编出来的那些名 字,所以脆弱的那一步是模型的猜测。在这里模型什么都没猜。它跑去找厂商,读厂商说了 什么,照着做,而正是那个一丝不苟的顺序把它送进去的。

注册表被入侵也是另一种形状。上个月当 一条发布流程把某个真实包的十个下毒版本推了出去 时,得先有东西坏掉:一条相信了评论的流程。在这里什么都没坏。一位技术写作者敲了一 条跟已发布产物不完全对得上的命令,这是软件业里最寻常的失误,而名字与产物之间的那 道缝隙,变成了网上任何人都能捡起来用的执行原语。

扫描发现了什么,以及证据回来得多快6,214个运行中的域名Fortune 500、科技、国防8,000+份给智能体看的文档llms.txt · llms-full.txt120个文件指向从未注册的东西237个空名,六个注册表外加 Vercel/Render/Fly然后他们注册了一批样本,等着来自 Fortune 500 环境的第一个安装回调不到 4 分钟第二个回调一小时之内 · 整个研究期间数十个组织
这份研究的漏斗。在 6,214 个带有面向智能体文档的企业域名中,有 120 份文件点名了未注册的东西,加起来是 237 个空名。把其中一批注册下来之后,第一个来自 Fortune 500 网络内部的安装回调,在不到四分钟内就出现了。

接住这件事的那一层,已经开着了

Bromure Agentic Coding 让每个编码智能体跑在你 Mac 上一台硬件虚拟化的 Linux VM 里, 而智能体抓取的每一个包,都会先经过那道边界主机这一侧的代理服务器,在任何一个字节 抵达 VM 之前。那里套用了好几层。就这个故事而言,其中一层自己就能把事情办完,而且 那正是一个新工作区已经开着的那一层。

年龄门槛让一个刚被取走的名字变成非事件。 工作区会拒绝比某条界线更新的包版本, 出厂设置是两天。代理服务器通过改写途经的注册表版本清单来执行这件事:它移除比界线 更新的版本,并把 latest 重新指向幸存下来的最新版。今天早上才第一次发布的包,没有 任何一个版本老得足以熬过这道筛子,所以从智能体的角度看,那个名字底下根本没有东西 可装。如果智能体直接指名要一个太新的版本,抓取会以 HTTP 451 回来,正文写着这个包 真实的年龄以及所要求的最小值,而 npm 会原封不动地把那段文字打印出来。

带着这一层再读一次这场攻击。整套手法都倚赖一个名字在攻击者选定的时刻,从未发布变 成已发布。那个转变就是一次全新的发布。攻击者唯一避不开的性质,正是默认策略所盯 住的那一个。

安装脚本剥除,拿掉的正是载荷所用的那个机制。 被观察到的那个包,是在 npm 的 preinstallinstallpostinstallprepare 钩子里做事的。开启脚本剥除后, 代理服务器会在传输途中把每个 npm tarball 解压,从 package.json 删掉那四个键,把 dist.integritydist.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 会把这个工作区联系过的每一个不同主机都 打印出来,并附上次数。你会在事情发生的当天,在自己的机器上,看到一个你手下工程师 没有人认得的回调域名,而不是几个月后在别人的数据集里读到它。

开发者机器上的一个智能体npx clerk-next-fix-auth-protection直接来自厂商自己的文档接下来发生的事,全都在公司网络里注册表给出已发布的东西,不管多新postinstall 以开发者本人的身份运行用户名、主机名与 cwd 都是公司事实回调想去哪里就去哪里没有针对目的地的逐请求记录安装成功了,看起来就跟智能体那天下午跑过的其他每一次安装一模一样第一个回调:不到四分钟。你要等到陌生人发表论文才会知道。一个 Bromure Agentic Coding 工作区npx clerk-next-fix-auth-protection同样的命令,同样的文档接下来发生的事,全都在主机上年龄门槛:不到两天的版本被剔除今天被取走的名字没有可安装的东西剥除:四个 install 钩子被移除用户名、主机名与 cwd 描述一台 Linux VM未匹配的出站流量按目的地 IP 拒绝拒绝是智能体读得懂的 451,还有一行写进 Security Log,附上包的真实年龄第一个回调:没有。你从自己的机器上,当天就知道。
左边:安装在公司网络内部的一台开发者机器上执行,直接向注册表查询,安装钩子带着开发者的身份执行,而回调离开时没有留下任何记录。右边:同样一条命令,走过一台主机端的代理服务器,它把刚发布的版本从元数据里滤掉,若真走到那一步就剥掉钩子,拒绝未匹配的出站流量,并为每一个请求写下一行记录。

已经开着的东西

一个新工作区出厂时,年龄门槛就是开着的,最小值两天,而这场攻击撞上的正是这个设 置。如果你的工作等得起,把它往上调,在步进器上最高可到 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,把安装这条路径放到主机上。