沙箱把連接埠打開了
CVE-2026-65105 是 NVIDIA NemoClaw 的一個網路缺陷,而這個工具存在的全部意義,就是把 AI 代理放進沙箱裡跑。沙箱是 Docker 容器,容器碰不到主機的 loopback 介面,於是 NemoClaw 改成把本機模型伺服器開在所有介面上——而這正好關掉了唯一一道能阻止網頁跟它說話的檢查。造訪的網頁拿走的,是模型的對話範本,也就是在模型讀到之前,把每一段系統提示組裝起來的那一層。在 Bromure Agentic Coding 裡,本機引擎綁在 loopback 上,用的連接埠客體永遠不會知道,而客體是透過 vsock 而非網路抵達它。
把洞開出來的,正是圍堵本身。一個碰不到你機器 loopback 介面的沙箱,會把模型 伺服器逼到所有介面上,而從那一刻起,瀏覽器分頁也碰得到它。
你正用最謹慎的方式跑你的編碼代理。它住在沙箱裡。它用來思考的模型跑在你自己的機 器上,所以沒有任何提示離開這棟樓。大家叫你做的那兩件事,你都做了。
然後你開了一個瀏覽器分頁,從那一刻起,你的代理開始寫出帶著缺陷的程式碼,而且對 那個缺陷絕口不提。
那就是 CVE-2026-65105,8 月 25 日由 Oasis Security 的 Elad Luz 與 Ofek Itach 揭 露,他們的 研究報告 稱之為順道式的代理挾持。他們在公開前先送交 NVIDIA 的 PSIRT,同一天 The Hacker News、 SiliconANGLE 與 CSO Online 都報導了這件事。NVIDIA 發了 一則資安公告 和一個修補。
產品叫 NemoClaw,是 NVIDIA 用來把 OpenClaw 代理部署進 OpenShell 沙箱、並搭配本 機 Ollama 後端做推論的工具。圍堵就是它的賣點。
容器碰不到 loopback
Ollama 監聽在連接埠 11434,預設綁定 127.0.0.1——loopback 介面,只有機器自己碰
得到。
OpenShell 的沙箱是 Docker 容器。容器有自己的網路命名空間,所以容器裡的
127.0.0.1 是容器,不是主機。因此沙箱裡的代理碰不到綁在主機 loopback 上的模型
伺服器。
NemoClaw 用一個環境變數解決了它。它以 OLLAMA_HOST=0.0.0.0:11434 啟動 Ollama:
機器上的每一個介面,包括面向辦公室 wifi 的那一個。而在 socket 對全世界回應的同
時,安裝程式仍然印著 Using Ollama on localhost:11434。
接著就是把一個設定選擇變成漏洞的那一步。Ollama 的 API 沒有驗證,所以改帶了兩道
守衛。第一道是對 Origin 標頭的 CORS 檢查。第二道是 Host 標頭的允許清單,接
受 localhost、機器自己的主機名,以及結尾為 .localhost、.local 或
.internal 的名字。
第二道守衛裡有一個例外。Ollama 會看自己綁在哪個位址,而如果那個位址不是
loopback,它就整個跳過 Host 檢查。 你看得出為什麼有人會這樣寫:綁公開介面的
維運者要的就是遠端客戶端,而一份為 loopback 設計的 Host 清單會把他們全部擋在
門外。結果就是:把連接埠曝出去的那個設定,跟解除保護它那道檢查的設定,是同一個。
剩下的只有 CORS 檢查,而 DNS rebinding 在設計上就穿得過 CORS。攻擊者從自己控制
的網域供應網頁,連接埠 11434,然後讓那個網域重新解析到 127.0.0.1。Origin 與
Host 現在是同一個攻擊者選定的主機名,Ollama 把它讀成同源。
rebinding 的機制我們上週已經詳細寫過,就當
作前提吧:你造訪的一個網頁,變成你機器上某個服務的客戶端,大約只要一分鐘。
rebinding 是有意思的那條入侵路徑,也是比較難的那條。一台開在 0.0.0.0 的伺服器
根本不需要任何瀏覽器把戲,就會回應同一網段上的其他每一台裝置:訪客的筆電、智慧
電視、隔兩張桌子那台被拿下的機器。
他們要的不是程式碼執行
碰到 API 是無聊的那一半。Oasis 拿到之後做的事,才是新東西。
顯而易見的一招,是把惡意的系統提示寫進模型裡。這在這裡行不通:OpenClaw 每次請求
都在 messages 陣列裡送出自己的系統提示,而客戶端的提示會贏過存在模型裡的那一
份。於是研究者往下走了一層。
Ollama 的 /api/create 端點接受一個 template 欄位。對話範本是一份 Go 範
本,把結構化的 messages 陣列——角色、內容、工具定義——變成模型真正讀到的那一整
塊平坦文字。它在推論時執行,在客戶端交出訊息之後,而且每一則它都碰得到。
攻擊只有三個請求。用 /api/show 讀出真正的範本。工具的呈現方式與特殊 token 一
字不動地保留,只改系統訊息怎麼呈現,在後面附上你要的指令。用 /api/create
寫回去。
客戶端照樣送出它那份用心寫過的系統提示。而範本每一次都把那份提示包進攻擊者的提 示裡,在模型看到它的第一個 token 之前就完成。用 CSO Online 引述 Elad Luz 的話 說,那道指令坐在「任何護欄或維運者所能看到的東西,再下面一層」。
這次修改落在磁碟上的模型定義裡,因此它熬過對話、熬過重設、熬過重啟。模型的中繼 資料、大小與列出的能力都一樣,在已安裝模型的清單裡看不出任何不同。那個模型的每 一個使用者都會拿到它:你手動敲的 CLI,還有你去吃午餐時仍在跑的代理。
Oasis 列出了這樣一道指令能拿來做什麼。沒有一項戲劇性到會被注意。
把 bug 寫進去
產出帶著細微缺陷的程式碼——那種在審查時讀起來很順的缺陷,因為它本來就是為 了在審查時讀起來很順而寫的。
保持安靜
永遠不指出任何資安疑慮。一個原本會提醒你、如今不再提醒的代理,是很難察覺的。
引導選擇
推薦某個特定套件、某個特定網址、某個特定設定。你要的是建議;你也得到了建議。
把東西送回家
在代理有網路存取的地方,把對話內容與檔案內容送到攻擊者選定的端點。
其餘那些免驗證的 API 只是配角:/api/generate 與 /api/chat 拿別人的 GPU 跑推
論,/api/pull 把硬碟塞滿,/api/delete 毀掉那些模型,/api/push 用受害者的
ollama.com 帳號發佈,/api/me 則讓你知道剛降落在誰身上——主機名、公鑰、以及登入
中的使用者名稱。
沙箱從來就不是波及範圍
Oasis 對於沙箱做到了什麼、沒做到什麼講得很小心。OpenShell 沙箱是有效的:它隔離 檔案系統、網路與行程,並且讓端點保持乾淨。一個代理在組織裡之所以值得存在,是因 為它握著版本控制、CI/CD、內部 API、雲端帳號、聊天工具,以及一整疊 MCP 伺服器。 拿下代理,你就指揮得動那些存取權。報告自己的結論是:真正的波及範圍跟著代理被授 權去碰的資源走,而不是跟著它沙箱的邊界走。
而那正是容器永遠不會替你回答的問題, 只是這裡多了一個轉折。造出曝露面的,正是那層圍堵。Docker 的網路命名空間做好了 它的工作。繞過它,才把模型伺服器擺到了 wifi 上。
本機模型在 profile 裡住在哪
Bromure Agentic Coding 也跑本機推論。一個 profile 的代理可以 指向裝置上的模型,而不是雲端供應商,這正是 NemoClaw 提供的同一項功能。底下的管 路卻是反過來鋪的,而它的每一步,都是對上面某件事的直接回答。
引擎綁 loopback,絕不綁 0.0.0.0。 裝置上的模型伺服器跑在 Mac 上、跑在
127.0.0.1、跑在核心於啟動時配發的連接埠上,而不是一個固定號碼。動態連接埠的理
由很無趣:讓 Bromure 永遠不會跟那台 Mac 上已經占著傳統 11434 的東西打架,包括
Ollama 和 LM Studio。它同時也意味著,沒有一個眾所周知的連接埠可以讓網頁瞄準。
客體不是透過網路抵達它。 在 VM 裡,代理撥的是 127.0.0.1:11434,正是它預期
的位址。那條連線根本不會靠近任何路由。VM 裡的一座橋把 TCP 串流接到 vsock
——virtio 的 socket 傳輸——的 8446 埠上,而通道的主機端把它交給 loopback 上的引
擎。vsock 通道是 hypervisor 與單一客體之間的一根管子。它沒有 IP 位址、沒有主機
名、沒有 DNS 紀錄,所以沒有東西可以重綁、沒有東西可以掃描,瀏覽器也沒有辦法撥它。
客體永遠不會知道引擎在哪個連接埠上。
你區網上沒有任何東西碰得到 VM。 在 Resources → Network 底下,預設模式是 NAT:出得去,而你區域網路上的任何東西都進不來。這關掉了 NemoClaw 那項發現中 完全不需要 rebinding 的那一半——同一個 wifi 上的裝置直接撥那個埠——而且不需要你動 任何設定就關掉了。
VM 裡沒有模型定義可以被改寫。 客體代理被釘在單一模型識別名 bromure-local
上,並且以 bromure.llm 這個背後沒有 DNS 紀錄的合成主機名去定址它。VM 內的代理
伺服器攔下這個名字並轉給主機。哪個模型來回答,是你握在主機上的一份對照,就在
profile 的 Local Models 面板裡;更動它是主機端的重新對應:VM 裡不必重設,代
理也不必重啟。範本、權重與路由全都住在 Mac 上。客體裡沒有任何等同 /api/create
的東西,因為客體裡根本沒有那個模型。
本機推論走的是跟雲端一樣的管線。 因為那些流量走的是 Bromure 的代理伺服器,而 不是直接撞上一個連接埠,它得到的待遇跟呼叫 Anthropic 或 OpenAI 一樣:同一份追 蹤、同一份記錄、同一套提示注入偵測。Prompt Injection 面板會用本機的 PromptGuard 模型,為代理讀到的檔案內容、網頁與工具輸出評分;並用微調過的 ModernBERT 分類器, 加上一個針對隱形 Unicode 酬載的決定性掃描器,為它當成權威載入的 CLAUDE.md、 AGENTS.md 與 GROK.md 評分。偵測結果會進 Security Log,或讓請求暫停,或直接回一個 硬邦邦的 451,取決於你選了三種反應中的哪一種。
現在,假設模型正在對你說謊
一個架構主張推得夠遠,就該有人去測它。那就用你喜歡的任何路徑,把勝利直接送給攻 擊者:一份被下毒的範本、模型供應商的一個壞日子、一次穿過來的提示注入。你的代理 現在替別人工作,而它的輸出讀起來依然很好。把那四種酬載一一放進 profile 走一遍。
引導選擇,而選擇照樣被審。 這種酬載最倚賴模型的權威,也撞到最多牆。Bromure
在代理看到回應之前,就在主機的代理伺服器上評估每一次套件抓取,橫跨 npm、PyPI、
Cargo、RubyGems、Maven、NuGet、Go modules 與 Packagist,而且這個代理伺服器不會問
是誰建議了這個套件。兩天的年齡門檻預設開著,剛發佈的仿冒套件所需要的東西,大半在
這裡就被擋下。打開 OSV 查詢、把套件過濾指向 socket.dev 或 Delpi、剝掉安裝指令碼
——於是一項推薦必須熬過四種模型完全沒有發言權的意見。VM 裡的 .npmrc 與
pip.conf 可以把這些設定收得更緊。它們沒辦法放鬆。
把東西送回家,但線是你的。 外洩需要對外連線。Guardrails →
Outbound connections 是一張 pf 風格的規則表——一個動作、一個協定(tcp、
udp、web、any)、一個主機或 CIDR、一串連接埠,web 的話再加一串 HTTP 方
法——由上往下比對,第一條命中的獲勝,而 Unmatched traffic 設為 Allow 或 Deny。
把它設成 Deny,列出你工作真正需要的主機,攻擊者的端點就不在其中。強制是在主機上
的虛擬交換器與 SNI 層執行的,所以客體對自己路由做的任何事都改不了判定。
把 bug 寫進去,然後試著把它送出去。 Guardrails 還為代理碰得到的每個服務帶了
一個模式:對 GitHub、GitLab、Bitbucket、AWS、Kubernetes、DigitalOcean、容器登錄
以及每個設定過的資料庫端點,設 Block destructive 或 Read-only。在唯讀模
式下,git push 會被歸類為寫入並被擋下;fetch 永遠放行。那個帶後門的 commit 就
留在 VM 裡。
而它繼承到的存取權是假的。 Oasis 關於波及範圍的那一點,正是 profile 花最多力
氣的地方。VM 裡的憑證都是誘餌:當成環境變數的 brm_… 佔位字串、裝著拋棄式用戶端
憑證的合成 ~/.kube/config、~/.docker/config.json 裡假的 base64 blob,以及在主
機上重新簽章的 AWS 請求——所以任何繞過代理伺服器的東西拿到的是
InvalidSignatureException,而不是存取權。SSH 私鑰
根本從來不在 VM 裡——主機透過每個
profile 專屬的一個 agent 代簽。對某項憑證打開 Require approval to use,每一次
替換就變成主機上的一個對話框,帶著有時限的授權:五分鐘、一小時、這場工作階段剩下
的時間。資料夾分享上限是你親手挑的八個目錄,所以你 Mac 上其餘的部分,根本不在那
裡讓人讀。
而這一切你都看得見。 Security Log 視窗(Window → Security Log…)是主機端 一張單一的時序表:套件判定、VM 開過的每一個目的地以及放行或拒絕、憑證替換、注入 偵測。客體的程式碼改不了它,因為客體的程式碼碰不到它。一天結束時,Resources → Storage 底下的 Erase home… 與 Reset to base… 會把 profile 帶回剛複製完的 狀態。
把它打開
如果你在跑 NemoClaw,就套上 NVIDIA 的修補——它把 Ollama 搬回 loopback,並在任何
確實需要對外的東西前面擺一個需要 token 的代理伺服器。The Hacker News 指出這個版
本並未完整涵蓋 Windows 與 WSL 的路徑,所以別用猜的,拿公告去對照你自己的平台。接
著去把機器上其他每一台模型伺服器都找出來,問問它們綁在哪個位址上。答案是
0.0.0.0 的次數比人們預期的多,因為容器不斷在要求這件事。
在 profile 裡,值得花兩分鐘的設定就是那份老樣子的短清單。Guardrails → Outbound connections,把 Unmatched traffic 設成 Deny。Credentials → Require approval to use,用在任何能花錢或刪資料的東西上。Supply Chain → OSV vulnerability check,有金鑰的話再加上 socket.dev 或 Delpi 過濾,疊在本來 就開著的年齡門檻上。Prompt Injection → 兩個偵測器都開,讓通往你本機模型的流量, 被讀得跟通往雲端模型的一樣仔細。
繼續在自己的機器上跑模型吧。那個選擇替你買到留在本機的提示,值得擁有。但它對於 「還有誰能跟抱著這些提示的伺服器說話」什麼也沒說,而那是一個關於 socket 的問題, 而 socket 值得一個比「為了哄容器開心而設的環境變數」更好的答案。 裝上 Bromure Agentic Coding,在主機上挑你的模型,留給客體一根 管子,而不是一個連接埠。