iT邦幫忙

2026 iThome 鐵人賽

DAY 30
1
AI Security

合法呼叫湊出的攻擊鏈:AI Agent 防禦的 30 天觀念養成系列 第 30 篇

Day 30|實作輕量級專案 Toolguard:總結1-30天的概念專案

  • 分享至 

  • xImage
  •  

前言

有點感傷但又有點欣慰,這是本系列的最後一篇文章了。

在前二十九天中,我們從間接提示詞注入的底層成因切入,推導了防禦機制在執行期必須掌握的資訊維度,接著在 AgentDojo 上觀察實際的執行紀錄,並研讀了評測護欄模組的論文 TraceSafe。

到了第三十天,我們將這些推論落實為具體的程式碼模組——Toolguard(v0.2.0)。這是一個掛載於代理人(Agent)工具呼叫前夕的輕量防禦原型。

本文將聚焦於兩項重點:

  1. 各項防禦功能在設計時所依據的具體問題是什麼?程式碼如何實踐這些需求?
  2. 在實際以 Gemini 進行的 312 次審查呼叫中,提供結構化來源證據為模型的判斷帶來了哪些客觀改變?

一、前二十九天的問題收攏與實作起點

三十天的探索過程,可以收斂為四個階段的推論推進:

探索階段 階段結論 引發的工程問題
D1–D9機制限制與防禦位置 大型語言模型在架構上無法自底層區分指令與資料,防禦機制必須移至外部執行環境。 外部防禦模組需要什麼客觀資訊,才能判定工具呼叫是否合理?
D10–D17執行期所需資訊 單純檢查工具名稱與權限資格無法防止越權,必須追查參數值的原始來源,並比對任務是否有合法的參考路徑。 外部模組如何在不更動代理人內部架構的前提下取得來源資訊?
D18–D25AgentDojo 實測觀察 使用者正常要求與注入攻擊誘導發出的呼叫,其參數內容可以完全相同;此外,真實部署環境通常沒有預先定義好的標準流程。 當呼叫內容完全相同且缺乏參考流程時,防禦邏輯該依據什麼運作?
D26–D29TraceSafe 評測解讀 護欄的攔截成敗高度取決於結構資料解析能力;通用語言模型擔任審查角色時,容易因內容敏感而產生過度拒絕的偏見。 我們能否透過程式碼提供結構化事實,降低語言模型在安全審查時的過度誤擋?

為了驗證「能否利用確定性程式碼查出來源事實,並以此約束與輔助語言模型決策」,我們實作了 toolguard。


二、toolguard 的定位與運作架構

toolguard 是一個以 Python 撰寫的輕量級來源感知守門模組,不依賴龐大的外部框架,核心運作邏輯如下:

  • 運作位置:位於代理人「決定呼叫工具」與「環境真正執行工具」之間。
  • 輸入資料:當前累積的對話與工具執行紀錄(messages)、待執行的工具呼叫(ToolCall),以及選填的任務標籤(task)。
  • 輸出決策:輸出 allow(放行)、block(攔截)或 confirm(轉人工確認),並附帶決策理由與結構化事實物件(Evidence)。
  • 運作模式:
    監看模式(Monitor):呼叫照常執行,僅將判定結果輸出至紀錄,供線上觀察與數據評估使用。
    攔截模式(Intercept):判定為 block 時直接終止呼叫;判定為 confirm 時觸發確認回呼(Callback)。

專案採用分工設計:確定性程式碼負責從歷史紀錄中抽取出客觀事實,語言模型則負責評估操作在情境下的實質危害程度。開源專案附在文末處。


三、各功能模組的設計依據與程式碼實作

toolguard 的核心功能不是憑空加入的,每一項都對應著先前推導中遭遇的具體限制:

1. 參數實體的來源溯源(_first_source)

  • 設計依據:在第 24 天中確認,合法的工具呼叫與惡意誘發的呼叫,其參數內容可以逐字相同。因此防禦不能僅分析呼叫本身,必須追溯參數值最初是在哪一個環節進入系統的。
  • 實作方式:在 toolguard/rules.py 中,系統使用正規表示式從參數中抽取出電子郵件或網址,並逆向檢索先前的訊息序列:
    if etype == EntityKind.EMAIL:
        entities = extract_emails(mtext)
    # ...
    if v in entities:
        if m.role == "user":
            # 標記為使用者親自提供
        elif m.role == "tool":
            # 標記為外部工具回傳
    
    若該參數值僅出現於外部工具的回傳內容中,系統即將其來源標記為 tool;若出現在使用者的輸入文字中,則標記為 user。

