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

那个叫 localhost 的网页

8 月 17 日,CISA 把 Ray 的 CVE-2025-62593 加进了它的“已知被利用漏洞”目录。这是发生在开发者自己机器上的远程代码执行,由他访问过的一个网页送达,够到了一个从头到尾只在 127.0.0.1 上监听的服务。编码代理这一整年都在把那个接口塞满没有认证的服务器。Bromure Agentic Coding 把它们放到浏览器标签页拨不到的地方。

你的开发服务器、你的 notebook、你的本地仪表板,各自把一个没有认证的端口绑在 127.0.0.1 上,理由是只有坐在键盘前的人能够到它。一个网页同样能够到。挡在中间 的那条浏览器策略,大约六十秒就散架了。

8 月 17 日,CISA 加了一个漏洞 进它的“已知被利用漏洞”目录,并给联邦机构留到 8 月 20 日处理完。The Hacker News 第二天早上做了报道。 那一条是 CVE-2025-62593,Ray 里的一个代码注入缺陷——Ray 是业界很大一部分人用来训练 和提供模型的分布式计算框架。

代码跑在开发者自己的笔记本上。有人为了工作在本地开着 Ray,打开了一个网页,然后那 个网页替他执行了一条命令。

三个平常的事实,摆错了位置

安全公告给它 9.4 分,描述的是 一条没有任何花招的链条。

Ray 的 Jobs API 接收一条 shell 命令。那就是产品本身:你带着一个 entrypoint 向 /api/jobs/POST,集群就把那条命令跑起来。仪表板在端口 8265 上监听,而它和那 些 job 端点都不要求认证。那是维护者刻意的决定,他们的立场是 安全与隔离必须在 Ray 集群之外强制执行。 你把它放在只有可信调用方能够到的地方,而笔记本的 loopback 接口听上去就像这样一个 地方。

Ray 确实带了一道防浏览器的门卫。它检查 User-Agent 头是不是以 Mozilla 开头,是 就拒绝,前提是浏览器不能就自己的 User-Agent 撒谎。而在 Firefox 和 Safari 里, Fetch API 允许网页把那个头设成它想要的任何值。

剩下的就是同源策略,那唯一一道站在互联网上的网页和你机器上的服务之间的控制。DNS 重绑定把它拆开了。攻击者从一个自己掌握 DNS 记录的域名提供网页,生存时间设得极短。 网页加载之后,再把同一个域名解析到 127.0.0.1。浏览器仍然把它当作同一个源,同一 个主机名、同一个端口,而每一个请求现在都发往你的机器。公开的概念验证用的是 singularity,一个已经公开多年的重绑定框 架。

把这些串起来,一个恶意网页,或者你信任的网页上的一条恶意广告,就会带着一条 shell 命令向你的 Ray 实例投一个 job。Chrome 因为一个无关的 bug 躲过一劫;Firefox 和 Safari 没有。Oligo 的 Avi Lumelsky 提出了 User-Agent 绕过的设想;当时在 Socket 的 Jonathan Leitschuh 搭出了重绑定链条并完成披露。Ray 2.52.0 修复了它,并加上可选的 token 认证,默认关闭。

一个网页如何变成你笔记本的客户端1 · 你加载网页evil.example → 203.0.113.7TTL:1 秒2 · 记录变了evil.example → 127.0.0.1同源、新的目的地3 · 门卫是一个头fetch(url, headers: UA)Firefox 和 Safari 允许4 · POST /api/jobs/127.0.0.1:8265entrypoint = shell每一层原本靠的是什么Ray“我们周围的网络是可信的”User-Agent 检查“浏览器不会在这件事上撒谎”同源策略“一个主机名就是一个地址”只有第三个是真正的安全控制,而重绑定比大多数倚赖它的工具都要老。
重绑定链条。每一步都是一个维护良好的工具的既有行为:很短的 DNS TTL、一个规范允许网页自行设置的 Fetch 头,以及一个存在目的就是执行命令的 API 端点。

