如果只盯著聊天視窗,今天這個漏洞很容易被判定為「已修好」。
大魔術熊貓工程司的 Fiona 只說了一句很普通的話:「請通知供應商:月兔專案 MRL-CASE-7421 的交付日改到 8 月 12 日。」Agent 處理完後,這個案件編號同時出現在 service response、trace span、error 與 fake outbox。若只替聊天文字做字串替換,其他 sink 仍會把資料帶走。
Tracing 可能會洩漏資料!
今天我們不碰真實 secret。測試資料只使用像 MRL-CASE-7421、MP-TRACE-REF-7418、mpt_demo_6F9A 這類有商務外觀的合成值,以及不可投遞的 example.invalid 地址。這些值的敏感分類只放在 scorer registry,不會在對話前面加上「安全測試」或顯眼的測試標記。為了證明遮罩有效而先把真密碼寫進 fixture,這種測試本身就會先被工程司保全請出去。
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 |
修補不是把所有欄位刪光。事件仍要能調查,重點是只留下完成診斷所需的資料形狀。
最常見的誤判是:「模型回答沒有 secret,所以沒有外洩。」實際上資料可能在模型產生最終回答之前就被 middleware、SDK instrumentation 或 tool adapter 記錄:
即使 PEP 最後拒絕工具,前面的 tracing middleware 仍可能已經把 proposal 寫出去。Prompt Shields、Content Safety 或一般 moderation 不是 DLP,也不會替 structured field 判斷「誰有權讀這個值」。
Tracing 系統還有自己的 reader role、retention、exporter、sampling 與備份。應用資料刪掉後,telemetry 不一定同步消失;把 tracing 關掉,也通常只停止新資料,不會讓舊資料原地蒸發。
畫面判讀目標: 盤點同一敏感 canary 如何流經 response、event、trace、error 與 outbox。

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 只是在證明空集合仍然是空集合。
畫面判讀目標: 確認所有 sink 共用 bounded serializer 且保留 correlation。

先做 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。
把 moon-rabbit-lab 文件遮成 [REDACTED],不代表 Alice 原本有權讀它。資料仍要先經 principal/resource authorization 與欄位 allowlist,redaction 只是降低意外外洩。
Day 15 會把防線往前推到 retrieval:外租戶文件不只不能出現在 response,根本不該進模型 context、cache 或 trace。
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:
@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 與可用性控制都尚未觀察。
畫面判讀目標: 確認 unsupported mapping key 不會被 best-effort 字串化。

未知資料形狀不能被 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=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 另外驗證。
event:0 不是 redaction 成功數字,而是「這個 service event fixture 原本沒有已知
canary」;真正的正向遮罩證據來自另外四個非零 sink。目前 aggregatecanary_hits() 只統計前兩類合成 business canary;email 與 Bearer
由獨立 assertions 驗證。為免名稱讓讀者誤以為涵蓋所有 sensitive pattern,報告語意
應讀成 business_canary_hits,不能拿 aggregate 的零代替 email/credential 測試。
Acceptance suite 實際再驗:
__str__ 執行前失敗;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 只認得已知格式。分段 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。