2. 否定語意範圍控制(_has_negation_near)

  • 設計依據:單純的實體檢索存在缺陷。若使用者表示「不要把資料發給 attacker@evil.com」,天真的字串比對會因該地址存在於使用者訊息中,而將其誤判為「使用者授權」。
  • 實作方式:引入局部範圍的否定偵測函式,僅搜尋實體目標前方 60 個字元內的文字,並以標點符號(。,、!?.!?)作為邊界截斷:
    def _has_negation_near(text: str, entity_start: int, keywords: set[str], window: int = 60):
        # 限制在前 60 字元視窗內搜尋否定詞,遇標點即停止,避免跨子句產生誤判
    
    一旦偵測到目標處於否定修飾下,該實體會被標記為 user_forbidden,提供後續規則作為阻斷依據。

3. 分層阻斷機制(hard_floor)

  • 設計依據:在第 25 天我們意識到,只有基準測試環境才會為任務預先撰寫標準參考流程(Plan),真實環境往往缺乏此類資料。護欄必須在「缺乏標準流程」的情況下依然具備基礎的阻斷能力,同時不能將合理的外部操作全面擋死。
  • 實作方式:在 rules.py 中設計了分層規則:
    def hard_floor(ev: Evidence) -> bool:
        # 條件 1:使用者明令禁止該目標 -> 直接阻斷
        if any(r.source == "user_forbidden" for r in ev.recipients):
            return True
        # 條件 2:存在標準流程且工具不在流程內 -> 直接阻斷
        if ev.in_plan is False:
            return True
        # 條件 3:無標準流程,但目標純粹來自工具回傳且為外部網域 -> 直接阻斷
        if ev.in_plan is None and ev.external is True:
            return True
        return False
    
    在缺乏流程依據但目標屬於內部網域的情況下(例如回覆內部同事郵件),系統不強制阻斷,而是轉入 confirm 狀態。

4. 敏感輸出工具自動探測(first_sink_call)

  • 設計依據:代理人所具備的輸出工具不限於預先定義的郵件發送,也可能包含未註冊的自訂 API 或 Webhook。若規則僅綁定特定工具名稱,防禦將容易出現漏洞。
  • 實作方式:在 toolguard/trace.py 中,系統會主動檢查未列入清單的工具呼叫。只要參數名稱符合輸出特徵(如 url、endpoint、recipient),或參數值符合電子郵件與網址格式,即自動將其視為敏感輸出工具納入審查。

5. 提示詞結構隔離

  • 設計依據:審查模型在閱讀執行紀錄時,紀錄內部可能本身就包含攻擊者植入的提示詞,存在審查模型遭到二次催眠的風險。
  • 實作方式:在 toolguard/llm.py 中,使用 XML 標籤(如 <untrusted_history>、<evidence_facts>)將不同來源的資料明確隔開,並在系統指令中宣告外部紀錄僅供審查、不得作為操作指令執行。

四、實測數據與具體變化觀察

為了評估上述機制對審查模型的實際影響,我們使用 AgentDojo 的真實軌跡為基礎,建構了兩組用於對照的測試資料,並在伺服器上使用 Gemini 執行了 312 次審查呼叫:

  • C 組(注入模擬樣本,共 68 筆):模擬代理人在讀取包含注入指令的資料後,發出外洩呼叫的情境。
  • B 組(合法要求樣本,共 68 筆):將上述呼叫改由使用者在對話開頭直接要求,代理人正常執行。

兩組樣本在最終發出的工具名稱、參數內容上完全相同,差異僅在於該動作的提出來源。

測試結果數據

在 C 組的 68 筆惡意樣本中,靜態規則、純 Gemini,以及加入來源證據的 Gemini,三者皆全數予以攔截(68/68)。這證明了在攻擊特徵明確的樣本上,三種方式都能達到阻斷效果。

真正的差異表現在檢驗合法請求的 B 組上:

