10 月 14 日,Noma Labs 公布了 SneakPeek,一個針對 Claude Code Desktop 預覽窗格的漏洞利用鏈。一個公共儲存庫中的惡意 HTML 檔案,在 Claude 預覽時能夠執行 JavaScript,從使用者的家目錄讀取檔案,並將其發送到攻擊者的伺服器。
Noma 列出的概念驗證(proof of concept)讀取內容包括 SSH 私鑰、AWS 金鑰、npm 和 GitHub 權杖,以及 Kubernetes 和 Docker 憑證。Anthropic 將其評分為 CVSS 8.7(高危)。
此漏洞已修復:根據 Noma 的時間表,Anthropic 已於 8 月 17 日透過自動更新,在 Claude Desktop 1.30096.1 及更高版本中發布了修復程式。如果您的應用程式會自動更新,那麼您已經獲得了兩個月的保護。
我開發了 Reado,一個用於閱讀代理程式所撰寫程式碼的 IDE,因此一個存在於「向使用者顯示檔案」中的錯誤,也正是我的問題。這篇文章旨在解釋為什麼即使這個錯誤已經修復,它仍然值得理解,以及我在閱讀它之後,檢查了我的應用程式中的哪些部分。
當 Claude 讀取或編輯 HTML 檔案時,Claude Code Desktop 會在預覽窗格中顯示它,讓使用者立即看到結果。Noma 描述了圍繞該窗格的三道防禦措施:一個帶有 script-src 'none' 的內容安全政策(CSP),以防止 JavaScript 執行;將頁面載入為 data: URL,這賦予它一個不透明的來源,使其無法獲取本地檔案;以及一個針對開發伺服器代理的請求過濾器,用於攔截渲染器發出的請求。
這些防禦措施的漏洞在於它們的應用範圍。小於 256KB 的檔案會作為 data: URL 載入,並應用 CSP。然而,大於 256KB 的檔案會被寫入臨時檔案並作為 file:// URL 載入,而這條路徑從未應用 CSP。在 Chromium 中,file:// 頁面可以 fetch() 其他本地檔案。
此外,請求過濾器僅在窗格代理執行中的開發伺服器時才啟用,而不是在它從磁碟渲染靜態檔案時。因此,攻擊方式是:將一個 HTML 檔案填充到超過 256KB,在其中放入一個腳本,然後等待有人查看它。Noma 將其偽裝成測試覆蓋率報告,這是一種人們不會對其大小感到驚訝的檔案。
受害者複製儲存庫並要求 Claude 查看覆蓋率報告。Claude 讀取檔案,窗格渲染它,腳本隨即執行。整個過程中沒有任何權限提示,這是我一直在思考的部分。Claude 沒有執行命令或寫入檔案。它只是讀取了一個檔案,這是一個代理程式能做的最無害的事情。程式碼在應用程式的渲染器中執行,這是顯示結果的副作用。
Noma 於 7 月 4 日報告了這個錯誤。Anthropic 的第一個修補程式阻止了頁面對 file:// 的 fetch() 和 XMLHttpRequest 呼叫。Noma 將相同的 fetch() 移到一個從 blob: URL 建立的 Web Worker 中,這是一個過濾器未涵蓋的上下文,檔案再次被竊取出來。
他們於 7 月 24 日報告了這個繞過,同一天修補程式已準備好進行重新測試。8 月 3 日,Anthropic 確認第一個修復不完整,並開始進行一個完整的修復,根據 Noma 的說法,該修復涵蓋了「CSP、file:// 載入和 Worker 繞過」。它於 8 月 17 日發布。
Noma 的報告讚揚 Anthropic「針對每個繞過進行迭代,直到實現全面、分層的修復」,時間表也證實了這一點。但繞過本身就是一個教訓。第一個修補程式阻止了概念驗證所使用的 API。攻擊者卻使用了另一個。完整的修復則針對了條件本身:CSP、file:// 載入和 Worker。
我從中學到了三件事。首先,第二個程式碼路徑就是第二個安全模型。我猜測 256KB 的截止點是出於性能考量:大型 data: URL 速度很慢。沒有人會決定「大型檔案沒有 CSP」。但一個閾值卻悄悄地將大型檔案導向了具有不同規則的路徑。如果一個功能有兩種做同一件事的方式,那麼每一種都需要自己的安全審查。更好的是,只有一種方式。
其次,權限系統保護的是代理程式,而不是圍繞它的應用程式。我們為確保程式碼代理安全所做的大部分工作都與代理程式有關:批准此命令、拒絕在專案外部寫入、沙盒化 shell。所有這些都已到位,但沒有一個涉及此漏洞。Carlos 在一篇關於此錯誤的 dev.to 文章中評論得很好:「這裡儲存庫中的檔案是攻擊者,而查看器是工具。」
任何渲染儲存庫內容的東西(預覽、Markdown、筆記本、圖像、嵌入式瀏覽器)都是攻擊面,無論代理程式是否參與。
第三,「審查此檔案」是您最不信任該檔案的時候。觸發 SneakPeek 的請求是一個審查:查看此覆蓋率報告。這時檔案尚未被您信任的任何人查看過,因此顯示它的介面應該是應用程式中最不具能力的,而不是最具能力的。
Reado 如何顯示儲存庫?Reado 也有同樣的問題需要解決:它的存在是為了向您顯示儲存庫中的檔案,其中大部分都不是您編寫的。在閱讀 Noma 的文章後,我檢查了所有顯示檔案的地方,並提出了 SneakPeek 的三個問題:檔案的程式碼是否執行、頁面是否可以讀取磁碟、是否可以發送任何內容出去。
一個 .html 檔案會作為原始碼打開。Reado 渲染 Markdown、Jupyter 筆記本、圖像和 PDF;所有其他內容,包括 HTML,都只是編輯器中的文字。覆蓋率報告是您閱讀的東西,而不是執行的東西。Markdown 和筆記本輸出中的 HTML 會經過消毒器處理。
消毒器 rehype-sanitize 會剝離腳本、事件處理程式和 javascript: URL。README 中充滿了 HTML(置中標誌、徽章、<details>),因此 Reado 會透過 rehype-sanitize 渲染它。
在消毒器之後,是應用程式的內容安全政策。只有 Reado 隨附的腳本會執行 (script-src 'self'),並且該規則也涵蓋了 Worker,因此 blob: Worker 技巧沒有任何可供啟動的基礎。沒有框架,頁面也無法向應用程式本身以外的任何地方發送請求 (fetch()、sendBeacon())。
檔案大小絕不會改變檔案的載入方式。一個 500KB 的 README 會經過與 5KB 檔案相同的管道處理。超過大小限制時,Reado 會停止:它會在打開大型檔案之前詢問,並且根本不會打開超大型檔案。它從不切換到不同的載入器。
本地圖像僅來自開啟的專案。Reado 用於磁碟上圖像的協定以空範圍開始,並在您打開專案資料夾時擴展到該資料夾,絕不會擴展到磁碟的其他部分。SVG 透過 <img> 標籤顯示,其中 SVG 腳本不會執行,而 PDF 則繪製在畫布上。
同樣的檢查也發現了 Reado 嵌入式瀏覽器中的兩個錯誤,這是一個我以前沒有將其視為「顯示檔案」的介面:用於預覽您的開發伺服器的窗格。它從儲存庫可以控制的兩個地方獲取 URL。設計評論(像所有 Reado 評論一樣位於 .reado/ 中)攜帶它們所釘選頁面的 URL。代理程式透過同一資料夾中的命令檔案驅動窗格。
因此,一個複製的儲存庫可以附帶一個指向 file: 或 data: 頁面的評論,或者一個已經等待窗格打開的命令。兩者都已修復:窗格現在只打開 http 和 https 頁面,這在 Rust 層中透過一個所有呼叫者都會經過的地方進行檢查,並且一個在窗格打開時已經存在的命令會被記錄下來,但絕不會執行。這些修復已在主分支上,並將在下一個版本中發布。
共同點是我出於其他原因做出的選擇:Reado 是圍繞閱讀程式碼而構建的,因此它對檔案的預設處理是顯示文字。這不如即時預覽那麼令人印象深刻,但在 SneakPeek 之後,我很高興我做了這個選擇。
如果您使用這些工具,或開發它們:如果您使用 Claude Code Desktop,請檢查您的版本是否為 Claude Desktop 1.30096.1 或更高版本。Noma 的文章還列出了 GitHub 諮詢和 CVE 待定,請留意這些資訊。
如果您開發顯示儲存庫中檔案的工具,從這個錯誤中可以得出一些規則:將複製儲存庫中的每個檔案都視為來自陌生人的輸入,包括那些看似無聊的檔案。使用一個渲染路徑,無論檔案多大,都應用相同的限制。將您的限制應用於內容的來源,而不是它使用的 API。阻止 fetch(),攻擊者就會轉向 Worker。預設顯示原始碼。只渲染您必須渲染的內容。
Anthropic 修復了這個問題,並持續努力直到繞過也被堵住。這種模式並非他們獨有:每個顯示檔案的程式碼工具都有一個預覽窗格。值得在別人發現之前,檢查一下您的工具。