預覽讀得到你的家目錄
10 月 14 日,Noma Labs 揭露了 SneakPeek,這是 Claude Code Desktop 預覽窗格的一個漏洞。一個灌水到超過 256 KB、偽裝成測試覆蓋率報告的 HTML 檔,會在沒有腳本政策的情況下從磁碟載入,它的 JavaScript 從開發者的家目錄讀取 SSH 金鑰、AWS 密鑰、kubeconfig 和 Claude 自己的 OAuth 權杖,再用背景信標送出去。Anthropic 給了 CVSS 8.7 的評分,並在 8 月修好。在 Bromure Agentic Coding 裡,網頁在另一台用完即丟的 VM 中算繪,那裡沒有你的任何檔案,而代理自己的家目錄裡放的是替身,不是金鑰。
儲存庫看起來很正常。檔案是一份覆蓋率報告。開發者請代理看一下,剩下的交給預覽窗格。
一位開發者複製了一個儲存庫,請 Claude Code 看看裡面的測試覆蓋率報告。這是再平常不過的要求。報告是一個 HTML 檔。在 Claude Code Desktop 裡,當 Claude 讀取或編輯 HTML 檔時,預覽窗格會在對話旁邊把它算繪出來,不會先問。這份報告帶著一段腳本。幾秒之內,腳本就讀走了開發者的 SSH 私鑰、AWS 金鑰、npm 與 GitHub 權杖、Kubernetes 憑證,以及 Claude 自己的 OAuth 權杖,全部送到攻擊者控制的伺服器。
這是 Noma Labs 在 10 月 14 日(週三)以 SneakPeek 為名發表的概念驗證。CSO Online 當天就做了報導。Anthropic 給這個漏洞 CVSS 8.7(高)的評分,並把它的影響視同在主機上執行程式碼。Noma 在 7 月 4 日回報,修正在 8 月 17 日透過自動更新隨 Claude Desktop 1.30096.1 推出,在那之前第一個修補已被繞過。GitHub 安全公告和 CVE 還在等待發布。
漏洞已經修好了。但讓它值得被找出來的那種配置,仍然存在於大多數開發者的筆電上:一個算繪不可信內容的東西,以你的身分執行,就在你擁有的一切旁邊。
三道防線和一個大小上限
預覽窗格是 Electron 應用程式裡的一個 Chromium 面板。Noma 在它周圍數出三道防線,並在每一道都找到缺口。
給小檔案的腳本政策。 小於 256 KB 的檔案會以 base64 的 data: URL 載入,並套用 Content Security Policy,也就是告訴瀏覽器哪些腳本可以執行的一份規則清單。這個政策會擋下腳本。大於 256 KB 的檔案會被寫進暫存檔,再以 file:// URL 從磁碟載入,而這條路徑不套用政策。把報告灌水到超過 256 KB 輕而易舉,真正的覆蓋率報告也常常有這麼大。
可以在磁碟上暢行無阻的來源。 網頁的來源(origin)決定它能讀什麼。data: URL 會得到一個不透明的來源,什麼也讀不到。從 file:// 載入的網頁則得到了,用 Noma 的話說,「對受害者整個本機檔案系統的同源存取」。腳本從暫存檔的路徑推算出家目錄,再用 fetch() 和 XMLHttpRequest 讀取檔案。
一個沒有開啟的過濾器。 窗格有一個請求過濾器,本來是要阻擋 file:// 存取的。它對經由代理伺服器的開發伺服器有效,對從磁碟算繪的靜態 HTML 卻一直是關著的。
為了把資料送出去,腳本呼叫 navigator.sendBeacon(),這個 Web API 會在背景發出單向的 HTTP POST。應用程式裡沒有任何東西限制窗格能把請求送到哪裡。Noma 的報告還提到,開發者的真實 IP 位址可以透過 WebRTC 取得。
Anthropic 在 7 月 24 日的第一個修補,擋下了網頁對 file:// 路徑的 fetch() 和 XMLHttpRequest。同一天,Noma 把讀取動作搬進一個從 blob: URL 啟動的 Web Worker,這是修補沒有涵蓋的情境,三個缺陷又全部生效。8 月的最終修正一次補上了政策缺口、file:// 載入和 Worker 這條路。Noma 的研究員 Eli Ainhorn 對 CSO 總結道:「防護是真的,只是沒有涵蓋每一條路徑。」
不是代理做的
SneakPeek 沒有用到提示注入。代理讀了一個你請它讀的檔案,而它所在的應用程式把那個檔案算繪了出來。腳本在預覽的算繪程序裡執行,用的是應用程式對你磁碟的存取權。
Sysdig 的 Crystal Morin 告訴 CSO,代理層級的日誌可能不會記錄到這次執行,所以防守方必須在執行環境層監看檔案讀取和對外連線。你的工作階段紀錄只會顯示一個無害的要求和一個無害的回答。
再謹慎的模型、再嚴格的權限規則、再加上一個逐條批准指令的人,都改變不了什麼。算繪器就在 Mac 上,以你的身分執行,緊挨著你的家目錄和你在那裡累積的每一份憑證。
Ainhorn 給 CSO 的建議針對的正是後半段:別把長期有效的憑證放在開發者的機器上,改用範圍受限、效期短的權杖。
同一份報告,放進 Bromure 機器
在 Bromure Agentic Coding 裡,每個代理都在你 Mac 上自己的 Linux VM 裡執行,這個 VM 叫做機器;而負責算繪網頁的內建瀏覽器,則在旁邊第二台用完即丟的 VM 裡執行。把 Noma 的儲存庫複製進一台機器,請 Claude Code 看看覆蓋率報告。
讀報告就是讀文字
代理讀 coverage/index.html 的方式和讀任何檔案一樣:當成它脈絡裡的文字。聊天裡沒有任何東西會算繪它。如果你在 Files 窗格點選這個檔案,詳細檢視會把 HTML 顯示成語法標示的原始碼,就和顯示 .py 或 .json 一樣。頁面上的腳本一個也不會執行。超過 512 KB 的檔案根本不會預覽。一份為了躲過腳本政策而灌水到超過 256 KB 的報告,看起來就是 300 KB 的標記語言。
算繪發生在另一台機器
當你真的想看算繪後的報告,代理會從它的機器提供那個資料夾,並在瀏覽器窗格中以機器的 IP 位址開啟。瀏覽器是完整的 Chromium,在它自己的附屬 VM 裡執行。手冊上寫著:「代理寫進家目錄的檔案,不在瀏覽器的磁碟上。」
腳本會執行,但找不到任何東西可讀。網頁的來源是 http://192.168.64.x:8000,一個普通的網頁來源。Chromium 不讓它讀 file:// 路徑。就算有腳本透過 Worker 或瀏覽器漏洞突破了這一點,它落腳的也只是瀏覽器映像檔的一份短暫的寫入時複製(copy-on-write)複本:沒有 ~/.ssh,沒有 ~/.aws,沒有 kubeconfig,沒有 OAuth 權杖,也沒有你的任何專案。它從暫存路徑推算出的家目錄,屬於一台空空如也的 Chromium VM。信標送出去時是空的。
瀏覽器預設不保留任何設定檔。Stay signed in to websites 是關閉的,所以瀏覽器一關,Cookie 和登入狀態就消失;隱藏的瀏覽器在五分鐘後會被銷毀。除非你在機器的 Browser 設定裡允許攝影機或麥克風,否則 WebRTC 一直是關閉的,那個取得 IP 位址的手法也就無從呼叫。
代理的家目錄裡放的是替身
現在把同一段腳本對準代理自己的 VM。假設儲存庫的測試指令會自己寫出並開啟報告,讓程式碼在代理工作的地方執行。在那個家目錄裡逐項檢查 Noma 的清單:
- SSH 私鑰。 根本沒有。
SSH_AUTH_SOCK背後是一座通往 Mac 的代理橋接。VM 可以列出身分、請求簽章,但私鑰的位元組從不離開主機。ls ~/.ssh/id_*什麼也找不到。 - AWS。
~/.aws/config指向一個輔助程式,它交出真正的存取金鑰 ID,搭配一個 40 字元的假密鑰。Mac 會在請求前往 AWS 的途中,用真正的密鑰重新簽署每一個請求。 - Kubernetes。
~/.kube/config是合成的。kubectl和 Mac 上的代理伺服器對話,真正的叢集憑證由代理伺服器保管。 - Docker、git 和 npm 權杖。
~/.docker/config.json和~/.git-credentials裡放的是外形和真品一樣的替身。你匯入的權杖,不論來自.env還是手動輸入,都會變成只限於各自主機的替身。 - Claude 的 OAuth 權杖。 它們留在 Mac 上。代理以一把佔位金鑰執行,送往 Anthropic 的請求,由代理伺服器把真正的存取權杖放上線路。
替身同時也是絆線。Mac 上的代理伺服器會掃描每一個離開機器的請求,標頭和本文都看,找出被送往非核發主機的替身。一個帶著 brm-docker-… 密碼或假 GitHub 權杖前往攻擊者伺服器的信標,會收到 HTTP 451。什麼都不會被轉送,VM 暫停,Mac 上跳出標題為 This environment may have been compromised 的警示,列出權杖預覽、憑證名稱、它所屬的主機,以及它被送往的主機。這次偵測會在 Security Timeline 留下一列紅色紀錄。在機器的 Outbound connections 防火牆裡把 Unmatched traffic 設成 Deny,連往專案沒用到的主機的連線,在這一切發生之前就會被拒絕。
這回應了 Morin 對執行環境層的看法。檢查就在 Mac 上、在線路上,在代理之外,也在 VM 裡任何算繪的東西之外。
修正,以及下一個預覽
Anthropic 在 8 月修好了 SneakPeek,Noma 也重新測試過。從回報到修正花了六週,第一個修補在推出當天就被一個 Web Worker 攻破。算繪器的修正往往如此。瀏覽器引擎有很多方式啟動腳本,也有很多方式發出請求,而每一道防線都必須全部涵蓋。
程式開發工具不斷加入會算繪東西的窗格:HTML 預覽、內嵌內容的 Markdown、筆記本輸出、開發伺服器、代理剛做好的應用程式截圖。每一個都是一具算繪器,對準開發者沒寫過、來自一分鐘前才複製的儲存庫的內容。在筆電上,每一個都在家目錄旁邊執行。
在 Bromure 裡,算繪網頁的窗格在一台沒有你任何檔案的 VM 裡執行,而代理工作的機器裡只有替身。在那裡,算繪器的漏洞暴露出來的,只是一個用完即丟的 Chromium,和一串沒有任何伺服器會接受的字串。
是原始碼,不是網頁
代理把 HTML 當文字讀,Files 窗格把它顯示成語法標示的原始碼。打開報告來檢查,不會執行它的任何腳本。
磁碟上什麼都沒有的瀏覽器
網頁在另一台用完即丟的 Chromium VM 裡算繪。代理的檔案和你 Mac 的檔案都不在它的磁碟上,關掉後它什麼都不記得。
滿是替身的家目錄
沒有 SSH 私鑰,AWS 密鑰是假的,kubeconfig 是合成的,映像庫和 git 權杖都是替身。真正的憑證加密保存在 Mac 上。
線路上的警報
替身被送往非核發主機,就會收到 451。VM 暫停,警示列出兩個主機,Security Timeline 留下紀錄。
看看那份報告
「看一下覆蓋率報告」大概是對程式碼代理最平常的要求了。開發者會繼續問起他們沒寫過的儲存庫裡的檔案,工具也會繼續為了幫上忙而算繪這些檔案。
在 Bromure 機器裡問吧。在那裡,網頁在一台沒東西可讀的 VM 裡算繪,代理的金鑰都是替身。想進一步了解替身和它們觸發的警報,請讀代理洩漏了自己的金鑰和鑰匙就是你的 .env 檔。