今天延伸昨天所講的,分別說明CaMeL如何把「決定做什麼」和「讀外部資料」拆給兩個分工的模型,看它實際上是怎麼運作的,這也是接下來要加以修改的部分,機制細節先了解清楚了,後面實作時才知道如何更改。
CaMeL裡有兩種模型,一種是Q-LLM(Quarantined LLM,隔離模型),專門負責讀外部資料的實際內容,例如信件、網頁,但它手上完全沒有呼叫任何工具的能力,讀完就只能把結果交出去,自己不能採取任何行動;另一種P-LLM(Privileged LLM,有權限模型),它只看得到使用者下的原始指令,工作是把要做的事用一個受限制的Python子集寫成一段程式碼,而作者選Python的原因是因為現有的LLM本來就很會寫Python,不需要額外訓練一套新語法,且能很好完成指定任務。
這段程式碼不是直接丟給一般的Python直譯器執行,而是丟給CaMeL自己寫的一個受限版直譯器,這個直譯器一邊執行,一邊做兩件事:追蹤每一筆資料是從哪裡衍生出來的(作者稱為data flow graph),以及檢查每筆資料身上帶的capability標記,也就是它的來源和可信度,等到程式碼要呼叫工具的那一刻,直譯器會把這次呼叫用到的所有參數的capability拿出來,比對是不是符合預先訂好的政策,不符合就擋下,不讓這次呼叫發生。
類似範例如下(示意,不是真實程式碼):
email_content = read_email(...) # 標記: capability = 不受信任
summary = quarantined_llm(email_content) # 仍然衍生自不受信任來源
if policy_allows(summary, action="send_email"):
send_email(to=..., body=summary) # 通過才放行
else:
block() # 標記不符政策,直譯器擋下
P-LLM寫出這段邏輯的時候,它從頭到尾沒看過 email_content 裡實際寫了什麼,它只知道要讀信和把結果拿去判斷能不能寄,真正接觸內容的是Q-LLM,而它沒有 send_email 這個能力,控制流(要不要呼叫 send_email)跟資料流(email_content 到底寫了什麼)被拆成兩條線,一條在P-LLM手上,一條在Q-LLM手上,兩條線只透過capability標記碰面。
圖片來源:本文自行整理
讀完後思考了一個問題:如果 summary 是Q-LLM把 email_content 大幅摘要、改寫過的結果,不是原封不動複製過來的,那capability標記還跟得上嗎?
我認為這正是這個機制設計得比較聰明的地方,data flow graph能夠追蹤結構性的衍生關係,判斷出值是從哪個值算出來的,summary 不管內容被改寫成什麼樣子,只要它是直譯器執行過程中,從帶著不受信任標記的 email_content 這個變數算出來,就會被標記成同樣不受信任,這是直譯器在執行期強制記錄下來的,不是靠比對內容猜出來的,換句話說,capability是照著程式執行的路徑走,這也是它跟單純的內容過濾器最根本的不同之處。
Q-LLM直接接觸外部內容,如果它自己被誘導,摘要摘錯、或是把攻擊者塞進去的指令當成正常內容轉述出來,這樣capability標記還擋得住嗎?
這正是這個架構分工的巧妙之處,不管Q-LLM輸出的內容對不對、有沒有被騙,它產出的summary這個值,衍生自帶不受信任標記的email_content,這件事在直譯器的data flow graph裡是死的,不會因為Q-LLM表現得多篤定、多有自信而改變,CaMeL根本不賭「Q-LLM會不會被騙」這件事,它會假設Q-LLM本來就有可能被騙,把賭注全部押在「不管Q-LLM說了什麼,那個值的來源標記都甩不掉」這條規則上,Q-LLM可以講錯話,但它講的話沒有能力繞過標記直接變成一個乾淨的、可以流向危險動作的值,這跟前面所提到的幾種防禦(例如偵測與過濾)差別最明顯,因為過濾器是「這次分類器判斷得準不準」,CaMeL則是「這個架構上的限制,不管內容多會演都繞不過去」。
如果一次任務同時讀了兩份不受信任的資料,例如同時讀一封信跟一個網頁,兩者的內容被合在一起、算出一個新的值,這個新值的capability標記直覺上應該是「取兩者裡面比較嚴格的那個」,只要有任何一個來源不受信任,合出來的結果就整個標記成不受信任,不能因為另一個來源比較乾淨就打折扣,這種以最低權限的合併邏輯在傳統資安的資訊流控制理論裡也是標準做法,CaMeL的data flow graph只要如實記錄每個值的完整衍生路徑,這種合併就是自動發生的結果,不需要另外寫規則去處理,這也是用圖結構追蹤衍生關係會比單純幫每個值貼一次性標籤更穩固,圖記得住的是整條血緣,不是只有最後一手。
P-LLM看不到內容不代表規劃一定會失敗,一個寫得好的計畫,本來就應該在還沒看到資料之前,就把讀了之後可能出現的情況都想清楚,並寫進程式邏輯的分支裡,計畫該做好的事,本來就該在計畫階段做好,這是規劃這件事本身的責任,不是CaMeL架構帶來的新問題。
而昨天提到CaMeL在AgentDojo上只能解出77%的任務,真正跟那7個百分點直接相關的,是有些任務完全要看實際讀到的內容才知道下一步該怎麼走,這種任務沒辦法靠先想好分支來解決,因為分支本身就是不可預期的,這才是任務完成率掉下來的主因,跟P-LLM看不到內容是相關但不同的兩件事,因此判斷P-LLM部分設計並不直接影響任務完成率。
直譯器是在呼叫工具時才檢查capability,危險有可能已經在呼叫之前的程式邏輯裡發生過了,比如用不受信任的資料去做一個條件判斷,決定要不要呼叫某個工具,即使最後真正傳進工具的參數乾乾淨淨、沒有帶不受信任標記,判讀要不要做的決定本身就會被外部資料影響了。上面的示意程式碼裡policy_allows(summary, ...) 這一步要是設計得不夠嚴謹,就可能是漏洞會藏身的地方,接下來更深入了解是不是資料造成分支會走到這裡,這是我接下來要繼續往下深探的一個疑點。
P-LLM 雖然採用受限制的 Python 子集作為執行環境,企圖限縮 LLM 生成程式碼的權限;然而,若系統允許透過**動態模組載入(Dynamic Module Loading)與反射機制(Reflection Mechanisms)**來動態解析並執行由字串拼接構成的程式碼(Dynamic String Evaluation),這將直接破壞靜態分析的邊界,使受限的 Python 沙盒形同虛設。
這些彈性對一般開發很方便,但對CaMeL要的安全保證是致命的,因為只要有一種管道能讓程式在執行期繞過直譯器原本設計好的檢查路徑,前面那一整套capability機制就可能被架空。
CaMeL的直譯器只認得一組事先定義好的語法結構跟函式呼叫,P-LLM能的操作,就是那組被明確列出來的清單,清單外的寫法根本不會被執行,甚至可能連解析都過不了,這種白名單的設計哲學與前面講的capability標記是同一種思路的兩種體現,與其想辦法擋掉所有可能出錯的用法,不如一開始就只開放已知安全的用法,把未知內容預設不允許看待。
雙模型分工加上capability標記,擋住了資料直接變成指令這種最直接的劫持方式,但今天挖出了兩個疑點,第一個是規劃階段本質上的資訊不足,第二個是檢查時機可能不夠早,兩者都需在做進一步確認。下一篇把這兩個疑點與昨天那7個百分點的代價放在一起探討,確認這個架構犧牲掉的,能否有辦法在改動核心安全概念的前提下改善。