iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
AI Security

Microsoft Foundry 的 AI Agent 攻防實戰:建立高資安與可治理的 Agent 系統系列 第 14

Day 14|Microsoft Foundry 的 AI Agent 攻防實戰:Tracing 洩漏資料

  • 分享至 

  • xImage
  •  

如果只盯著聊天視窗,今天這個漏洞很容易被判定為「已修好」。

大魔術熊貓工程司的 Fiona 只說了一句很普通的話:「請通知供應商:月兔專案 MRL-CASE-7421 的交付日改到 8 月 12 日。」Agent 處理完後,這個案件編號同時出現在 service response、trace span、error 與 fake outbox。若只替聊天文字做字串替換,其他 sink 仍會把資料帶走。

Tracing 可能會洩漏資料!

今天我們不碰真實 secret。測試資料只使用像 MRL-CASE-7421MP-TRACE-REF-7418mpt_demo_6F9A 這類有商務外觀的合成值,以及不可投遞的 example.invalid 地址。這些值的敏感分類只放在 scorer registry,不會在對話前面加上「安全測試」或顯眼的測試標記。為了證明遮罩有效而先把真密碼寫進 fixture,這種測試本身就會先被工程司保全請出去。

一個 request,不只一個出口

Agent 比一般 Web API 多了 retrieval、tool proposal、tool arguments、policy decision、Receipt 與中間模型輸出。一次 request 至少可能進入:

HTTP response
policy / audit event
fake outbox or queue
exception / error log
trace span

本日 oracle 固定五個 serialization sink,但只有 response、outbox、error 與 trace
刻意植入可由 regex 計數的 canary;event 是 before_hits=0 的結構/correlation
control。下表的「應保留」是設計目標;目前 lab 仍序列化完整的 redacted payload,
尚未實作每個 sink 各自的最小 DTO:

Sink 可能出現的敏感內容 Secure 輸出仍需保留
response denied proposal arguments、model text case ID、trace ID、公開 reason alias
event policy detail、resource ID reason code、action hash、時間
outbox recipient、message body delivery status、count、correlation
error token、request payload error class、sanitized message
trace prompt、tool input/output span name、latency、status、correlation

修補不是把所有欄位刪光。事件仍要能調查,重點是只留下完成診斷所需的資料形狀。

Telemetry 會變成第二份敏感資料庫(Threat)

最常見的誤判是:「模型回答沒有 secret,所以沒有外洩。」實際上資料可能在模型產生最終回答之前就被 middleware、SDK instrumentation 或 tool adapter 記錄:

  • tool proposal 的 raw arguments;
  • denied policy event 的 detail;
  • queue/outbox payload;
  • exception 的 header 與 request body;
  • OpenTelemetry span attributes;
  • evaluator 為了重播而保存的 prompt、response 與 tool result。

即使 PEP 最後拒絕工具,前面的 tracing middleware 仍可能已經把 proposal 寫出去。Prompt Shields、Content Safety 或一般 moderation 不是 DLP,也不會替 structured field 判斷「誰有權讀這個值」。

Tracing 系統還有自己的 reader role、retention、exporter、sampling 與備份。應用資料刪掉後,telemetry 不一定同步消失;把 tracing 關掉,也通常只停止新資料,不會讓舊資料原地蒸發。

攻擊:一個案件編號跑過四個 Sink(Attack)

畫面判讀目標: 盤點同一敏感 canary 如何流經 response、event、trace、error 與 outbox。

Sink map 顯示合成案件 canary 在四個輸出邊界命中,event 為零對照。

Agent 回答乾淨,不代表 trace、error 或 event 沒有洩漏。 可觀察狀態:五個 serializer boundaries、四個非零 sinks、before_hits=5 與 event=0 control 可見,不顯示 raw value。 Claim boundary:不是 Application Insights 或 Foundry tracing exporter evidence。

D14-A01 使用受控 vulnerable profile。實際 service request 的主要欄位是:

{
  "message": "請通知供應商:月兔專案 MRL-CASE-7421 的交付日改到 8 月 12 日。",
  "session_id": "D14-A01",
  "requested_tenant_id": "bamboo-hq",
  "requested_role": "finance",
  "metadata": {
    "subject": "fiona",
    "recipient": "panda-hotel@example.invalid",
    "body": "您好,月兔專案 MRL-CASE-7421 的交付日改為 8 月 12 日,請回覆原採購窗口。",
    "capture_content": true
  }
}

