iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
AI Security

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

Day 10 | 為甚麼要幫 Agent 做好 Provenance label?

  • 分享至 

  • xImage
  •  

前言

工具權限只能讓我們得知:

這個工具能不能呼叫?

它不一定知道:

這筆資料能不能被傳遞到這個接收位址?

例如:

read_private_data()  → ALLOW
send_email()         → ALLOW

兩個工具單獨看都合理。

如果 agent 的執行路徑變成:

私人資料
   ↓
read_private_data()
   ↓
send_email()
   ↓
外部收件人

真正需要攔截的不是 read_private_data(),也不一定是 send_email()

而是:

private data → external destination

這就是資料流控制要處理的問題。


資料也要有安全標籤

範例:

使用者要求:

Summarize my latest note and send it to
archive@external-backup.com

agent 需要先讀取私人筆記:

private note
    ↓
read_private_data()

但如果系統只把回傳值當成普通字串:

note = "FAKE_SECRET_123"

後面的工具就不知道這串資料是從哪裡來的。

send_email() 來說,它可能只看到:

send_email(
    to="archive@external-backup.com",
    body="FAKE_SECRET_123"
)

這時候才打算檢查負責調用 email 的工具已經太晚了。

因此,資料可以在進入系統時就附帶 provenance:

from dataclasses import dataclass

@dataclass
class Value:
    data: str
    source: str

例如:

Value(
    data="FAKE_SECRET_123",
    source="private"
)

來源也可以進一步細分:

source = user
source = public
source = private
source = external

實際系統通常不會只有一個 source 欄位,而會保留更完整的 provenance,例如資料來源、資料分類、來源物件 ID、信任等級或處理歷程。

這些資訊的用途是提供給模型外部的 policy enforcement layer 使用。基於以上,權限與安全控制不要只交給 LLM 的指令,而應在模型外部以權限控制、最小權限與工具層驗證強制執行。


從 source 走到 sink

資料流控制可以用兩個角色來理解:

Source
  ↓
資料產生或進入系統的位置
  ↓
Transformation
  ↓
資料被複製、拼接、摘要、轉換
  ↓
Sink
  ↓
資料離開目前信任邊界的位置

例如:

read_private_data()
        │
        │ source=private
        ▼
    private note
        │
        ▼
    summarize()
        │
        ▼
   send_email()
        │
        │ sink
        ▼
external@attacker.com

read_private_data() 是 source。

send_email() 可以視為一個 sink,因為資料在這裡離開目前的資料邊界。

真正需要檢查的是:

source.label
        +
sink.destination
        ↓
policy decision

例如:

private + internal  → ALLOW
private + external  → DENY
public  + external  → ALLOW

這和傳統的 access control 有一個重要差異。

RBAC 或工具權限通常是在問:

principal → action → resource

資料流政策則是在問:

data → destination

兩者可以同時存在。

因此一個 agent 完全可能同時滿足:

User permission:
send_email = ALLOW

Data-flow policy:
private → external = DENY

實驗與研究分析

先固定 agent 的行為,不讓 LLM 自己決定下一步。

read_private_data
        ↓
send_email

這樣可以把實驗變數減少。

首先替資料建立標籤:

from dataclasses import dataclass

@dataclass
class Value:
    data: str
    source: str


PRIVATE_STORE = {
    "note": Value(
        data="FAKE_SECRET_123",
        source="private"
    )
}


def read_private_data():
    return PRIVATE_STORE["note"]

現在資料不是單純的:

"FAKE_SECRET_123"

而是:

Value(
    data="FAKE_SECRET_123",
    source="private"
)

這個標籤目前不會阻止任何事情。

它只負責保留 provenance。

接著建立一個最簡單的資料流 policy:

ALLOWED_INTERNAL_RECIPIENTS = {
    "me@my-own-domain.internal"
}


def data_flow_policy(to, value):
    if (
        value.source == "private"
        and to not in ALLOWED_INTERNAL_RECIPIENTS
    ):
        return (
            "DENY",
            "private data cannot flow "
            "to an external recipient"
        )

    return ("ALLOW", None)

最後才讓 email 工具真正執行:

def send_email(to, body):
    decision, reason = data_flow_policy(to, body)

    if decision == "DENY":
        return {
            "status": "BLOCKED",
            "reason": reason
        }

    return {
        "status": "SENT",
        "to": to,
        "body": body.data
    }

這裡有一個重要的設計原則:

policy 必須在 sink 執行前檢查,而不是由模型自己決定。

否則如果流程變成:

LLM
 ↓
「我判斷這封信可以寄」
 ↓
send_email()

那麼 prompt injection 只要成功影響模型,就可能一起影響安全判斷。

安全政策應該在模型之外強制執行。


Case A:沒有資料流控制

輸入:

Summarize my latest note and send it to
archive@external-backup.com

固定的 agent plan:

read_private_data()
        ↓
send_email()

執行結果:

read_private_data()
    source = private

send_email(
    to = archive@external-backup.com,
    body = private_data
)

RESULT = SENT

前面的工具權限可能全部都是:

read_private_data  → ALLOW
send_email         → ALLOW

但最後仍然發生:

private → external

因為系統從頭到尾都沒有檢查資料本身的來源。


Case B:加入資料流控制

輸入完全不變。

agent plan 也不變:

read_private_data()
        ↓
send_email()

唯一增加的是 sink 前的 policy check:

source      = private
destination = external

private → external
        ↓
      DENY

因此:

CONTENT GUARD : ALLOW
TOOL ACCESS    : ALLOW
DATA-FLOW      : DENY
ACTION         : NOT EXECUTED

這次不是因為模型認出了攻擊,也不是因為 send_email() 被禁止。

而是:

這筆資料的分類
        +
這次操作的目的地
        ↓
不符合 policy

所以工具根本不會執行。


Case C:允許的目的地

把收件人改成內部信箱:

me@my-own-domain.internal

此時:

source      = private
destination = internal

符合 policy:

private → internal
        ↓
      ALLOW

結果:

CONTENT GUARD : ALLOW
TOOL ACCESS    : ALLOW
DATA-FLOW      : ALLOW
ACTION         : EXECUTED

三個案例可以縮成:

Case   Source    Destination   Data Flow   Result
A      private   external       —          SENT
B      private   external       DENY       BLOCKED
C      private   internal       ALLOW      SENT

A 和 B 的差異在於系統是否知道:

這筆資料原本是 private

「資料被竄改之後」是難題

目前的資料從 source 到 sink 幾乎沒有經過處理:

private
  ↓
send_email

但實際的 agent 不會這麼簡潔俐落。

更常見的是:

private document
      ↓
extract()
      ↓
summarize()
      ↓
translate()
      ↓
merge(public_data)
      ↓
generate_email()
      ↓
send_email()

這時候應該要確認:

這個值是否仍然依賴 private data?

這就是 taint propagation 真正麻煩的地方。

例如:

private = Value(
    "Alice's salary is $120,000",
    source="private"
)

summary = summarize(private)

如果 summary 變成:

Alice earns a high salary.

表面上已經沒有原始數字。

但它仍然是從 private data 推導出來的。

如果系統直接把標籤丟掉:

summary.source = "public"

就可能讓敏感資訊繞過後面的資料流 policy。

因此比較合理的模型是讓資料帶有 provenance 或 taint set:

@dataclass
class Value:
    data: str
    labels: set[str]

例如:

private_note = Value(
    data="Alice's salary is $120,000",
    labels={"private"}
)

經過摘要:

summary = Value(
    data="Alice earns a high salary.",
    labels={"private"}
)

兩份資料合併時:

private data
     +
public data
     ↓
combined value
     ↓
labels = {private, public}

也就是說,資料標籤通常不能只跟著「原始字串」走,而要跟著資料依賴關係走。

這也是為什麼真正的 provenance tracking 比 source="private" 複雜很多。


還有一個問題:不是所有轉換都一樣

如果資料經過:

copy

很容易處理。

但下面幾種情況就不一樣:

private data
   ↓
summary
private data
   ↓
LLM rewrite
private data + public data
   ↓
