返回所有文章
發佈於 · 作者 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 rebinding 把它拆了。攻擊者從一個他掌握 DNS 記錄的網域供應那個網頁,存活時間設 得極短。網頁載入之後,再把同一個網域解析到 127.0.0.1。瀏覽器仍然把它當成同一個 來源,同一個主機名、同一個通訊埠,但每一個請求現在都送往你的機器。公開的概念驗證 用的是 singularity,一個已經公開好幾年 的 rebinding 框架。

把這些串起來,一個惡意網頁,或者你信任的網頁上的一則惡意廣告,就會帶著一道 shell 指令對你的 Ray 實例投一個 job。Chrome 因為一個無關的 bug 而躲過;Firefox 與 Safari 沒有。Oligo 的 Avi Lumelsky 提出了 User-Agent 繞過的理論;當時在 Socket 的 Jonathan Leitschuh 做出了 rebinding 這條鏈與整個揭露。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 檢查「瀏覽器不會在這件事上說謊」同源政策「一個主機名就是一個位址」只有第三個是真正的安全控制,而 rebinding 比大多數倚賴它的工具都要老。
rebinding 這條鏈。每一步都是一個維護良好的工具的既有行為:很短的 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:port 形式列出 來,附上握著它的行程名稱。這張卡片會藏起只在 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」