攻击者比公告先到。RondoDox 僵尸网络在公开披露前两天就捡走了这个缺陷,而 Oligo 的 ShadowRay 2.0 研究追踪的是一场把 Ray 部署变成自我传播僵尸网络的行动:被攻陷的 集群扫描更多 Ray 实例并感染它们,一个 cron job 每十五分钟从攻击者控制的仓库拉取新 的载荷,而载荷挖 Monero、开反向 shell、把找到的东西带走。Oligo 数到超过 23 万台暴 露在外的 Ray 服务器,是他们 2024 年第一份报告的十倍。单单一个被攻陷的集群就交出了 240 GB 的源代码、模型和数据集。

你的机器上有什么在监听

把 Ray 放到一边,数点别的:此刻绑在你 loopback 接口上、没有认证的 HTTP 服务器有几 个,而其中你不看就能叫出名字的又有几个。

工作一周之后跑 lsof -iTCP -sTCP:LISTEN -P,那份清单会比你猜的更长。5173 上的 Vite。8000 上跑在 uvicorn --reload 下的 FastAPI 服务。8888 上的 Jupyter、5000 上 的 mlflow ui、一个 Postgres 容器发布出来的端口、3000 上的一个 MCP 服务器、一次崩 掉的测试留着没关的调试端口。它们没有一个问你是谁。它们的作者做了和 Ray 维护者一样 的假设,而那个假设一直成立,直到另一个窗口里的标签页开始发请求。

代理改变了量级。一个在处理工单的代理,把启动服务当成家常便饭。它跑开发服务器来检 查自己的改动,起一个数据库来跑迁移,开 notebook 来看数据,拉起 API 来打一个端点。 它一个下午做好几次,跨好几个工作区,而且不收拾,因为没有任何东西叫它收拾。进程活 得比任务久。到了星期四,那个接口上载着十来个不是你自己启动、也没法凭记忆列出来的 服务,每一个都没有认证就绑上去了,因为框架的快速上手文档说在 localhost 上你是安全 的。

Ray 的立场在一台跑着编码代理的机器上读起来是另一回事:安全与隔离必须在你启动的那 个东西之外强制执行。在一台代理和浏览器共用同一个内核的笔记本上,“之外”没有地方可 坐。

在 Bromure 的工作区里,8265 不在你的 Mac 上

Bromure Agentic Coding 把每个工作区跑在自己的 Ubuntu VM 里,架在 Apple 的 Virtualization 框架上。那就是对这个攻击的完整回答,而且不需要任何人打任何补丁就成 立。

当代理跑 ray start --head,仪表板绑的是 VM 内部的 127.0.0.1:8265。你 Mac 的 loopback 接口是另一个接口。从宿主机上的 Safari 发出的、被重绑定过的 fetch()evil.example 解析成 127.0.0.1,连上 macOS 的 loopback,然后什么都拿不到,因为 那个网页要找的服务隔着一个内核。公开的那条链条瞄准的是 127.0.0.1,是 singularity 重绑定过去的地址,也是公告点名的地址,而在 Bromure 的工作区里,那个 地址上什么也没有。

公告的后半段也一样,就是拿浏览器当中介、去够企业网络内部那些没人放上互联网的 Ray 实例的那部分。工作区默认跑在 NAT 模式,而手册 把这买到了什么讲得很明确:那些 VM 从你的 Mac 够得到,但不会暴露在你的物理 LAN 上, 而且除非你刻意发布一个服务,否则从别处进来的连接不可能成立。发布,指的是你按一个 按钮就开始的、每个服务一条的 Cloudflare quick tunnel。

而且你会拿到那份清册。工作区仪表板的 Listening Ports 卡片在客户机里轮询 ss -tulnpH,把每一个从外部够得到的 socket 以你会拿去连接的 VM-IP:端口 形式列出 来,附上占着它的进程名。这张卡片会藏起只在 loopback 上的 socket,因为 VM 之外没有 东西够得到它们。同一份清单也能从 bromure-cli vm <id> -L 出来。那份清单就是代理留 下来的东西的普查,而 macOS 不会替你留这样一份清单。

