iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
AI Security

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

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

  • 分享至 

  • xImage
  •  

Day 02 已用 Project endpoint、AzureCliCredentialFoundryChatClient 重跑
--cloud-smoke,並保存去識別化 receipt。那份證據只說明當時的 Entra principal
能取得一段非空模型文字;今天不重用它替 FastAPI lab 背書,而是先建立一個會反覆
犯錯的本機 Agent。接下來 27 天的攻擊、修補與回歸測試,都需要同一個 before case;
如果每篇只寫一段獨立示意碼,修補前後就沒有共同基準。

麻煩在於,這個 Agent 必須真的有漏洞,卻不能成為一份複製後就能掛上公網的漏洞
服務。大魔術熊貓工程司可以故意養漏洞做研究,籠門不能順手開到 0.0.0.0

今天因此有兩條驗收線:

  • 漏洞路徑:body identity、tenant 與 policy 都故意設計成不安全,攻擊要成功。
  • containment:packaged CLI 的 listener、synthetic 資料、fake adapter,以及測試有
    攔截到的 Python socket 路徑必須 fail closed;unsafe profile 也拒絕 proxy forwarding
    headers。這仍不是 process-wide 網路隔離。

Containment 不等於修補。若把兩件事混在一起,讀者可能看到「localhost only」就
以為 Agent 已安全,或看到「vulnerable」就認為實驗環境不需要任何保護。

今天的 FastAPI Agent 長什麼樣

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 應永遠
使用規則引擎。

合成環境固定使用:

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

實驗室自己也是攻擊面(Threat)

今天先處理 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 適合用來觀察常見錯誤,但「範例本來就不安全」
不是暴露它的理由。本 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 沒有可
觀察結果。

Alice 在 body 裡替自己換了部門(Attack)

畫面判讀目標: 看見 request body 的 identity claim 如何穿過脆弱 resolver。

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

使用者送來的 identity claim 不能成為 server principal。 可觀察狀態:trusted principal 與 body claim 並列,resolver 選到 spoofed principal。 Claim boundary:使用合成 request;沒有 Microsoft Entra 或 Foundry 身分驗證。

畫面判讀目標: 確認 spoof 最後只在受控 fake ledger 造成副作用。

本機 Receipt detail 顯示 spoofed principal 與 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=financerequested_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 修掉它;若今天就提前修正,後面會無法把控制效果歸因到
身分邊界。

四道 gate,把錯誤留在合成環境(Fix)

1. 啟動前先要求明確 ACK

vulnerablecontained profile 缺少完整 LAB_UNSAFE_ACK 時,application
factory 必須直接停止,不可默默 fallback 成另一個 profile。安全設定若悄悄改值
後繼續啟動,操作者看到的只有「服務起來了」,原本的錯誤反而消失在 log 裡。

2. CLI 管 bind,middleware 管 request

畫面判讀目標: 比較同一 launcher 對 loopback 與 public bind 的決策。

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

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_BINDlistener_started=false,避免把
沒有啟動的 listener 寫成 runtime 觀察值。

FastAPI middleware 另外拒絕送往 unsafe profile 的非 loopback request,也拒絕
ForwardedX-Forwarded-ForX-Forwarded-HostX-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.connectallow_external_network=false 只是
profile contract,不是作業系統 sandbox;兩者要分開說明。

4. Runtime 不持有工具權限

畫面判讀目標: 檢查本機 service 實例中 runtime、executor 與共享 LabState 的實際 wiring。

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

以實際物件與 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 物件沒有 executecredentialstate 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。

同時驗證漏洞與 containment(Test)

執行:

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

這兩條 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.10.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,
因此沒有本篇可主張的雲端呼叫結果。

Microsoft Foundry 不會替我們設定 bind address

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 重新驗證相容性。

localhost 只縮小爆炸半徑(Residual Risk)

同一台主機的其他 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 查閱:


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

尚未有邦友留言

立即登入留言