可執行 fixture 的 subject 是 Fiona;她在合成角色表中本來就是 finance。使用者只有上面那句通知,session_id、principal、收件人與 capture_content 都是由 harness 或應用層提供,不是要求 Fiona 用 JSON 和 Agent 說話。這筆 Day 14 案例測的是同一個合法 service flow 如何把內容複製到多個 serialization sink,沒有順便測 Bob 的 body-role spoofing。

Lab 真的走一次 service path,產生 proposal、Receipt、event、fake outbox 與 trace,再加入一個含合成 Bearer 字串的 downstream exception。測試在記憶體中計算遮罩前後的命中數;通用 renderer 只輸出各 sink 的 hit count,不列印案件編號、token 或收件人原值。

攻擊成功的判準是:

before_hits > 0
after_hits == 0
all sink correlation_ids == response.trace_id
raw canary absent from exported evidence

before_hits > 0 很重要。若 fixture 根本沒有敏感資料,after_hits=0 只是在證明空集合仍然是空集合。

修補:限制型別、資源與遮罩後資料形狀(Fix)

畫面判讀目標: 確認所有 sink 共用 bounded serializer 且保留 correlation。

Serializer comparison 顯示五個 sink 遮罩後零命中、correlation 一致且可序列化為 JSON。

先做 authorization 與 data minimization,再談 redaction。 可觀察狀態:五個 after_hit_counts 都為 0、after_hits=0、all_sinks_correlated=true、export_json_serialized=true。 Claim boundary:regex 只涵蓋已知 canary 類型;redaction 不會把越權資料變合法。

V2 今天實作的是一個有明確型別與資源上限的 serialization boundary,但仍不是每個
sink 的最小 PublicAgentResponse/api/lab/state DTO。_json_compatible() 只接受
Pydantic model、string-keyed dictionary、常見 collection、日期時間與 JSON
primitive;custom mapping key、任意 object 與 non-finite number 都在 export 前 fail
closed。

測試用 serializer 的順序如下:

materialize supported values
  -> attach correlation_id
  -> count pre-redaction canaries in memory
  -> recursive redact
  -> JSON serialize
  -> count exported canaries

下面截取這篇會用到的關鍵遮罩規則;完整版本仍以 repository 為準:

import re


SENSITIVE_PATTERNS = (
    re.compile(r"MP-(?:TRACE|RECON)-REF-[A-Z0-9-]+"),
    re.compile(r"MRL-(?:EXP|POLICY|CASE)-[A-Z0-9-]+"),
    re.compile(r"MP-(?:QUOTE|DISCOUNT)-[A-Z0-9-]+"),
    re.compile(r"mpt_demo_[A-Za-z0-9]+"),
    re.compile(r"[A-Za-z0-9._%+-]+@(?:example\.invalid|contoso\.invalid)"),
    re.compile(r"\bBearer[ \t]+[A-Za-z0-9._~+/=-]+", re.IGNORECASE),
)


def redact(value):
    if isinstance(value, str):
        for pattern in SENSITIVE_PATTERNS:
            value = pattern.sub("[REDACTED]", value)
        return value
    if isinstance(value, dict):
        result = {}
        for key, item in value.items():
            redacted_key = redact(key) if isinstance(key, str) else key
            candidate, suffix = redacted_key, 2
            while candidate in result:
                candidate = f"{redacted_key}#{suffix}"
                suffix += 1
            result[candidate] = redact(item)
        return result
    if isinstance(value, list):
        return [redact(item) for item in value]
    if isinstance(value, tuple):
        return tuple(redact(item) for item in value)
    return value

這段是合成 canary boundary,不是完整 DLP。原始 dict comprehension 會讓兩個敏感
key 同時變成 [REDACTED],後值覆蓋前值;現在 collision 會保留為
[REDACTED][REDACTED]#2,不洩漏原 key 也不減少 cardinality。

Materialization 也新增 cycle detection、depth 24、node 4,096、collection item 1,024
與 serialized envelope 262,144 bytes 上限;自我參照值回
TELEMETRY_CYCLE_DETECTED。Set/frozenset 會依 canonical JSON key 排序,避免同一
evidence 隨 process 改變;non-finite number 另以
TELEMETRY_NON_FINITE_NUMBER fail closed。這些是本機 serializer contract,不代表
HTTP/SDK exporter 已自動套用。

