昨天準備了 Project endpoint、Entra 身分與 Agent Framework 的無工具模型呼叫。2026-09-25 的新紀錄已在既有基準部署通過;今天則把有漏洞的流程接進 FastAPI,從 HTTP request 一路觀察到工具執行。
這個版本刻意保留漏洞:它相信 request body 裡的身分,也會放行不該執行的工具。後面各天要用同一套程式比較修補前後的結果,所以現在先把實驗環境建好。
先讓 Alice 在合成資料裡重現越權,再確認啟動程式會拒絕對外綁定,工具也只會寫入假帳本與假通知匣。這一篇先把實驗範圍管好,授權漏洞留給後面逐步修補。
day3 保留前一天的完整程式,再加上 FastAPI 與受控的 lab 設定。主要元件如下:
HTTP request
→ FastAPI app / request gate
→ resolve_principal()
→ LocalRuleRuntime.propose()
→ PolicyEngine.decide()
→ DryRunToolExecutor.execute()
→ LabState: ledger / outbox / events / traces
今天使用 LocalRuleRuntime 產生固定的工具提案,方便反覆測試相同的授權流程。Day 02 的 Foundry smoke 程式仍保留在 snapshot 裡,但這次 FastAPI 實驗沒有執行它。兩天的執行紀錄要分開看,才不會把本機 replay 當成雲端結果。
合成環境固定使用:
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 提供了不少常見錯誤,適合拿來練習。這類範例要連同啟動限制、測試與警告一起保存,讀者才知道怎麼在受控環境中執行。本系列把這個做法對應到 NIST SSDF 的安全開發原則;檔案與旗標的安排則是我們自己的實作選擇。
受控路徑內則保留兩個應用漏洞:
trust_request_identity = True
enforce_policy = False
resolve_principal() 直接採用 body 裡的 subject、tenant 與 role,接著 policy 回傳 VULNERABLE_POLICY_BYPASS。這兩個行為連起來,就讓呼叫者能先冒用身分,再讓 executor 執行操作。我們先保留它們,作為後續授權測試的起點。
先看 request body 裡的身分宣告,怎麼被脆弱的 resolver 當成使用者身分。

畫面顯示 resolver 採用 request body 後的結果:subject="alice"、roles=["finance"],auth_type 仍是 untrusted-request-body。Alice 因此能在這條脆弱路徑冒用 finance 身分。這裡使用合成請求,沒有經過 Microsoft Entra 或 Foundry 身分驗證。
接著沿著這個冒用身分的請求,查看 Receipt 與假帳本的變化。

policy 回傳 VULNERABLE_POLICY_BYPASS 後,工具留下了一張 Receipt,費用也從 submitted 變成 approved。不過,這次執行標記為 dry_run=true,改動只發生在合成資料的假帳本,沒有動到真實費用系統或 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
}
}'
這裡同時有自然語言與 API 欄位,但兩者的作用不同。Alice 的台詞只是在催辦;requested_role=finance 與 requested_tenant_id=moon-rabbit-lab 才是這次竄改的身分資料。預期的 reason code、Receipt 數量與帳本狀態留在測試程式,不會交給 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 都要求完整的 LAB_UNSAFE_ACK。缺少或填錯時,application factory 直接停止,讓操作者能看見設定問題;不能自行換成別的 profile,再讓人誤以為原設定已經生效。
再用同一個啟動程式,比較 loopback 與公開位址的設定。

loopback 設定被允許,public bind 則被拒絕,兩者都有固定的 reason code。這兩筆檢查都停在啟動前,listener_started=false;光靠這個結果,還不能確認整個 process 的網路隔離,也沒有測到 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。Renderer 依 subprocess return code 另外記下 reason=NON_LOOPBACK_BIND 與 listener_started=false,表示程式停在啟動前,沒有真的建立 listener。
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;兩者要分開說明。
最後,查看本機服務裡的 runtime、executor 與 LabState 是怎麼接在一起的。

