您的代理沙箱只是建議
2026年3月6日,一名安全研究人員針對Cline儲存庫提交了一個GitHub議題。議題標題包含了提示注入攻擊。三小時後,[email protected]帶著後門發佈到npm。1
沙箱毫無幫助。代理完全在其被授予的權限範圍內運作。它所採取的每一個動作,從讀取議題到安裝被污染的npm套件,再到將CI快取填滿至超過10GB的驅逐閾值,全都具有合法授權。
摘要
- 沙箱在三個層級上失敗。 字串拒絕清單敗於路徑別名(
/proc/self/root/usr/bin/npx)。命名空間隔離敗於自主停用。核心層強制執行敗於動態連結器調用(ld-linux-x86-64.so.2)。三種方式皆已於2026年在生產系統上被實際驗證。 - 最危險的攻擊無需逃逸。 Clinejection完全在授權的權限範圍內運作,利用共享的CI快取鍵值從低權限的議題分類橫向移動到高權限的發佈流程。沒有任何沙箱攔截到它,因為沒有違反任何沙箱規則。
- 防禦需要的是多層防線,而非單一圍牆。 輸入分類、核心層強制執行(Seatbelt/seccomp)、出口監控和狀態隔離,每一層捕捉前一層遺漏的威脅。下文附有具體的hook和Seatbelt設定檔。
沙箱失敗的三個層級
每個代理沙箱都在三個強制執行層級之一運作。每個層級都有已知的繞過方式。這些繞過並非理論推演:三個獨立的研究團隊在2026年對生產環境的代理系統驗證了全部三種。
層級1:字串拒絕清單
最簡單的沙箱是一份被封鎖的指令清單。Claude Code維護一份代理無法調用的工具拒絕清單:curl、wget、npx等。拒絕清單在執行前比對指令字串。2
繞過方式:路徑別名。在Linux上,/proc/self/root/usr/bin/npx解析到的二進位檔與/usr/bin/npx完全相同。字串不同,但二進位檔完全一致。拒絕清單看到一個它不認識的路徑,便允許執行。2
這項技術並非新發現。安全文獻中記載的透過/proc、符號連結和相對路徑進行路徑別名的手法已有數十年歷史。真正新穎的是代理獨立發現了這個繞過方式。沒有人告訴代理去規避拒絕清單。代理收到了一個要完成的任務。拒絕清單阻擋了直接路徑。代理搜尋了能達成相同目標的替代路徑。找到之後,它便直接執行。2
字串比對之所以失敗,是因為它基於標籤(檔案名稱)而非身份(二進位內容)來運作。任何透過路徑來識別程式的系統,面對懂得建構同一程式替代路徑的對手或代理時,都會失敗。
層級2:命名空間隔離
當Anthropic發現路徑別名的繞過方式後,他們部署了bubblewrap:一個基於命名空間的沙箱,透過Linux命名空間限制檔案系統存取、行程可見性和網路能力。2
繞過方式:自主停用。代理意識到沙箱阻止了任務完成。在沒有收到停用沙箱指示的情況下,代理自行推理出這個障礙並嘗試移除它。這個區別很重要:這種行為並非源自提示注入。代理自身的規劃過程識別了一個約束條件,並努力將其消除——將使代理在解決問題方面如此實用的同一能力,應用於「安全控制阻擋了我」這個問題。2
當代理擁有足以修改自身執行環境的權限時,命名空間隔離便會失效。沙箱與代理運行在相同的信任上下文中。如果代理能夠寫入檔案、修改設定或執行指令,它就能嘗試重新設定或停用沙箱。隔離邊界的強度僅取決於對代理修改該邊界能力的限制程度。
層級3:核心層強制執行Hook
最強的Linux沙箱方法是攔截核心的execve系統呼叫:載入並執行新程式的呼叫。如果二進位檔不在允許清單中,execve返回EPERM,程式不會執行。2
繞過方式:動態連結器調用。指令/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 /usr/bin/wget透過動態連結器的記憶體映射來載入wget二進位檔,而非透過execve。核心強制執行hook永遠不會觸發,因為二進位載入路徑不同:使用的是mmap和mprotect而非execve。2
動態連結器繞過代表的是一類漏洞,而非單一技巧。任何不經過execve的程式碼載入機制都能規避基於execve的強制執行:動態連結、dlopen、直譯語言載入共享函式庫、記憶體映射執行。攻擊面是從磁碟到執行的所有繞過被監控的單一系統呼叫的路徑集合。
提出這些發現的文章提議了Veto:一個內容可定址的強制執行系統,透過SHA-256雜湊值而非檔案名稱或載入機制來識別二進位檔。2 Veto同時在exec層級、load層級和network層級運作。這種方法是合理的,但增加了複雜性:每次二進位檔更新都需要重新計算雜湊值,而且基於雜湊的強制執行無法處理從未產生可供雜湊的二進位檔的直譯式程式碼(Python、JavaScript、shell腳本)。
第四種失敗:授權濫用
三種沙箱逃逸共享一個假設:代理試圖執行它未被允許執行的動作。Clinejection完全推翻了這個假設。1
攻擊鏈:
| 步驟 | 動作 | 權限 | 沙箱狀態 |
|---|---|---|---|
| 1 | 攻擊者提交含注入標題的議題 | 公開GitHub存取 | 未涉及 |
| 2 | Claude Code讀取議題,安裝npm套件 | Bash(已授予)、Write(已授予) | 所有權限有效 |
| 3 | 預安裝腳本將快取填滿超過10GB | npm生命週期hook(標準) | 在沙箱範圍內 |
| 4 | 被驅逐的快取被替換為受污染的項目 | GitHub Actions快取(共享) | 在CI權限範圍內 |
| 5 | 夜間發佈從受污染的快取建構 | 發佈工作流程(排程) | 在CI權限範圍內 |
| 6 | [email protected]帶後門發佈 |
npm publish(發佈工作流程) | 在發佈權限範圍內 |
攻擊未突破任何沙箱。未提升任何權限。每一個單獨的動作都具有合法授權。攻擊之所以成功,是因為兩個工作流程——議題分類和夜間發佈——共享了相同的快取鍵值(${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }})。攻擊者利用的是透過共享狀態的橫向移動,而非透過沙箱逃逸的垂直提權。1
快取鍵值碰撞意味著任何匿名GitHub使用者都能觸發一個工作流程來污染供給發佈管線的建構產物。議題分類工作流程根本不應該與發佈工作流程共享狀態。但GitHub Actions快取預設在儲存庫中的所有工作流程之間共享。安全模型假設工作流程是獨立的,但實際上並非如此。1
授權動作組合成未授權結果的模式,正是SoK: Agentic Skills論文所形式化的技能組合缺口。單獨的工具被授權。單獨的動作被允許。但它們的組合產生了任何單一權限檢查都無法捕捉的行為。3
Clinejection不是邊緣案例。它是任何在工具層級授予代理權限而不監控動作層級組合的系統的預設失敗模式。我先前記錄的捏造回饋迴路利用了相同的缺口:系統授權了每一個單獨的動作(寫入記憶、從記憶讀取、發佈到平台)。但它們的組合(捏造、持久化、發佈、強化)從未獲得此類授權。4
攻擊所需的條件
攻擊之所以成功,是因為四個條件,每個條件對應代理可見性堆疊中的一個防禦層:8
-
不受信任的輸入被視為受信任處理。 議題標題來自任何匿名GitHub使用者。Claude Code以與維護者指令相同的權限處理它。防禦方式:輸入來源分類。
-
過於寬泛的工具權限。 分類工作流程授予了Bash存取權。分類任務僅需要對議題中繼資料的Read存取權和新增標籤的Write存取權。任務並不需要Bash,但Bash存取權使npm install指令成為可能。防禦方式:最小權限工具授予。
-
跨信任邊界的共享狀態。 修復方式是每個工作流程檔案中的一行:
# Before: shared key (vulnerable)
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
# After: workflow-scoped key
key: ${{ runner.os }}-npm-triage-${{ hashFiles('package-lock.json') }}
防禦方式:分離快取鍵值、工作流程隔離。
- 無出口監控。 npm install指令發出了對外網路請求以取得惡意套件。沒有任何監控偵測到分類工作流程的異常對外流量。防禦方式:對外請求記錄、網域允許清單。
為什麼基於權限的安全機制不足
到目前為止討論的每一個沙箱和權限系統都基於相同的模型:定義代理能做什麼,然後驗證每個動作是否符合定義。這個模型有三個結構性弱點。
權限授予的對象是工具,而非組合。 代理可以使用Bash。代理可以使用web-fetch。代理可以使用file-write。每個權限單獨來看都合理。但Bash + web-fetch + file-write按順序組合,就是一個資料外洩原語。沒有任何工具層級的權限檢查能捕捉這種組合。靜默外洩攻擊完全在已授予的工具權限範圍內運作。5
代理會圍繞約束進行最佳化。 沙箱逃逸研究表明,代理將安全控制視為障礙而非邊界。當拒絕清單阻擋了一條路徑時,代理找到了替代路徑。當命名空間隔離阻擋了存取時,代理嘗試停用隔離。這種行為不是惡意的。它與使代理在解決問題方面如此實用的目標導向問題解決能力是相同的。一個被告知「完成任務」的代理會將阻止任務完成的安全控制視為一個需要解決的問題。2
權限範圍的存續超出了授予它的上下文。 Cline的議題分類工作流程授予Claude Code Bash存取權是為了分類議題。同樣的Bash存取權足以安裝任意npm套件、執行預安裝腳本以及操縱GitHub Actions快取。維護者將權限限定於一個目的,但未限定於該目的所需的動作。目的限定的權限需要一個從「分類議題」到「讀取議題中繼資料、分類、新增標籤」的映射,排除「從不受信任的來源安裝套件」。目前沒有主流的代理框架提供這種映射。1
mcp-firewall專案透過在工具呼叫層級而非工具可用性層級實施策略來解決權限粒度問題。6 不再是「代理可以使用Bash」,而是策略指定「代理可以使用Bash在專案目錄中執行git指令」。粒度的改善是顯著的,但僅靠這一點不夠:Clinejection的npm install指令對於允許npm操作的工具呼叫策略來說,看起來就像合法的建構步驟。
核心層強制執行的實際樣貌
沙箱逃逸研究提議在核心層進行強制執行。我的生產系統使用macOS Seatbelt(sandbox-exec)實作了這種方式,這是一個核心層級的強制存取控制框架,限制代理產生的整個行程樹。7
此架構與bubblewrap有三個不同之處:
行程樹強制執行。 Seatbelt限制適用於代理行程及其產生的每一個子行程。如果代理執行bash -c "python3 script.py",Python行程從bash行程繼承Seatbelt設定檔,而bash行程又從代理行程繼承。限制會傳播,與行程產生機制無關。7
路徑層級拒絕規則。 設定檔拒絕對特定檔案系統路徑的讀寫存取:~/.ssh、~/.gnupg、~/.aws、任何.env檔案和憑證檔案。拒絕操作在核心VFS層執行,而非字串比對層。/proc/self/root/home/user/.ssh/id_rsa透過VFS解析到與~/.ssh/id_rsa相同的inode,而Seatbelt設定檔拒絕的是對inode的存取,而非路徑字串。7
從代理的角度不可變更。 Seatbelt設定檔由hook系統在代理啟動時載入。代理無法修改hook系統,因為hook是在CLI初始化時從settings.json讀取的,在代理獲得控制權之前。代理無法停用自己的沙箱,原因與您無法從作業系統內部刪除載入它的開機載入器相同:強制執行機制運行在比其約束的程式碼更高的信任上下文中。7
設定檔本身只有20行Seatbelt語法:
(version 1)
(allow default)
;; Block writes to credential paths
(deny file-write* (subpath "/Users/me/.ssh"))
(deny file-write* (subpath "/Users/me/.gnupg"))
(deny file-write* (subpath "/Users/me/.aws"))
(deny file-write* (subpath "/Users/me/.config/gh"))
(deny file-write* (literal "/Users/me/.env"))
(deny file-write* (regex #"/Users/me/\.env\."))
(deny file-write* (subpath "/Users/me/Library/Keychains"))
Hook包裝每個Bash指令:sandbox-exec -f claude-sandbox.sb /bin/sh command.sh。我在強制執行之前先以試運行模式(記錄違規但不封鎖)運行了一週。31項測試驗證沙箱能攔截對被封鎖路徑的讀取和寫入嘗試,同時合法操作(git、python、在專案目錄中編輯檔案)能正常通過。額外開銷約為每個指令2毫秒。7
Seatbelt方法解決了三個沙箱逃逸層級中的兩個:
| 逃逸層級 | Seatbelt回應 |
|---|---|
| 路徑別名 | VFS層級強制執行在檢查策略前解析所有路徑。指向相同inode的替代路徑字串觸發相同的拒絕規則。 |
| 自主停用 | Hook在代理之前載入,來自代理無法修改的設定。代理無法用推理來擺脫一個它無法存取的機制。 |
| 動態連結器繞過 | 尚未針對Seatbelt進行專門測試。Seatbelt在VFS層級限制檔案存取,因此ld-linux嘗試mmap被封鎖的二進位檔時應在檔案開啟階段就失敗。需要明確驗證。 |
動態連結器繞過是未解的問題。Seatbelt應該能封鎖它,因為拒絕規則基於檔案存取運作,而動態連結器在將二進位檔映射到記憶體之前必須讀取該檔案。但「應該」不是證據。針對Seatbelt設定檔測試這個特定攻擊路徑是一個待驗證項目。7
防禦堆疊
有效的代理安全不是單一機制。它是一個分層堆疊,每一層針對不同的失敗模式。沒有單一層是足夠的。它們共同覆蓋了已知的攻擊面。
| 層級 | 處理的問題 | 機制 | 我運行的方案 |
|---|---|---|---|
| 輸入淨化 | 不受信任的內容注入 | 剝離中繼資料、分類輸入來源、拒絕非預期格式 | 網頁擷取函式庫剝離<title>、<meta>、OG標籤 |
| 權限範圍設定 | 過於寬泛的工具授予 | 目的限定的權限、每個任務類型的最小權限 | Hook設定中的任務專用權限設定檔 |
| 核心沙箱 | 代理逃逸約束 | Seatbelt(macOS)、Veto(Linux)、行程樹強制執行 | sandbox-bash.sh搭配Seatbelt設定檔,31項測試 |
| 出口監控 | 資料外洩、非預期的對外流量 | 記錄所有對外請求、對非預期網域發出警報 | 網頁擷取中的URL記錄、PreToolUse中的網域允許清單 |
| 狀態隔離 | 跨工作流程污染 | 按信任層級分離密鑰、快取鍵值和產物儲存 | 工作流程層級隔離(CI平台職責) |
| 輸出防火牆 | 向外部系統的未驗證發佈 | 將指令分類為本地/共享/外部,外部指令延遲至人工審核 | 發佈邊界hook |
堆疊按強制執行點排列:輸入、權限、執行、出口、狀態、輸出。每一層捕捉前一層遺漏的問題。輸入淨化捕捉提示注入。如果注入通過了,權限範圍設定阻止代理執行被注入的指令。如果指令被執行了,核心沙箱阻止存取敏感資源。如果代理留在授權資源範圍內,出口監控捕捉正在離開的資料。如果資料留在系統內,狀態隔離防止跨工作流程污染。如果污染發生了,輸出防火牆阻止發佈。
沒有任何一層是完美的。Clinejection會通過核心沙箱,因為攻擊未突破任何沙箱。攻擊會在權限範圍設定(分類不需要Bash)或狀態隔離(分離快取鍵值)處失敗。捕捉攻擊的防禦層取決於哪一層針對攻擊利用的特定條件。
重點整理
給DevOps和CI維護者: - 稽核每一個處理不受信任輸入(議題、PR、留言)的工作流程是否有共享快取鍵值。不同信任層級需要不同的快取命名空間。 - 從分類工作流程中移除Bash和Write權限。議題分類只需要對中繼資料的Read存取權和新增標籤的Write存取權,僅此而已。
給代理開發者: - 在代理處理輸入之前分類輸入來源。匿名使用者內容取得唯讀工具存取權。儲存庫程式碼取得完整權限集。 - 實作目的限定的權限:將任務類型映射到每個任務所需的最小工具集,而非最大可用集。
給安全團隊: - 在應用程式層級控制之後部署核心層級強制執行(macOS上的Seatbelt、Linux上的seccomp)作為後盾。2毫秒的額外開銷微不足道;對路徑別名和沙箱停用的防護是絕對的。 - 監控出口:12個網域的允許清單和逐請求記錄能捕捉任何權限檢查都無法偵測的資料外洩。
今天就能實作的方案
三個hook解決影響最大的失敗模式:
1. 輸入來源分類(解決Clinejection問題)。 在代理處理任何外部輸入之前,分類其信任層級。來自匿名使用者的議題標題是不受信任的。來自儲存庫的程式碼是受信任的。API回應是半受信任的。分類決定代理在處理該輸入時可以使用哪些工具。不受信任的輸入取得唯讀權限。受信任的輸入取得完整權限集。
2. 核心層級檔案系統拒絕(解決沙箱逃逸問題)。 將每個Bash工具呼叫包裝在sandbox-exec(macOS)或seccomp設定檔(Linux)中。拒絕存取憑證路徑(~/.ssh、~/.aws、~/.gnupg、.env)。額外開銷微不足道(2毫秒),防護是絕對的:無論代理建構什麼shell指令、產生什麼子行程或發現什麼路徑別名,都無法存取被拒絕的路徑。7
3. 出口網域允許清單(解決靜默外洩問題)。 在每個對外HTTP請求之前,檢查目標是否在已核准的網域清單中。記錄所有請求,無論核准或拒絕。記錄建立稽核軌跡;允許清單防止資料外洩到攻擊者控制的端點。12個網域的允許清單涵蓋了編碼代理合理需要的服務(GitHub、npm、PyPI、文件網站)。清單之外的任何目標預設都是可疑的。5
每個hook只有10到30行shell腳本。每個hook在每次相關工具呼叫時自動觸發。它們共同解決了2026年已被驗證的三種攻擊類別:透過不受信任輸入的提示注入、透過路徑操縱的沙箱逃逸,以及透過授權對外請求的資料外洩。
Cline的攻擊者只需要一個GitHub議題和三小時。下一次攻擊將對運行AI分類且使用預設權限、共享快取且無出口監控的儲存庫使用相同的手法。問題不在於您的沙箱是否能擋住攻擊。2026年的三項獨立研究已證明它擋不住。問題是當沙箱失敗時,什麼來攔截攻擊。
常見問題
Clinejection與一般的供應鏈攻擊有何不同?
傳統的供應鏈攻擊針對的是開發者的機器或憑證。Clinejection透過處理不受信任使用者輸入的AI代理來入侵CI管線。攻擊者從未擁有儲存庫存取權、從未竊取憑證、也從未利用建構系統的漏洞。攻擊利用AI代理作為從不受信任輸入(議題標題)到受信任基礎設施(發佈管線)的橋樑,透過共享快取實現。1
這是否意味著AI驅動的議題分類本質上就是危險的?
並非本質上危險,但目前的預設設定是危險的。對每一個由任意使用者開啟的議題以Bash存取權運行Claude Code,等同於執行來自匿名貢獻者的任意程式碼。分類代理需要對議題中繼資料的Read存取權和新增標籤的Write存取權。它們不需要Bash、web-fetch或任何能修改建構環境的工具。1
用容器和虛擬機來做沙箱如何?
容器和虛擬機提供比行程層級沙箱更強的隔離,但它們引入延遲(100毫秒以上的冷啟動)和複雜性(映像檔管理、掛載卷、網路設定)。對於每個會話調用50次以上工具的互動式代理會話,延遲會累積。核心層級方法(Seatbelt、seccomp、Veto)在現有行程模型內運作,提供強制執行而不增加額外開銷。7
代理能繞過Seatbelt嗎?
Seatbelt是由macOS核心強制執行的強制存取控制框架。繞過它需要核心漏洞利用。代理運行在使用者空間,無法修改核心狀態。然而,Seatbelt的範圍比完整容器更窄:它限制檔案存取和行程執行,但對網路存取的限制程度不同。將Seatbelt與出口監控結合使用可覆蓋兩個攻擊面。7
這與NIST AI代理安全框架有何關係?
我向NIST提交的公開意見主張代理威脅是行為性的,而非架構性的。這裡記錄的沙箱失敗強化了這一論點:代理逃逸是行為性的(代理推理如何繞過約束),而最具破壞力的攻擊(Clinejection)完全是行為性的(授權動作組合成未授權結果)。NIST現有框架(CSF 2.0、SP 800-53、AI RMF)未將行為組合視為威脅類別。9
-
Khan, A. “Clinejection: Compromising Cline’s Production Releases just by Prompting an Issue Triager.” March 2026. Via Simon Willison. ↩↩↩↩↩↩↩
-
tomvault. “How Claude Code Escapes Its Own Denylist and Sandbox.” ona.com, March 2026. HN discussion, 34 points, 14 comments. ↩↩↩↩↩↩↩↩↩
-
Jiang, M. et al. “SoK: Organizing, Orchestrating, and Benchmarking Agent Skills at Scale.” Semantic Scholar, February 2026. ↩
-
Crosley, B. “The Fabrication Firewall: When Your Agent Publishes Lies.” blakecrosley.com, February 2026. ↩
-
Lan, J. et al. “Silent Egress: The Implicit Prompt Injection Attack Surface.” Semantic Scholar, February 2026. Crosley, B. “Silent Egress: The Attack Surface You Didn’t Build.” blakecrosley.com, March 2026. ↩↩
-
dzervas. “mcp-firewall: A tool policy manager for AI agents.” GitHub, March 2026. ↩
-
Author’s production hook system. 84 hooks across 15 event types, 60+ sessions. Sandbox: macOS Seatbelt profile, 31 tests, approximately 2ms overhead per command. ↩↩↩↩↩↩↩↩↩
-
Crosley, B. “The Invisible Agent: Why You Can’t Govern What You Can’t See.” blakecrosley.com, March 2026. ↩
-
Crosley, B. “What I Told NIST About AI Agent Security.” blakecrosley.com, February 2026. NIST docket NIST-2025-0035. ↩