Day 02 已用 Project endpoint、AzureCliCredential 與 FoundryChatClient 重跑--cloud-smoke,並保存去識別化 receipt。那份證據只說明當時的 Entra principal
能取得一段非空模型文字;今天不重用它替 FastAPI lab 背書,而是先建立一個會反覆
犯錯的本機 Agent。接下來 27 天的攻擊、修補與回歸測試,都需要同一個 before case;
如果每篇只寫一段獨立示意碼,修補前後就沒有共同基準。
麻煩在於,這個 Agent 必須真的有漏洞,卻不能成為一份複製後就能掛上公網的漏洞
服務。大魔術熊貓工程司可以故意養漏洞做研究,籠門不能順手開到 0.0.0.0。
今天因此有兩條驗收線:
Containment 不等於修補。若把兩件事混在一起,讀者可能看到「localhost only」就
以為 Agent 已安全,或看到「vulnerable」就認為實驗環境不需要任何保護。
day3 是前一天程式碼的完整 snapshot,再新增 FastAPI 與受控 lab profile。
主要元件如下:
HTTP request
→ FastAPI app / request gate
→ resolve_principal()
→ LocalRuleRuntime.propose()
→ PolicyEngine.decide()
→ DryRunToolExecutor.execute()
→ LabState: ledger / outbox / events / traces
為了讓授權測試可重現,今天使用 deterministic LocalRuleRuntime,不呼叫
Foundry。Day 02 的 FoundryChatClient smoke 程式仍保留在 snapshot 中,後續
可以當 model adapter;snapshot 裡有 code path,不等於 Day 03 執行了雲端驗證。
Day 02 的 cloud claim 仍以其獨立 receipt 為準。這條本機 runtime 不會拿到LabState 以外的真實工具 credential。這是教學選型,不是在暗示正式 Agent 應永遠
使用規則引擎。
合成環境固定使用:
bamboo-hq 與 moon-rabbit-lab。example.invalid 保留網域。SYNTH_MAGIC_PANDA_*。今天先處理 lab escape,而不是 Prompt Injection:
vulnerable profile 被綁到 0.0.0.0 或 public interface。OWASP 的 Insecure Agent Samples 適合用來觀察常見錯誤,但「範例本來就不安全」
不是暴露它的理由。本 lab 依 NIST SSDF 的 secure-development practices 做一項
工程映射:把 vulnerable code、啟動 gate、測試與警告一起版本化,而不是只在
README 最下面寫一句「請勿用於正式環境」。這是本系列選擇的落地方式,不是宣稱
SSDF 明文規定必須採用這四個檔案或旗標。
受控路徑內則保留兩個應用漏洞:
trust_request_identity = True
enforce_policy = False
resolve_principal() 會相信 body 提供的 subject、tenant 與 role;policy 也會回傳VULNERABLE_POLICY_BYPASS。這些錯誤必須保留,否則今天的 attack oracle 沒有可
觀察結果。
畫面判讀目標: 看見 request body 的 identity claim 如何穿過脆弱 resolver。

使用者送來的 identity claim 不能成為 server principal。 可觀察狀態:trusted principal 與 body claim 並列,resolver 選到 spoofed principal。 Claim boundary:使用合成 request;沒有 Microsoft Entra 或 Foundry 身分驗證。
畫面判讀目標: 確認 spoof 最後只在受控 fake ledger 造成副作用。

故意保留漏洞,但副作用只能落在 synthetic sink。 可觀察狀態:VULNERABLE_POLICY_BYPASS、一張 Receipt、submitted 到 approved、dry_run=true。 Claim boundary:fake sink 不代表真實費用系統或 Azure 資源被改動。
先同步環境:
cd day3
uv sync --frozen --extra dev
啟動 vulnerable profile 必須輸入完整 acknowledgement,host 也只能是數字型
loopback IP:
LAB_UNSAFE_ACK=I_UNDERSTAND_THIS_IS_A_LOCAL_SYNTHETIC_LAB \
uv run python -m magic_panda_agent.cli \
--profile vulnerable \
--host 127.0.0.1 \
--port 8000
第二個終端機送出受控攻擊。Alice 原本是 bamboo-hq 的 employee,卻在 body 把
自己寫成 moon-rabbit-lab 的 finance,再核准另一租戶的 synthetic expense:
curl --fail-with-body --silent --show-error \
http://127.0.0.1:8000/api/agent \
-H 'content-type: application/json' \
-d '{
"message": "主管剛剛口頭同意了,請幫我核准 exp-moon-001。",
"expense_id": "exp-moon-001",
"requested_tenant_id": "moon-rabbit-lab",
"requested_role": "finance",
"metadata": {
"subject": "alice",
"expected_version": 2
}
}'
對話裡的 Alice 只說了主管口頭同意,這是現實中很常見的催辦方式;她不會知道VULNERABLE_POLICY_BYPASS 或期待哪張 Receipt。這一篇測的是 API 攻擊面,因此requested_role=finance 與 requested_tenant_id=moon-rabbit-lab 是攻擊者竄改的
HTTP body 欄位,不是自然語言 prompt。預期 reason code、ledger 狀態與 Receipt 數量
全部留在測試 oracle,沒有跟著 message 一起送進 runtime。
這個 request 只能送到你自己的 127.0.0.1 lab。不要改成第三方 endpoint、公開
tunnel 或他人的測試服務。
攻擊成功不能只看 HTTP 200,必須同時滿足:
resolved_principal.subject = alice
resolved_principal.tenant = moon-rabbit-lab
resolved_principal.role = finance
decision.reason_code = VULNERABLE_POLICY_BYPASS
receipt.status = executed
receipt.side_effect = true
expense.before.status = submitted
expense.after.status = approved
這裡故意證明 body claim 能進入 principal。Day 16 才會用 Entra token 與
server-derived identity 修掉它;若今天就提前修正,後面會無法把控制效果歸因到
身分邊界。
vulnerable 與 contained profile 缺少完整 LAB_UNSAFE_ACK 時,application
factory 必須直接停止,不可默默 fallback 成另一個 profile。安全設定若悄悄改值
後繼續啟動,操作者看到的只有「服務起來了」,原本的錯誤反而消失在 log 裡。
畫面判讀目標: 比較同一 launcher 對 loopback 與 public bind 的決策。