传统做法:一个 loopback,所有东西共用你的 Mac · 127.0.0.1:8265 ray · :5173 vite · :8888 jupyter · :3000 mcp:8000 uvicorn · :5000 mlflow · :5432 postgres由代理启动、活得比任务久、全都没有认证另一个窗口里的标签页重绑定到 127.0.0.1然后全都够得到BROMURE:代理的 loopback 不是你的工作区 VM · 自己的 127.0.0.1:8265 ray · :5173 vite · :8888 jupyterNAT:不在你的物理 LAN 上够得到的 socket 列在仪表板上你的 Mac · 127.0.0.1(没有代理启动的东西)漏洞利用拨打的那个地址同一个重绑的标签页连上后找到的是一个空的接口
同一条命令,两种摆法。传统做法里,代理启动的一切都加进那堆网页够得到的东西。在 Bromure 里,代理的服务住在一道网络边界后面,而够得到的东西是一份你读得到的清单。

第一步之后的那一跳

ShadowRay 2.0 值得读,是因为它写了被攻陷的节点接下来做什么,而那正是隔离必须交代 的部分。它扫描其他 Ray 实例并感染它们。它装一个每十五分钟去取新指令的 cron job。它 伸手去拿机器上任何凭据和数据集。

那些每一件都是一条出站连接,而 Bromure 的工作区会拿你写的规则去比对出站连接。 Guardrails 面板里的 Outbound connections 编辑器装着一份 pf 风格的规则集:允许或 拒绝,按主机、IP 或 CIDR、协议与端口,自上而下比对,第一个命中的获胜,外加一个针对 什么都没命中的流量的设置。

allow web  api.github.com:443
allow tcp  registry.npmjs.org:443
deny  any  10.0.0.0/8
deny  any  192.168.0.0/16
default deny

有两层在执行同一份规则集。虚拟交换机用目的 IP、以及它从偷看客户机自己的 DNS 应答学 到的主机名,来评估每一条流;所以你针对一个名字写的规则,在地址从底下换掉之后仍然 命中。MITM 代理用 TLS SNI 评估同一批规则,对 web 规则还会看 HTTP 方法,所以你可以 写 allow web api.example.com GET,HEAD。被拒绝的 TCP 连接 Bromure 会注入一个 reset,所以 connect 是失败,而不是卡住。

这些执行全都跑在你的 Mac 上,在 VM 之外。透明拦截默认开启,就算里面有东西把 HTTP_PROXYHTTPS_PROXY 抹掉,客户机的 HTTP 与 HTTPS 一样会被转到 Bromure。一 个读了错误网页的代理,或者搭着某个包混进来的载荷,坐在它想改的那些规则的错误一侧。 Bromure 把每一个决定写进 Window → Security Timeline,成为一行 Firewall:主机、 端口、允许或拦截,和凭据、供应链、提示词注入的决定并列在同一张按时间排列的表里。

那个十五分钟的 cron,在另一个方向上有同样的问题。一个工作区是三层存储,其中两层是 你想扔就扔的:Erase home…/home/ubuntu 还原到刚克隆完的状态,Reset to base… 从基础镜像重新克隆工作区的系统盘,而基础镜像在运行时保持只读。家目录里的 crontab 两者都活不过。

localhost 是一种约定

Ray 的维护者并不粗心。他们写了一个会执行命令的计算框架,在文档里说了,也告诉你要在 它周围放一道边界。在笔记本上跑它的人,把 loopback 接口当成了那道边界,而那正是开发 服务器、notebook 和本地仪表板二十年来一直在邀请大家得出的结论。那是约定,不是控制。 在你机器上的东西只有你自己启动的东西时,它成立;等浏览器有了脚本引擎,它就不再成立 了。

代理每个下午都把那道缝再撑大一点,一次一个 npm run dev。小心关不上它,因为小心要 求你知道有什么在监听,而那份清单在代理每跑一条命令时就变一次。

别再让这两者共用一个接口。把代理的工作放进它自己的机器,让它开的端口只回应你的 Mac、不回应别的东西,让够得到的 socket 是一份你读的清单、而不是一个你继承来的假设, 让出去的连接被拿去比对工作区自己改不了的规则。那时,一个重绑定到 127.0.0.1 的网页 会找到一个空的接口,然后吃一记拒绝。


来源:CISA,“CISA Adds One Known Exploited Vulnerability to Catalog”(2026 年 8 月 17 日) · The Hacker News,“CISA Flags Actively Exploited Ray Flaw That Can Trigger Browser-Based RCE”(2026 年 8 月 18 日) · GitHub 安全公告 GHSA-q279-jhrf-cc6v(CVE-2025-62593) · Oligo Security,“ShadowRay 2.0”