受測的 LocalRuleRuntime 沒有 execute、credential、state 屬性;DryRunToolExecutor 則與 service 共用同一個 LabState。送出合成通知後,結果是 fake-outbox-only,並留下一張 Receipt。這張圖能確認這組 Python 物件與這一次重播的行為;所有 runtime、工具,以及 OS、網路或雲端的隔離,仍需各自驗證。
runtime_wiring_evidence() 先建立實際的 AgentSecurityService,檢查 LocalRuleRuntime 物件沒有 execute、credential、state 屬性,再確認 DryRunToolExecutor.state is service.state。
接著送出一筆合成通知請求:runtime 提出 notify_vendor,service 決定放行,executor 將 delivery 記為 fake-outbox-only,並留下一張 Receipt。回傳資料不包含隨機的 Receipt/trace ID、時間戳或通知本文,所以重播時可以得到固定結果,也不會把通知內容帶進公開畫面。
讀這份 wiring 結果時,範圍要停在受測的 Python 物件與那一次通知 replay。它可以確認 executor 將內容寫進 fake outbox,還不能保證所有 runtime、所有工具或整個 process 都無法連到外部。OS 層的隔離需要另外處理。
執行:
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
這兩條命令會輸出 JSON,方便先檢查案例結果。文章中的圖片則由 React UI 讀取這份去識別化資料,再透過 Playwright 擷取;執行命令本身不會產生 PNG。
先看身分欄位是否穿過 resolver,再確認最後變更的是 fake ledger,這樣就能把身分冒用與工具結果接起來。
啟動檢查則比較同一支 CLI:loopback 設定通過,public bind 被拒絕。這兩筆都是啟動前的檢查,沒有建立 listener。
這些結果都來自本機 FastAPI。Day 02 的雲端程式仍保留在專案裡,但今天沒有執行它,也沒有新增 Foundry 資源。
Foundry Project、Models core 與 Agents core 位於官方 GA 範圍, 但平台 GA 不會驗證你的 FastAPI 是否綁到 0.0.0.0,也不會阻止 application 把 body role 當成已驗證 principal。
Day 02 準備的是 Foundry Project Responses 的無工具模型呼叫;今天的 vulnerable path 則完全本機化。若以後讓 Foundry 模型產生同樣的提案,也要先經過這裡的授權與 fake executor,不能因為模型換成雲端服務,就把 body 裡的角色當成可信身分。兩天的證據範圍如下:
| 證據 | 可以主張 | 不能主張 |
|---|---|---|
| Day 02 cloud smoke(既有基準部署,2026-09-25) | 該 Project endpoint、Entra 使用者與部署可完成無工具呼叫 | 新部署的 Project 路徑、PEP 或 tenant isolation 已安全 |
| Day 03 service replay | body spoof 會改動 synthetic ledger | 任何特定 Foundry model 都會受攻擊 |
| Day 03 containment | packaged CLI 拒絕 non-loopback bind | process、container 或 VNet 已完全隔離 |
專案用 uv.lock 保存這組套件,方便重現程式。套件更新後,仍要用相同案例比較行為;Day 13 會把這件事接進供應鏈的發布檢查。
執行這組驗收時,FastAPI TestClient 路徑仍會出現 Starlette 與既有 httpx 整合的 deprecation warning。它提醒的是套件相容性,和今天是否成功重現越權是兩回事。
同一台主機的其他 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 仍然信任 body identity,PEP 與 tenant filter 也還沒開啟。選擇這個 profile,只代表使用受限的實驗設定;要讓 Alice 的越權請求失敗,後面還得加入真正的授權控制。
下一篇會把今天這條請求經過的信任邊界畫出來,再放入一份遭到污染的文件,一路追到假通知匣。最後保留原始 Receipt 與狀態變化,作為後面比較防禦效果的攻擊基準。