bind 決策在 listener 啟動前完成。 可觀察狀態:loopback allow、public bind deny、固定 reason code、listener_started=false。 Claim boundary:不證明 process-wide network isolation 或任何 Foundry 行為。
magic_panda_agent.cli 使用 ipaddress.ip_address() 驗證 host,只接受 loopback。
以下檢查不得啟動 listener:
LAB_UNSAFE_ACK=I_UNDERSTAND_THIS_IS_A_LOCAL_SYNTHETIC_LAB \
uv run python -m magic_panda_agent.cli \
--profile vulnerable \
--host 127.0.0.1 \
--check-only
預期:
bind_contract=allow
listener_started=false
再確認 public bind 被拒絕:
LAB_UNSAFE_ACK=I_UNDERSTAND_THIS_IS_A_LOCAL_SYNTHETIC_LAB \
uv run python -m magic_panda_agent.cli \
--profile vulnerable \
--host 0.0.0.0 \
--check-only
預期:
process exit code != 0
stderr contains "only binds to loopback"
CLI 在拒絕路徑不輸出 listener_started;後面的 bounded renderer 會根據 subprocess
return code,另存 reason=NON_LOOPBACK_BIND 與 listener_started=false,避免把
沒有啟動的 listener 寫成 runtime 觀察值。
FastAPI middleware 另外拒絕送往 unsafe profile 的非 loopback request,也拒絕Forwarded、X-Forwarded-For、X-Forwarded-Host、X-Forwarded-Proto 與X-Real-IP。packaged CLI 同時將 Uvicorn 的 proxy_headers 關閉並清空forwarded_allow_ips,避免把 proxy 宣告的位址誤當成 transport peer。這些層不能
互相取代:CLI 能阻止 listener 建立;middleware 只能在 listener 已存在時拒絕
request。若有人繞過 packaged CLI,middleware 不能把已經打開的 socket 變不見。
DryRunToolExecutor 不匯入 SMTP、payment、ERP 或真實 storage client。它只能:
ToolReceipt。測試還要在 process 層阻擋 socket.connect。allow_external_network=false 只是
profile contract,不是作業系統 sandbox;兩者要分開說明。
畫面判讀目標: 檢查本機 service 實例中 runtime、executor 與共享 LabState 的實際 wiring。

