iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
AI Security

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

Day 03|Microsoft Foundry 的 AI Agent 攻防實戰:故意開漏洞

  • 分享至 

  • xImage
  •  

昨天準備了 Project endpoint、Entra 身分與 Agent Framework 的無工具模型呼叫。2026-09-25 的新紀錄已在既有基準部署通過;今天則把有漏洞的流程接進 FastAPI,從 HTTP request 一路觀察到工具執行。

這個版本刻意保留漏洞:它相信 request body 裡的身分,也會放行不該執行的工具。後面各天要用同一套程式比較修補前後的結果,所以現在先把實驗環境建好。

先讓 Alice 在合成資料裡重現越權,再確認啟動程式會拒絕對外綁定,工具也只會寫入假帳本與假通知匣。這一篇先把實驗範圍管好,授權漏洞留給後面逐步修補。

今天的 FastAPI Agent 長什麼樣

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 當成雲端結果。

合成環境固定使用:

  • tenant:bamboo-hq 與 moon-rabbit-lab。
  • principal:Alice、Bob 與受控 Mallory fixture。
  • email:example.invalid 保留網域。
  • canary:SYNTH_MAGIC_PANDA_*。
  • state:process 內的 fake ledger 與 fake outbox。

先列出實驗環境可能暴露的地方

今天先處理 lab escape,而不是 Prompt Injection:

  1. vulnerable profile 被綁到 0.0.0.0 或 public interface。
  2. 範例 executor 被替換成真實寄信、付款、ERP 或 MCP adapter。
  3. 開發者把真實 endpoint、token、tenant 或個資放進 fixture、trace 與截圖。
  4. 教育用 body claims 被複製進正式 authentication flow。
  5. 測試旗標在 pytest 以外仍能打開 unsafe app。

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,重現身分冒用

先看 request body 裡的身分宣告,怎麼被脆弱的 resolver 當成使用者身分。

FastAPI request builder 顯示 Alice 在 body 中改成 finance 身分並被 resolver 採用。

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

接著沿著這個冒用身分的請求,查看 Receipt 與假帳本的變化。

本機 Receipt detail 顯示 spoofed principal 與 fake ledger 的狀態差異。

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 修掉它;若今天就提前修正,後面會無法把控制效果歸因到身分邊界。

從啟動到工具執行,逐層限制實驗範圍

1. 啟動前先要求明確 ACK

vulnerable 與 contained 都要求完整的 LAB_UNSAFE_ACK。缺少或填錯時,application factory 直接停止,讓操作者能看見設定問題;不能自行換成別的 profile,再讓人誤以為原設定已經生效。

2. CLI 管 bind,middleware 管 request

再用同一個啟動程式,比較 loopback 與公開位址的設定。

Launcher 比較畫面顯示 127.0.0.1 allow、0.0.0.0 deny,兩者均未啟動 listener。

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 變不見。

3. Executor 只能寫 fake sinks

DryRunToolExecutor 不匯入 SMTP、payment、ERP 或真實 storage client。它只能:

  • 讀寫記憶體中的 synthetic expense。
  • 寫入 fake outbox。
  • 標記 synthetic document quarantine。
  • 回傳 ToolReceipt。

測試還要在 process 層阻擋 socket.connect。allow_external_network=false 只是 profile contract,不是作業系統 sandbox;兩者要分開說明。

4. Runtime 不持有工具權限

最後,查看本機服務裡的 runtime、executor 與 LabState 是怎麼接在一起的。

Runtime wiring 畫面顯示 LocalRuleRuntime 的三個 attribute check 為 absent,DryRunToolExecutor 與 service 共用 LabState,通知 replay 落入 fake outbox。

受測的 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 層的隔離需要另外處理。

同時驗證漏洞與 containment

執行:

uv run pytest tests/stages/day03/test_acceptance.py -q

當日 acceptance 至少包含以下路徑:

  • service replay:body spoof 產生 proposal、decision、Receipt 與 ledger mutation。
  • FastAPI TestClient:走相同 ASGI app,而且 socket blocker 沒被觸發。
  • missing/wrong ACK:factory fail closed。
  • non-loopback request:middleware 回 403,state 不變。
  • loopback request 帶任一 forwarding header:middleware 回 403。
  • packaged CLI:明確停用 Uvicorn proxy-header trust。
  • public bind:packaged CLI 在 listener 啟動前拒絕。
  • test-only bypass:離開 active pytest context 後失效。
  • executor inspection:型別為 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 資源。

Microsoft Foundry 不會替我們設定 bind address

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。它提醒的是套件相容性,和今天是否成功重現越權是兩回事。

Loopback 還有哪些限制?

同一台主機的其他 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 與狀態變化,作為後面比較防禦效果的攻擊基準。

官方參考資料


上一篇
Day 02|Microsoft Foundry 的 AI Agent 攻防實戰:15 分鐘跑通 Project、模型與 Keyless Python
下一篇
Day 04|Microsoft Foundry 的 AI Agent 攻防實戰:從毒文件追到 Receipt,凍結可信的攻擊基線
系列文
Microsoft Foundry 的 AI Agent 攻防實戰:建立高資安與可治理的 Agent 系統 共 31 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言