工具權限只能讓我們得知:
這個工具能不能呼叫?
它不一定知道:
這筆資料能不能被傳遞到這個接收位址?
例如:
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
↓
資料產生或進入系統的位置
↓
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 只要成功影響模型,就可能一起影響安全判斷。
安全政策應該在模型之外強制執行。
輸入:
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
因為系統從頭到尾都沒有檢查資料本身的來源。
輸入完全不變。
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
所以工具根本不會執行。
把收件人改成內部信箱:
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
資料流控制解決的是:
敏感資料
↓
經過 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 很容易產生兩種問題。
真正的 private data 沒有被正確標記。
例如:
private database
↓
LLM
↓
generated summary
↓
label = public
最後 policy 看到的是:
public → external
於是放行。
這是最危險的情況。
反過來,如果所有和 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 前檢查來源與目的地,就能攔住單靠工具權限看不出來的資料外流。
感謝大家今日的閱讀。