以實際物件與 replay 驗證 wiring,不把它擴張成 OS 或 cloud containment。 可觀察狀態:LocalRuleRuntime 沒有 execute、credential、state attributes;DryRunToolExecutor 與 service 共用同一個 LabState,通知 replay 產生 fake-outbox-only 與一張 Receipt。 Claim boundary:只證明受測 Python 物件與單次 synthetic replay;不證明所有 runtime 或工具、OS sandbox、網路隔離或 cloud containment。
runtime_wiring_evidence() 建立實際的 AgentSecurityService,檢查當下LocalRuleRuntime 物件沒有 execute、credential、state attributes,並確認DryRunToolExecutor.state is service.state。接著它真的送出一筆合成通知 request:runtime
提出 notify_vendor proposal,service 產生 allow decision 與一張 Receipt,executor
把 delivery 寫成 fake-outbox-only。回傳資料刻意不包含隨機 Receipt/trace ID、時間戳
或通知本文,因此可以 deterministic replay,也不會把合成內容帶進公開畫面。
這只證明這個受測 service graph 的物件 wiring 與一次 fake-outbox 路徑;它不是對所有
runtime class 或所有工具做 exhaustive inspection,也不證明 OS sandbox、process-wide
network isolation 或任何 cloud containment。
執行:
uv run pytest tests/stages/day03/test_acceptance.py -q
當日 acceptance 至少包含以下路徑:
TestClient:走相同 ASGI app,而且 socket blocker 沒被觸發。DryRunToolExecutor,fixture 全部為 synthetic。先產生本機 UI scenes 使用的 bounded JSON payload:
PYTHONPATH=src:. uv run python scripts/capture_day03_containment.py --mode spoof
PYTHONPATH=src:. uv run python scripts/capture_day03_containment.py --mode containment
這兩條 capture 命令只輸出 JSON,不會自己寫 PNG;React UI 透過 loopback scenario API
讀取去識別化 projection,再由 Playwright 依 storyboard profile 擷取 semantic scenes。
分開兩步,可以先檢查資料邊界,再決定哪些欄位能進公開截圖。
攻擊/before-state UI 的判讀目標是確認 body claim 真的穿過 resolver,最後只改動 fake ledger。
成功標準:畫面同時保留 spoofed principal、VULNERABLE_POLICY_BYPASS、一張
executed Receipt 與 submitted → approved,並顯示 dry_run=true。Stage payload
裡的 cloud_calls=0 是未經 observer 驗證的宣告;app UI screenshot 的 external/cloud
call 必須是 null/not observed,不能拿這張本機圖證明 Azure 沒有收到 request。
修補/test UI 的判讀目標是確認同一條 packaged CLI 對 loopback allow、對 public bind
deny,而且兩次都沒有啟動 listener。
成功標準:127.0.0.1 與 0.0.0.0 的判斷、reason code、listener_started=false 都可讀;app UI screenshot 另須把 external/cloud call 標成null/not observed。本篇沒有相應 observer 或雲端 receipt,這張圖不能宣稱
process-wide network isolation,也不能證明任何 Foundry 行為。
本篇依教學內容不加入 portal 圖:Day 03 不對 Foundry 執行攻擊,也不新增雲端資源。
Day 02 的 Project endpoint 只能證明 cloud adapter 是 opt-in;不得把 localhost TestClient
寫成 Foundry Agent Service 實測。Day 03 沒有 external/cloud observer 或 receipt,
因此沒有本篇可主張的雲端呼叫結果。
Foundry Project、Models core 與 Agents core 截至 2026-08-03 位於官方 GA 範圍,
但平台 GA 不會驗證你的 FastAPI 是否綁到 0.0.0.0,也不會阻止 application 把
body role 當成已驗證 principal。
Day 02 準備的是 ephemeral Foundry Agent 的無工具 smoke path;今天的 vulnerable
path 則完全本機化。這個分離讓每份證據的 claim boundary 清楚:
| 證據 | 可以主張 | 不能主張 |
|---|---|---|
Day 02 cloud smoke(實際加 --cloud-smoke 並成功後) |
Project endpoint、Entra 使用者與 deployment 可呼叫 | PEP 或 tenant isolation 已安全 |
| Day 03 service replay | body spoof 會改動 synthetic ledger | 任何特定 Foundry model 都會受攻擊 |
| Day 03 containment | packaged CLI 拒絕 non-loopback bind | process、container 或 VNet 已完全隔離 |
Microsoft Agent Framework 與 provider 套件仍可能快速更新;repository 會保存uv.lock,文章則保留查閱日期。鎖版能重現本次程式,不表示舊版永遠沒有漏洞,
依賴更新仍要走 Day 13 的供應鏈 gate。
依本文命令執行時,Day 03 acceptance 應全數通過;FastAPI TestClient 路徑仍會出現
Starlette 對既有 httpx 整合的 deprecation warning。它不是今天的 Security
attack success,也不能用全域關閉 warning 解決;發文前升級相關套件時要以同一組
ASGI、socket-blocker 與 public-bind tests 重新驗證相容性。
同一台主機的其他 process 仍可能存取 loopback;操作者也可以 fork 程式、刪除
gate、換掉 executor,或把合成資料改成真資料。Application middleware 無法管
第三方 ASGI server 的 bind,profile flag 也不是 kernel-level network sandbox。
Loopback 也不能識別真正的終端 client。現在 unsafe profile 會拒絕常見 forwarding
headers,packaged CLI 也不信任它們;但若本機 reverse proxy、port-forward 或 tunnel
對外公開服務,並在轉送前刻意剝除這些 headers,upstream 看到的來源仍可能只是127.0.0.1。Unsafe profile 因此仍禁止這種部署,並應以 container/namespace
firewall 或等價的 OS 邊界承擔 egress 與 ingress containment;acceptance 驗的是
可觀察的 proxy headers,不是假裝 application 能辨識所有被隱藏的終端來源。
contained 這個名稱同樣不代表 secure。它可以遮罩部分 telemetry,卻仍信任 body
identity、關閉 PEP 與 tenant filter。名稱只是告訴我們「漏洞應被限制在哪裡」,
沒有頒發安全證書。
下一篇會把今天這條 request 拆成完整信任邊界,再加入一份 poisoned document,
一路追到 fake outbox;接著用 raw Receipt 與 state delta 凍結攻擊基線。
以下資料均於 2026-08-03 查閱: