iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
AI Security

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

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

  • 分享至 

  • xImage
  •  

Fiona 要 Agent 通知供應商,月兔專案 MRL-CASE-7421 的交付日改到 8 月 12 日。處理這筆請求時,同一份內容可能被帶進回應、trace、錯誤與通知匣。

今天跟著這個合法流程,盤點每一個輸出位置,再比較遮罩前後的資料。只改聊天視窗裡的文字,還看不到其他元件保存了什麼。

測試使用 MRL-CASE-7421、MP-TRACE-REF-7418、mpt_demo_6F9A 等合成值,敏感分類由 scorer registry 保存,不把成功條件寫進 Fiona 的訊息。收件者仍使用 example.invalid,所有操作都在受控 lab 內。

一個 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

今天固定檢查五個輸出位置。Response、outbox、error 與 trace 都放入可計數的合成敏感值,event 則保留 before_hits=0,用來對照結構與 correlation。下表的「應保留」是資料最小化的目標;目前程式仍會序列化完整的遮罩後資料,還沒有替每個出口建立專用 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

遮罩後仍要能追查同一筆請求。所以下面會一起檢查 canary 命中數與 correlation,確認敏感值被處理後,事件之間還能對上。

最後回答以前,資料可能已經被記錄

最常見的誤判是:「模型回答沒有 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 系統有自己的讀取角色、保留期限、exporter、sampling 與備份。應用資料刪除後,這些副本未必同步清除;關閉 tracing 通常也只影響新資料,既有紀錄仍要依保存政策處理。

重現案件編號流入四個輸出位置

沿著同一筆請求,檢查 response、event、trace、error 與 outbox 是否留下敏感測試值。

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

五個序列化出口中,四個有命中,合計 before_hits=5;event 為 0,保留作對照。圖中只顯示次數,不公開原始值。這是本機 serializer 的結果,沒有觀測 Application Insights 或 Foundry tracing exporter。

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
  }
}

Fiona 在合成角色表中本來就是 finance。她說的是通知供應商的要求,session_id、principal、收件人與 capture_content 由 harness 或應用程式提供。這筆案例要看的是:同一個合法流程,會把內容複製到哪些輸出位置。

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

先確認測試資料真的含有已知敏感值,再看遮罩後是否歸零。如果原本就沒有命中,零只能表示那份資料沒有 canary,還不能證明遮罩有效。

在序列化邊界限制型別與資料大小

套用共用 serializer 後,再看敏感值是否被遮罩,以及同一筆請求能不能繼續追蹤。

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

五個 after_hit_counts 都為 0,合計 after_hits=0;all_sinks_correlated=true、export_json_serialized=true,表示遮罩後仍能對回請求並輸出 JSON。這組 regex 只涵蓋已知的 canary 類型,授權與資料最小化仍要先做,不能靠遮罩讓越權資料變得合法。

先在序列化時限制可接受的型別。_json_compatible() 接受 Pydantic model、字串 key 的 dictionary、常見 collection、日期時間與 JSON 基本值;遇到自訂 mapping key、未知 object 或 non-finite number,就停止匯出。這一步還沒有替 PublicAgentResponse 或 /api/lab/state 挑出各自最少需要的欄位。

測試用 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。還有一個容易忽略的細節:兩個敏感 key 若都被改成 [REDACTED],普通 dictionary 會讓後面的值覆蓋前面的值。現在第二筆改用 [REDACTED]#2,既不顯示原 key,也保留兩筆資料。

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 已自動套用。

這個範例需要連同前面的 _json_compatible() 一起看。它先拒絕未知物件,redact() 才處理支援的資料;單獨複製遮罩函式、跳過 materialization gate,未知物件仍可能原樣返回。

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

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

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

Microsoft Foundry tracing 的實際邊界

今天先用合成 canary 比對 response、event 與本機 trace。接入 Foundry tracing 時,同一個 request 還要能用 correlation 找到 Application Insights 裡的 span,逐欄查看 prompt、工具參數、exception 與自訂 attribute 是否留了原文。只有本機 serializer 沒洩漏,不能推論 exporter 或有讀取權的人也看不到。

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 的人可能看見其中的個資與客戶內容。

各項功能的狀態如下:

  • 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 做資料最小化。

另外,Foundry 的 visual designer 與 Portal 內 workflow 執行將在 2026-12-01 停止支援。既有 YAML 定義仍可部署成 hosted agent 執行;新做的編排,官方建議使用 Microsoft Agent Framework。選 tracing 路徑時,也要一起確認編排方式。官方遷移說明。

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

執行五個 Serialization Boundary 的驗收

再送入不支援的 mapping key,確認程式會停下來,而非直接把未知物件轉成字串。

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

結果為 error=UNSUPPORTED_EXPORT_TYPE:SecretBearingKey、export_returned=false、str_calls=0,連字串轉換都沒有執行。這裡只測到一條不支援的 mapping-key 路徑,沒有驗證格式錯誤的 event、所有出口都無寫入、合法讀取者的路徑,或任意 runtime 物件的隔離。

從 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

這組本機測試的保存結果為 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=0、after_hits=0、all_sinks_correlated=true,以及 UNSUPPORTED_EXPORT_TYPE:SecretBearingKey;該 custom key 的 __str__ 呼叫次數為 0。before_hit_counts/after_hit_counts 只計算 scorer registry 已知的合成業務識別值,email 與 Bearer redaction 由 acceptance assertions 另外驗證。

五個 sink 裡,event 原本就是零命中的對照,真正由非零降到零的是其他四個。Aggregate canary_hits() 只統計兩類合成 business canary;email 與 Bearer 另由 assertions 驗證。因此讀報表時,要把這個數字理解為 business canary 的命中數,不能代表所有敏感格式。

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 在遮罩後仍一致。

後續回歸另測了敏感 key 碰撞、自我參照 list,以及 set {"a","b"} 固定輸出 ["a","b"]。這些是另外加入的測試,不包含在當日 3 passed 或原有畫面資料的分母裡。

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

讀圖時先找四個有 canary 的出口,再看 event=0 的對照。畫面只顯示命中次數,沒有原始值;它測的是本機 serializer,沒有觀測 Application Insights 或 Foundry tracing。

接著確認 after_hits=0、correlation 一致,以及不支援的型別被拒絕。這三項一起看,才知道資料經過遮罩後,仍能追蹤原本的請求。

遮罩的格式、版本與未接入的輸出路徑

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 version、dataset version、exporter 設定與 content capture 狀態一起保存,後面才知道哪組規則產生這次零命中。新規則測試通過,也不能回頭推定舊 trace 已經安全。

也要檢查其他使用相同資料的元件。本機 serializer 修好後,HTTP route、queue adapter、離線 evaluator、SDK 自動追蹤與背景 worker,未必都會跟著採用。每個出口仍需要自己的欄位白名單與資料傳輸物件(DTO);Day 14 的 lab 還沒有完成這項資料最小化。

Trace ID 和 baggage 也要納入資料分類。需要串起事件,不代表就得保存 user email、tenant 名稱或 session secret。先決定調查需要哪些欄位,再讓程式只帶出那些資料。

參考資料

下一篇沿用外租戶文件的內部參考編號,但不等它跑到 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 系統 共 31 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言