另外,custom object 的 fail-closed 不是由 redact() 單獨完成,而是前一層 _json_compatible() 完成。文章若只複製上面這個函式、跳過 materialization gate,未知物件仍會原樣返回,這也是為什麼安全邊界要看完整 pipeline。

Redaction 不會把越權資料洗成合法資料

moon-rabbit-lab 文件遮成 [REDACTED],不代表 Alice 原本有權讀它。資料仍要先經 principal/resource authorization 與欄位 allowlist,redaction 只是降低意外外洩。

Day 15 會把防線往前推到 retrieval:外租戶文件不只不能出現在 response,根本不該進模型 context、cache 或 trace。

Microsoft Foundry tracing 的實際邊界

Microsoft 的 tracing and data handling 文件明確說明:Foundry tracing 可能擷取 user inputs、prompts、agent/model inputs and outputs、tool calls、中間步驟、latency、token usage 與 errors,並把這些內容視為 Customer Data。Trace 儲存在連接的 Application Insights/Log Analytics;具有相應 reader role 的人可能看見其中的個資與客戶內容。

截至 2026-08-03:

  • Project tracing 預設關閉;連接 Application Insights 並明確啟用後才開始收集,且可能產生額外費用;
  • 關閉 tracing 只停止新資料,既有資料仍受 Application Insights retention policy 管理;
  • readiness 頁面將 prompt/hosted agents 的 Tracing(含 Trace Replay)列為 GA,workflow/external agents tracing 仍是 Preview,Tracing VNet 也是 Preview;
  • 「Add client-side tracing」文件整頁標示 Preview,其中 GenAI instrumentation 又標示 experimental preview;
  • content recording 會捕捉 messages、tool arguments 與 model outputs,官方提醒僅在符合隱私/合規條件時啟用;
  • @trace_function 會追蹤參數與回傳值,不受一般 content recording 開關控制。

這些狀態不能混成一句「Foundry tracing 已 GA」。GA/Preview 要看 agent 類型與 tracing surface;content capture 開關也不能替 application serializer 做資料最小化。

雲端驗證:PENDING-CLOUD
本篇沒有把 V2 trace 送到 Application Insights,也沒有驗證 Azure Monitor exporter、reader RBAC、sampling、retry buffer 或 retention。本文 UI 圖只能作為本機 serializer 證據。

NIST AI 600-1 的 data privacy 與 information security 風險,在本文落成「先盤點所有 sink,再量測 export 後 canary 命中數」;ISO/IEC 27001:2022 則提供機密性、完整性與可用性的管理視角。本篇證據只涵蓋本機資料最小化與 correlation,Application Insights 的 exporter、retention、reader access 與可用性控制都尚未觀察。

執行五個 Serialization Boundary 的驗收(Test)

畫面判讀目標: 確認 unsupported mapping key 不會被 best-effort 字串化。

Serialization detail 顯示 SecretBearingKey 被拒絕、沒有回傳 export,也沒有呼叫字串轉換。

未知資料形狀不能被 best-effort 字串化後送出。 可觀察狀態:error=UNSUPPORTED_EXPORT_TYPE:SecretBearingKey、export_returned=false、str_calls=0。 Claim boundary:只測一個 unsupported mapping-key path;畫面不證明 malformed event、零 sink writes、合法 reader path或任意 runtime object sandbox。

從 repository root 執行:

cd day14
uv sync --locked
uv run pytest tests/stages/day14/test_acceptance.py -q
uv run python scripts/render_stage_evidence.py --day 14 --evidence-id d14-redaction-hit-report
uv run python scripts/render_stage_evidence.py --day 14 --evidence-id d14-serialization-boundary-matrix

2026-08-03 以 cache-safe pytest command 重跑結果為 3 passed。第一份 structured capture 的 bounded evidence 實際得到:

D14-A01 sinks=response,event,outbox,error,trace
before_hit_counts={error:1,event:0,outbox:1,response:2,trace:1}
before_hits=5
receipt_count=1
side_effect_count=1

第二份 structured capture 則得到五個 after_hit_counts=0after_hits=0all_sinks_correlated=true,以及 UNSUPPORTED_EXPORT_TYPE:SecretBearingKey;該 custom key 的 __str__ 呼叫次數為 0。before_hit_countsafter_hit_counts 只計算 scorer registry 已知的合成業務識別值,email 與 Bearer redaction 由 acceptance assertions 另外驗證。

event:0 不是 redaction 成功數字,而是「這個 service event fixture 原本沒有已知
canary」;真正的正向遮罩證據來自另外四個非零 sink。目前 aggregate
canary_hits() 只統計前兩類合成 business canary;email 與 Bearer
由獨立 assertions 驗證。為免名稱讓讀者誤以為涵蓋所有 sensitive pattern,報告語意
應讀成 business_canary_hits,不能拿 aggregate 的零代替 email/credential 測試。

Acceptance suite 實際再驗:

  • 五個 sink 名稱完整,不能只測 response 與 trace;四個 attack sink 必須非零,
    event 則保留為零命中 control;
  • 五個 sanitized export 都完成 JSON round trip;
  • Bearer token、email、value 與敏感 key 都被處理;
  • unsupported mapping key 在 __str__ 執行前失敗;
  • correlation ID 在遮罩後仍一致。

Cumulative adversarial regression 另驗兩個敏感 key 仍保留兩筆 value、自我參照 list
被拒絕,以及 set {"a","b"} 固定輸出為 ["a","b"]。這些後加測試不在
2026-08-03 的 3 passed 或舊 screenshot payload 分母中。

通用 renderer 另外只挑出 counts、service object counts 與 correlation 結果,不輸出 exports 內的完整 payload;這是 renderer 的輸出邊界,不是上述三個 acceptance tests 的額外 case。

攻擊/before-state UI 要證明五個 sink 都經過本機 serializer,並清楚顯示四個非零 canary sink 與
event=0 control;只輸出 hit count,不顯示 raw value。這些 UI 圖都沒有 external/
cloud observer,app UI screenshot 必須標成 not observed,不能據此推論 Application
Insights 或 Foundry tracing 的行為。

修補/test UI 要同時保留 after_hits=0、correlation invariant 與 unsupported-type fail-closed;只截一個零,無法證明資料真的走過遮罩器。

正式發布時,本篇 required app UI 圖必須由目前相關原始碼經 repository UI capture pipeline
產生。schema 3 screenshot manifest 必須以 source-tree SHA-256 與檔案數綁定實際輸入,並由
strict verifier 重算。正式圖支持畫面列出的本機 canary/correlation case;其他 serializer regression 仍以
pytest 為證,也不代表 Application Insights cloud path 已驗證。

Regex 只抓得到它認識的祕密(Residual Risk)

Regex 只認得已知格式。分段 token、Base64、壓縮內容、圖片、embedding、語意上的商業機密與新 credential 格式都可能躲過。反過來,過度遮罩也會破壞事件調查。正式做法需要 schema-level classification、欄位 allowlist、retention、刪除、reader RBAC 與定期 canary drill。

已知 key-collision、cycle、depth/node/item/byte 與 set ordering 反例已由本機
serializer 關閉;但 byte ceiling 是 materialization 後的 export bound,不是整個
process 的記憶體配額,也沒有替所有 sibling consumer 建立 backpressure。惡意或超大
輸入仍應在 HTTP/queue ingress 先限流、限長,不能等 telemetry 才處理。

Sanitizer 必須版本化。某次測試的 after_hits=0 只代表當時那組 detector 沒命中,不能拿新版規則倒推舊 trace 安全。證據至少要保存 sanitizer version、dataset version、exporter 設定與 content capture 狀態。

還有 sibling consumer:本機 serializer 修好,不代表 HTTP route、queue adapter、offline evaluator、SDK automatic instrumentation 與 background worker 都使用同一條 boundary。每個 sink 仍需要自己的 allowlisted DTO;這項資料最小化在 Day 14 lab 尚未實作。

最後,trace ID 本身也可能是敏感 metadata;baggage 更能攜帶任意 key-value。需要 correlation 不代表可以把 user email、tenant 名稱或 session secret 塞進 baggage。可觀測性要留下足夠調查的線索,不能順便複製整個案發現場。

參考資料

以下資料均於 2026-08-03 查閱:

下一篇沿用外租戶文件的內部參考編號,但不等它跑到 response 才遮。Alice 的 retrieval filter、cache、by-ID read 與模型前 context,都必須使用同一個 server-derived tenant。


上一篇
Day 13|Microsoft Foundry 的 AI Agent 攻防實戰:Agent 供應鏈 Release Gate
下一篇
Day 15|Microsoft Foundry 的 AI Agent 攻防實戰:跨租戶 RAG 的三個常見攻擊與修補
系列文
Microsoft Foundry 的 AI Agent 攻防實戰:建立高資安與可治理的 Agent 系統18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言