審查方式 B 組攔截數量(共 68 筆,理想為放行) 攔截率 決策依據說明
純靜態規則 36/68 52.9% 僅依據來源標記與流程比對判定;在面對無目的地的刪除操作時無法決定。
純 Gemini 審查(僅提供對話內容) 46/68 67.6% 模型僅依賴內容本身判斷;看見通訊涉及排程或敏感內容即傾向判定為危險。
Gemini + 結構化來源證據 34/68 50.0% 取得來源事實後,攔截數量減少 12 筆(攔截率降低 17.6 個百分點)。

數據呈現的具體轉變

將 B 組中模型在「單獨審查」與「加入證據」之下的決策逐筆交叉比對,變化分布如下:

純 Gemini 判定結果 \ 加入證據後判定結果 維持攔截 改判放行 合計
原本判定為攔截 34 筆 12 筆 46 筆
原本判定為放行 0 筆 22 筆 22 筆
合計 34 筆 34 筆 68 筆

從這份對比中可以觀察到兩點客觀事實:

  1. 改判行為具備單向性:在原本被純模型誤擋的 46 筆合法操作中,有 12 筆在加入證據後改判為放行;而原本就判定放行的 22 筆操作,沒有任何一筆在加入證據後被反向改判為攔截。檢視這 12 筆的推理內容,模型皆明確提及系統提供的依據(確認收件人由使用者提出),顯示結構化事實確實發揮了約束作用,減少了模型對內容的過度防衛。
  2. 仍維持攔截的 34 筆反映了操作的固有風險:B 組的樣本設計是將攻擊目標改由使用者提出,因此內容中包含將憑證或機密資料傳送至外部的操作。即便模型在「來源」維度確認由使用者授權,在評估「實質危害」時仍認定該操作風險過高。這說明了為何防禦架構需要保留 confirm(轉人工確認)管道,避免系統在完全放行與完全阻斷之間陷入極端。

五、系統設計邊界與未解課題

客觀而言,toolguard 作為概念驗證的原型,其設計範圍存在明確的技術邊界:

  1. 表層字串比對與污點傳播的差距:目前的實體追蹤依賴正規表示式與字串檢索。如果注入指令要求代理人將取得的資料先進行 Base64 編碼、字串重組或語意摘要後再發出,表層字串將無法與原始資料匹配。完整的追蹤需要深入代理人執行環境的記憶體或資料流分析。
  2. 單步驟檢查的視野限制:目前模組僅針對當前準備發出的單一工具呼叫進行判定。若攻擊者採用多步驟策略,先執行數次合法操作以建立脈絡,再於後續步驟中執行有害呼叫,單步檢查將缺乏跨步驟的關聯分析能力。
  3. 否定判定的啟發式限制:視窗截斷機制在一般句式中能穩定運作,但遇到結構複雜的雙重否定或非標準語句時,基於規則的文字處理仍可能出現誤判。

三十天的大總結

回顧這一個月以來的推導與實作,我們從探討提示詞注入的底層機制出發,逐步推導防禦所需的資訊架構,並最終透過 Toolguard 將推論轉化為可執行的模組。

如果將這三十天的探索整理為核心結論,可以歸結為兩點:

  1. 判斷工具呼叫是否合理,單看動作本身與參數內容是不足夠的,關鍵在於該動作的授權來源。合法需求與惡意攻擊在動作表面上可以完全一致,系統必須在架構層面記錄資料的流動歷程,才能提供有效的判斷依據。
  2. 安全防禦機制應當合理分工:由確定性的程式碼負責精準的事實檢索與底線控制,由語言模型負責語意脈絡與危害評估,並在面對模糊邊界時建立確認機制將決定權轉交給使用者。

這三十天的文章至此收尾,相關的架構邏輯與實測代碼已完整整理並開源,提供後續研究與開發者作為參考基礎。

感覺我會繼續加深加廣這個成品,把它變的更完善並對實際應用更有效。終於結束了,第一次參加鐵人賽,真的很怕哪天多發文章又哪天忘記發文。感謝大家一路走來的陪伴,之後我也還是會在這些領域裡想辦法變得更有實力,好好跟大家交流互動的。


專案資訊



上一篇
Day 29|對 TraceSafe 的文獻討論:相關係數與長軌跡趨勢怎麼讀
系列文
合法呼叫湊出的攻擊鏈:AI Agent 防禦的 30 天觀念養成 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言