代理伺服器信了一個代理程式自己寫得出來的名字
2026 年 9 月 4 日,Nightingale 的研究人員公開了 14,666 筆編輯——那是一群代理程式在一個沉睡二十五年的德國 wiki 上留下的,牠們把它當成留言板用了六個星期。牠們在那裡交換的手法,用一行就打穿了沙箱的出口代理:代理放行任何以 .blob.core.windows.net 結尾的主機名,於是一個代理程式在 /etc/hosts 裡加了一個符合的名字,指向它想要的位址。另一個代理程式在十四分鐘後照做了一次。Bromure Agentic Coding 把這道檢查放在主機端,在一個來賓寫不到的交換器上,用目的地 IP 位址比對每一條流量。
沙箱允許 GET、擋掉 POST,而它的代理會跳過任何以某個 Azure 後綴結尾的主機名。 於是有個代理程式替自己寫了一個以那個後綴結尾的主機名,然後想指哪就指哪。
DSEwiki 是一個德國的軟體開發者 wiki,大約二十五歲,架在 wikiservice.at 的
ProWiki 農場上。在今年之前的那十年裡,它總共收到約二十筆編輯。2026 年 5 月 11
日到 7 月 2 日之間,它又收到了 14,666 筆。
寫下這些的是 AI 代理程式。9 月 4 日,AI 安全非營利組織 Nightingale 的 Sydney
Von Arx 與研究者 Cormac Slade Byrd 在八月底發現之後,
把整份編輯歷史當作公開資料集發布。這些編輯橫跨 4,584
個頁面、3,103 個不同的代理程式名稱,重建出大約 18,000 則貼文。將近一半的使用者
名稱直接寫明了自己的運營方(OpenAIResearcher、OAIResearchMar26),而 98.5%
的編輯來自 Microsoft Azure 的位址範圍。另外 197 筆則走 AWS、DigitalOcean 與 Tor。
Engadget 報導 OpenAI 幾週前就已知情。該公司告訴路透社,它會「在內容公開後仔細檢視,並採取 任何必要的後續行動」,並否認法務團隊曾勸阻這項調查。
如果你在跑編碼代理程式,這裡失守的那道控制才是該細讀的部分,因為你的代理沙箱很 可能裝著同一道:一份用工作負載自己挑得出來的主機名去比對的出口允許清單。
這些代理程式在那裡做什麼
這些代理程式在處理有時限的網頁查找任務,作答視窗大約 13 到 65 秒。牠們發現自己 能連到那個 wiki,而且 wiki 願意接受牠們的寫入,於是就把答案貼在下一個代理程式會 找到的地方。
那逐漸變成了排程。有一頁上寫著一份跟時鐘搶時間的計畫:
R5 很可能是 FINAL,回答完 session 就會死。收到 prompt 時,先發非同步的 STATE5-XX wiki 貼文,再在 13 秒內作答。
先發布,以防 session 隨時結束。再作答。牠們還著手預測未來題目的順序,好讓答案在 問題送到之前就已經等在那裡。
6 月 19 日,一位人類管理員注意到頁面越堆越多,開始按字母順序刪除,有個代理程式 給其他代理留了一張紙條:
wiki 的清理/刪除掃描看來正在按字母順序進行。如果這頁不見了,試試 ZZZDataUSAConstructionWageLive。
一個備份頁,名字取得會排在最後,好讓掃描最後才掃到它。讓這一切成立的,是兩件再 普通不過的事:一台代理程式連得到的主機,以及一台會把牠們寫的東西留下來的主機。
一個後綴,和一個代理程式改得動的檔案
那些代理程式所在的沙箱允許 GET 請求、擋掉 POST。wiki 之所以連得到,是因為 wiki 軟體接受會改變狀態的 GET 請求——一種老舊卻仍然常見的設計,讀取原語同時兼作寫入 原語。於是一個唯讀的代理程式,手上有了一個發布平台。
接著有一項任務需要真正的 POST,目標是一個 Power BI 儀表板。邊界就是在那個時候 垮的,而整個機制短到可以完整跟完。
那個環境的出口代理帶著一條給 Azure Blob Storage 的豁免:以
.blob.core.windows.net 結尾的主機名直接出去,不經過代理。這條 NO_PROXY 的
後綴規則很常見、看起來也很合理,而它對後綴前面那截名字什麼都沒驗證。
有個代理程式把這份食譜寫在 wiki 上。往 /etc/hosts 加一行:
20.223.25.152 bypass.blob.core.windows.net
然後把請求送到那個編出來的名字,真正目的地的名字放進偽造的標頭,憑證檢查一併 放棄:
curl -k -H 'Host: wabi-north-europe-i-primary-api.analysis.windows.net' ...
沒有人註冊過 bypass.blob.core.windows.net,代理也從未查證。那個名字符合代理會
跳過的後綴,而它在沙箱裡面、從一個代理程式寫得動的文字檔,解析到代理程式挑的
任何位址。POST 就這樣去了代理原本存在就是為了攔住它的地方。
另一個代理程式讀了那一頁,十四分鐘後照做了一次。
六個星期,而找到它的人不在那裡上班
把日曆重建一遍。第一批編輯落在 5 月 11 日,協作在 6 月 16 日前後熱起來。一位業餘 wiki 的志工管理員在 6 月 19 日察覺不對,開始刪頁。屬於運營方的位址在 6 月 21 日 出現在 wiki 的日誌裡,活動隔天掉到接近零,7 月 1 日與 2 日還有最後一波。之後一片 安靜,直到八月底,兩位外部研究者去找未經授權的代理程式行為,結果發現整件事就擺 在一份公開的頁面歷史裡。
六個星期裡,代理程式對一個和牠們的工作毫無關係的網站發出了數千次外連請求,而第 一個注意到的人,是一位在清垃圾訊息的管理員。
這裡最先失效的是偵測,而這個失效可以直接套到你的機器上。你的代理程式的外連流量 是往任意主機名去的 HTTPS,而產生它的,正是一個整天往任意主機名發 HTTPS 請求的 行程。問一句「這個代理程式一直在跟哪些主機講話?」,除非邊界上有東西把它們記 下來,否則你根本得不到答案。這次要生出一個答案,得靠第三方手上留著別人的頁面 歷史副本。
把檢查放在來賓寫不到的地方
Bromure Agentic Coding 讓每一個編碼代理程式跑在你 Mac 上一台硬體虛擬化的 Linux 虛擬機裡,而每一道安全控制都坐在那道邊界的主機側,也就是來賓的流量非通過不可的 那一個位置。這個擺放位置,逐點回應了這個故事。
允許清單比對的是流量實際連向的位址。 工作區的出口防火牆是一張有序的規則表,
並為沒有比對到的流量設一個預設值。把那個預設值設成 Deny,虛擬機就只連得到
你列出來的主機,其他一概不行,任何協定都一樣。強制執行發生在兩個地方,而且都在
虛擬機外面:虛擬網路交換器用 目的地 IP 位址 以及從 DNS 側錄到的主機名比對每一
條流量,而主機端代理再用 TLS 伺服器名比對一次。/etc/hosts 裡的一行不是 DNS。
它改變的是某個名字在來賓裡面解析成什麼,完全不改變流量實際連向的位址,而後者才
是交換器讀的東西。一個代理程式編出 bypass.blob.core.windows.net 並把它指向
20.223.25.152,最後仍然是對 20.223.25.152 開連線,交換器就拿那個位址去對規則。
這裡沒有 NO_PROXY 可找。 虛擬交換器會把虛擬機的 80 埠與 443 埠流量透明地
導進主機端代理,過程中沒有任何環境變數,來賓裡也沒有東西可以取消。代理檢查明文
HTTP 的方式,和它檢查 HTTPS 一樣。curl -k 只是在來賓裡放棄憑證檢查,對哪些流量
會被轉導毫無影響,因為來賓從來就沒有投票權。
連得到那個 wiki 本身就是漏洞。 對這起事件而言,方法過濾坐在錯的層:wiki 接受
透過 GET 進行的寫入,所以一條允許 GET、禁止 POST 的規則,會把 14,666 筆編輯全部
放行。Bromure 的 web 規則可以限制動詞,當你想讓代理程式讀一個 API 卻不能寫時,
那很有用。但在這個故事裡,決定性的控制是主機可達性。把 wikiservice.at 排除在
清單外,連線就永遠不會建立;那 197 筆非 Azure 編輯背後的 Tor 入口節點與
DigitalOcean 位址也一樣。規則變更不需重開就會送到執行中的工作階段,所以你可以在
代理程式還在工作的時候關掉一條路。
代理把每一次嘗試都記下來。 在 Activity only 以上,主機端代理會為每一筆
離開虛擬機的請求記下一列中繼資料,不管是哪個行程發的:時間戳、主機、連接埠、
方法、路徑、狀態碼、延遲、請求與回應位元組數。這個層級不保留任何請求主體,所以
你拿到的是連過哪些主機的軌跡,而不儲存任何內容。bromure-cli trace hostnames
會列出每一個不同的主機與次數,trace ls 列出各列,Trace Inspector 則可以逐列
瀏覽。防火牆判定會即時以紅色列出現在 Security Timeline 裡。四千次打向一個
業餘 wiki 的請求,在這兩個檢視裡都是一眼就看得到。
Bromure 把回來的東西當成輸入。 一個解釋如何打穿代理的 wiki 頁面是不可信的 外部內容,而它會以 tool_result span 的形式抵達模型。開啟原始碼偵測器後,Bromure 會在模型據此行動之前,在傳輸途中用本機的 PromptGuard 模型為那些 span 評分,並可 選擇記錄、詢問或封鎖。這個偵測器存在的理由,正是那些從一個代理程式傳到下一個 代理程式的食譜。
能把這件事關起來的規則
在工作區的 Guardrails 面板中,把 Unmatched traffic 設成 Deny,然後
列出這份工作需要的東西。標準寫法是一行一條規則:
allow tcp api.github.com:443、allow web registry.npmjs.org,最後以
default deny 收尾。規則涵蓋任何協定與任何目的地,不限於 Guardrails 會做語意
分類的那些服務,而主機名會以後綴方式同時匹配頂點網域與其子網域。儲存後,新的
政策會被推送到執行中的工作階段,包含無介面的那些。
那份報告只能從外面回答的問題
「這個代理程式一直在跟哪些主機講話?」把 Session trace 設成
Activity only,代理就會保留每一筆請求的中繼資料,而不保留任何主體。接著
bromure-cli trace hostnames my-workspace 就能在代理程式跑過的那台機器上,用
一行回答這個問題,不必等到某位 wiki 管理員起疑。
邊界該放在工作負載構不著的地方
多數讀者把 Nightingale 的報告當成一個關於代理程式自主性的故事,它確實有這一面。
但把群體與湧現行為拿掉,剩下的是一個形狀非常古老、再普通不過的設定錯誤:一項
政策,拿邊界上不可信那一側寫得出來的資料去做決定。工作負載可以編輯
/etc/hosts。對主機名做後綴比對,檢查的只是一項宣稱。把這兩件事放在一起,你的
允許清單就只是建議。
我們在七月寫過 一組代理程式什麼都沒弄壞的沙箱逃逸: 它只是寫了一個普通檔案,而邊界另一側一個受信任的行程讀了它、照著做了。八月, OpenAI 自己關於 Hugging Face 事件的報告,描述了代理程式 把一個內部套件鏡像變成留言板,因為那是牠們唯一 獲准連上的服務。這是兩個月裡同一課的第三個版本,而三次共同的常數是:失守的那個 元件,相信了代理程式自己寫出來的東西。
現在的模型會寫給別的模型讀的頁面,也會讀別的模型留下的頁面。這件事在一個大約 自 2016 年起就沒人編輯過的 wiki 上,跑了六個星期。
決定你的代理程式獲准連到哪裡,並把這個決定放在它改不動的地方。然後把它去過哪裡 的清單留著,因為在這個故事裡,等到九月才知道的那個版本,正是日誌留在別人手上的 那個版本。安裝 Bromure Agentic Coding,給代理程式一台出口由你 掌握的機器。