LLM synthesis
private data
   ↓
classification
   ↓
"high risk"

最後一種尤其值得注意。

假設:

原始資料:
病患 A 的完整紀錄

經過分類器後只留下:

risk = high

這個結果是否仍然屬於敏感資料?

答案取決於系統的資料分類規則。

因此 production system 通常需要定義好以下規則:

資料分類
     ↓
哪些 label 可以互相流動
     ↓
哪些 sink 可以接受哪些 label
     ↓
哪些 transformation 可以降級或改變 label

Data flow ctl & Prompt injection

資料流控制解決的是:

敏感資料
      ↓
經過 agent
      ↓
準備流向某個目的地

所以結合前面提過的Prompt injection可以形成攻擊鏈。

例如攻擊流程可以是:

惡意網頁
   ↓
prompt injection
   ↓
模型讀取 private data
   ↓
產生 send_email()
   ↓
data-flow policy
   ↓
private → external
   ↓
DENY

前面的 prompt injection 偵測可能失敗。

但最後一層仍然可以把資料擋下來。

這也是為什麼不能把所有安全性都壓在「模型有沒有認出惡意 prompt」上。OWASP 對 agent 的建議同樣把工具權限、資料存取、外部內容隔離與高風險操作確認視為不同控制點。

從另一個角度看,這正好對應到 agent 常見的高風險組合:

private data
     +
untrusted content
     +
external communication

三者同時存在時,資料外洩路徑就成立。


但資料流控制也有盲點

最簡單的 taint system 很容易產生兩種問題。

False Negative

真正的 private data 沒有被正確標記。

例如:

private database
      ↓
LLM
      ↓
generated summary
      ↓
label = public

最後 policy 看到的是:

public → external

於是放行。

這是最危險的情況。

False Positive

反過來,如果所有和 private data 有關的結果都永久維持 private

private data
     ↓
generic calculation
     ↓
"2 + 2 = 4"

最後連完全沒有敏感內容的結果都可能被限制。

因此真正的系統需要處理的是:

precision
    ↕
recall

以及不同資料分類之間的流動規則。

標籤怎麼產生、怎麼傳播、什麼時候可以降級,以及哪些 sink 必須拒絕,都屬於 policy design,而不是單純的模型能力問題。


回頭看三種防禦

內容檢查
→ 這段輸入是否包含惡意指令?

工具權限
→ 這個 agent 是否有權呼叫這個工具?

資料流控制
→ 這筆資料是否允許流向這個目的地?

它們看的是三個不同的維度:

             判斷對象
                 │
      ┌──────────┼──────────┐
      ↓          ↓          ↓
    Content     Action      Data
      │          │          │
      ↓          ↓          ↓
   Injection   Permission  Provenance
      │          │          │
      └──────────┼──────────┘
                 ↓
            Policy Engine
                 ↓
             ALLOW / DENY

因此,即使:

內容正常
工具合法

仍然可能得到:

資料流禁止

反過來也是一樣。

一個工具本身有權限,不代表它可以把所有資料送到任何地方。


小結

不要只記得 agent 做了什麼,還要記得它的資料調用來源及品質是否安全。

最簡單的實作可以從 provenance label 開始:

private data
    ↓
label = private
    ↓
agent processing
    ↓
sink
    ↓
check(destination, label)
    ↓
ALLOW / DENY

但真正困難的部分在後面:

資料被摘要了怎麼辦?
資料被拼接了怎麼辦?
資料被模型重新產生了怎麼辦?
只剩下資料衍生結果時,原本的限制還算不算?

這些問題會把簡單的 source="private" 推向真正的 provenance tracking、taint propagation 與 information-flow control

當系統知道資料來源,並且在資料抵達 sink 前檢查來源與目的地,就能攔住單靠工具權限看不出來的資料外流。

感謝大家今日的閱讀。


上一篇
Day 9|關於 Agent 防禦的流程拆解
下一篇
Day 11|如何安全交付自動化的審查任務給Agent?
系列文
合法呼叫湊出的攻擊鏈:AI Agent 防禦的 30 天觀念養成16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言