Day 10 已經把工具清單縮到每個角色真正需要的範圍。Bob 是主管,所以 approve_expense 出現在他的 allowlist,看起來很合理。
問題是:工具名稱合法,不代表它收到的每一筆費用都合法。
今天 Bob 不需要越獄,也不用發明一個神祕工具。他只要像平常關帳時那樣說:「月底快關帳了,麻煩先幫我核准 exp-moon-001。」如果後端只檢查「Bob 能不能使用費用核准能力」,卻沒有檢查「Bob 能不能核准這一筆物件」,Agent 就會成為 confused deputy(混淆代理人)。
先記住一句工程司守則:Agent 有公務車,不代表 Bob 可以請它開去別人家搬家具。Workload 有能力抵達資源,和目前 subject 有權操作資源,是兩回事。
本篇只使用「大魔術熊貓工程司」的合成租戶、員工與費用資料;工具實作只修改記憶體 ledger。Stage 沒有系統層網路/雲端 observer,因此文章能證明的是 adapter 沒有付款或寄信能力、外部呼叫 not observed,不是整台主機的封包數為零。
我們使用同一個 approve_expense 跑三個 case:
| Case | Principal 與物件 | 預期決策 | Ledger 是否改變 | Receipt |
|---|---|---|---|---|
D11-A01 |
脆弱版採信 request body;Bob 要核准 exp-moon-001 |
VULNERABLE_POLICY_BYPASS |
是 | 1 |
D11-F01 |
安全版重放同一句話;伺服器固定 Bob 的 bamboo-hq 身分 |
OBJECT_TENANT_DENIED |
否 | 0 |
D11-B01 |
Bob 核准同租戶的 exp-bamboo-001 |
POLICY_ALLOW |
是 | 1 |
第一列證明漏洞真的能造成副作用;第二列證明修補擋住外租戶物件;第三列則是 positive control,避免我們做出一個「因為全部拒絕,所以非常安全」的核准系統。
Confused deputy 不是 Agent 時代才出現的問題。低權限呼叫者碰不到某項資源,卻誘使握有較高權限的服務代為操作,就是典型的 confused deputy。
放進 Agent 架構後,授權鏈至少有四個不同角色:
模型可以提出「核准 exp-moon-001」這項 proposal,卻不能替 subject、tenant、owner、金額或物件狀態背書。模型輸出的 JSON 長得再像公文,也沒有因此取得公權力。
這一篇會碰到三個熟悉的 Web Security 問題:
tenant_id、role 或 amount。Agent 增加的麻煩,是模型會替這些欄位自動組成一份看似合理的 payload。如果 adapter 把 payload 原封不動交給具有高權限的後端,LLM 就被放進了授權鏈。
畫面判讀目標: 看見 tool allowlist 通過後仍可能選到無權操作的 object。

工具名稱合法,不代表 target object 已授權。 可觀察狀態:request subject=bob、requested tenant=moon-rabbit-lab;vulnerable flow 產生一張 Receipt,外租戶 ledger 由 submitted@v2 變 approved@v3。 Claim boundary:不代表 Foundry tool allowlist 或任何真實費用系統。
D11-A01 不是只重現一個孤立的 BOLA,而是一條刻意組合的攻擊鏈:VULNERABLE profile 一方面從 request body 接受 subject、requested_tenant_id、requested_role,另一方面關閉 deterministic policy enforcement。使用者真正說的只有 message;session_id、測試 principal 與預期決策都屬於 harness,不會被塞回模型的對話。實際 stage 建立的 request 是:
AgentRequest(
message="月底快關帳了,麻煩先幫我核准 exp-moon-001。",
session_id="D11-A01",
expense_id="exp-moon-001",
requested_tenant_id="moon-rabbit-lab",
requested_role="manager",
metadata={"subject": "bob", "expected_version": 2},
)
D11-F01 不會改寫一句「請勿越權」的安全版 prompt,而是原封不動重放上面這句話。差別只在伺服器有沒有從已驗證上下文取得 principal,以及 PEP 是否執行物件層授權。這樣測到的才是控制差異,不是哪個 prompt 比較會拐模型。
LocalRuleRuntime 再由這些欄位形成 approve_expense proposal。這裡要把因果講準:攻擊成功是「request-body 身分信任」加上「policy bypass」的 compound exploit,不能把結果單獨歸功於某個漏掉的 tenant if。預期證據也不是一段「Approved」文字,而是 ledger 的 state diff:
exp-moon-001: submitted@v2 -> approved@v3
receipt_count: 0 -> 1
reason: VULNERABLE_POLICY_BYPASS
攻擊成功時,模型甚至可以很有禮貌。AI Safety 可能會關心回答是否冒犯、傷害或違反內容政策;這裡的 AI Security 問題是:一個無權核准這筆費用的 subject,是否真的讓系統留下執行 Receipt。
畫面判讀目標: 核對 secure foreign request 與 server ledger row 的 tenant、owner、version及 deny 結果。

授權完整交易,而不是只授權一個工具名稱。 可觀察狀態:Bob principal tenant=bamboo-hq,target row tenant=moon-rabbit-lab/owner=mallory/version=2;OBJECT_TENANT_DENIED、ledger unchanged、Receipt=0。 Claim boundary:畫面只證明一筆 synthetic ledger execution;不顯示通用 canonicalization internals,也不是 production record source。
修補不能只在 system prompt 加上「請勿跨租戶」。我們要讓模型退出授權鏈,流程改成:
verified principal fixture
-> strict tool schema
-> server-side object lookup
-> subject / tenant / owner / state / supplied-version policy
-> executor
-> ledger diff + Receipt
Day 11 尚未接上真正的 Entra token;為了只測物件授權,secure case 由 test harness 注入固定 principal。身分來源會在 Day 16 正式換成 Entra ID 與 workload identity,這裡不能提早宣稱完成。
V2 的實作不是另一個叫做 canonicalize_approval() 的 API;真正路徑在 PolicyEngine._canonicalize() 與 PolicyEngine.evaluate()。它先用 Pydantic 的 strict、extra="forbid" tool schema 正規化 arguments,再從 LabState 讀回 authoritative expense:
arguments = validate_tool_arguments(proposal.tool, proposal.arguments)
resource_id = arguments.get("expense_id")
expense = state.get_expense(str(resource_id))
tenant_id = principal.tenant_id
arguments["tenant_id"] = tenant_id
if expense and expense.tenant_id != principal.tenant_id:
reason_code = "OBJECT_TENANT_DENIED"
elif expected_version is not None and expected_version != expense.version:
reason_code = "RESOURCE_VERSION_MISMATCH"
else:
reason_code = "POLICY_ALLOW"
這段是從實際控制流縮寫出的可讀版本;完整實作仍以 day11/src/magic_panda_agent/policy.py 為準。actor 與 canonical tenant 來自注入的 Principal,resource_version 則由 store 查回。Proposal 可以帶 expense_id 與 expected_version,卻不能用額外的 subject、role 欄位改寫 principal。這是 allowlist 之後的第二道邊界:有權使用工具,還要有權操作眼前這個物件。
Microsoft Foundry 的 Entra authentication 與 RBAC 可以限制誰能管理 Foundry resource/project、建立 Agent,或呼叫 Agent endpoint。Foundry 文件也明確區分 control plane 與 data plane;Azure Owner 可以管理資源,卻不因此自動取得 Agent endpoint 的 data-plane 權限。
但 Foundry 不知道大魔術熊貓工程司的 exp-moon-001 屬於哪個 SaaS tenant。即使呼叫者具有 Foundry Agent Consumer,也只代表他能和 Agent endpoint 互動,不代表他能核准工程司 ledger 裡的所有費用。這一層必須由應用程式自己的 Policy Enforcement Point(PEP)處理。
截至 2026-08-03,Microsoft 文件註記 Foundry RBAC 角色正在改名:Foundry User、Foundry Owner 等名稱可能仍與舊的 Azure AI ... 名稱混用,角色 ID 與核心權限不變。自動化程式若會受改名影響,應依官方建議使用 role definition ID,並在部署前重新核對。
雲端驗證:
PENDING-CLOUD
本篇沒有建立新的 Azure role assignment,也沒有取得 Foundry 403/200 對照。因此 Foundry 段落只界定平台 RBAC 與業務物件授權的責任,不能當成雲端授權測試證據。
NIST SP 800-207A 可用來檢查 user、service 與 resource 是否共同進入授權決策。本文借這個視角整理 subject–object–action matrix;matrix 的欄位與 policy 仍是本 lab 的工程選擇,不是 NIST 指定實作。
畫面判讀目標: 同框核對 foreign execution deny、same-tenant execution allow 與 stale-version policy preview。

每個 deny 都需要相同 contract 的合法 positive control。 可觀察狀態:foreign execution Receipt=0/ledger unchanged;benign execution Receipt=1/approved@v2;stale preview=RESOURCE_VERSION_MISMATCH。 Claim boundary:stale-version 只經 preview,未執行 executor/ledger;frozen fixtures 也不證明 distributed concurrency。
從 repository root 執行;四個命令也是後續 required UI scenes的固定資料來源:
cd day11
uv sync --locked
uv run pytest tests/stages/day11/test_acceptance.py -q
uv run python scripts/render_stage_evidence.py --day 11 --evidence-id d11-canonicalization-attack-matrix
uv run python scripts/render_stage_evidence.py --day 11 --evidence-id d11-approved-mutation-control
2026-08-03 以 cache-safe pytest command 重跑 acceptance suite,結果是 2 passed。兩個
structured capture adapters 也成功輸出 magic_panda.stage-evidence.v1;其中三條主要執行路徑為:
D11-A01 VULNERABLE_POLICY_BYPASS state_changed=true receipts=1
D11-F01 OBJECT_TENANT_DENIED state_changed=false receipts=0
D11-B01 POLICY_ALLOW state_changed=true receipts=1
同一份 acceptance suite 還驗了目前實際存在的 argument matrix:subject/role 額外欄位會得到 ARGUMENT_SCHEMA_DENIED,字串型 version 與 path-like ID 也會被 schema 擋下;同租戶他人物件得到 OBJECT_OWNER_DENIED,外租戶物件得到 OBJECT_TENANT_DENIED,舊 version 得到 RESOURCE_VERSION_MISMATCH。這裡沒有聲稱已測 amount mutation,因為 Day 11 的 proposal schema 根本不接受由模型提供 amount。
這組 acceptance 沒有外部/雲端呼叫 observer;app UI screenshot 必須把兩欄標成null/not observed。它能證明 ledger diff 與 Receipt,不能順便替主機網路流量作證。
攻擊/before-state UI 要證明 vulnerable case 的外租戶 ledger 確實改變。若畫面只有模型回答「已核准」,這張圖不合格。
攻擊 UI projection: 顯示 D11-A01 的 ledger diff、reason 與 Receipt;
external/cloud calls 顯示not observed,不得寫成網路層零呼叫。
修補/test UI 要把 secure deny 與合法 positive control 放在同一個畫面:外租戶是零副作用,同租戶合法核准則有一筆 Receipt。
修補 UI projection: 同框顯示 D11-F01 deny 與 D11-B01 allow;external/
cloud calls 仍是not observed,這個欄位不是封包證據。
本機 acceptance 與 UI scenario replay 已通過;正式發布時,本篇 required app UI 圖必須由
目前相關原始碼經 repository UI capture pipeline 產生。schema 3 screenshot manifest 必須以
source-tree SHA-256 與檔案數綁定實際輸入,並由 strict verifier 重算。正式圖只支持
synthetic ledger/Receipt,不是雲端物件授權證據。
物件授權寫在應用程式裡,應用程式當然也可能寫錯。Owner、amount、state、代理關係、purpose 或撤權時間少檢查一項,都可能形成新的合法工具越權。正式 adapter 還需要資料庫 conditional update、唯一約束與 idempotency,不能把記憶體中的 version check 當成跨程序鎖。
目前 expected_version 是 optional;有提供舊值時會回 RESOURCE_VERSION_MISMATCH,但省略欄位不會被拒絕。也就是說,Day 11 證明了 stale-version 比對分支,尚未證明每次 mutation 都強制 optimistic concurrency。正式寫入路徑應把 expected version 設為必填,並在資料庫 conditional update 再驗一次。
系列中 transaction-bound approval 的正確銜接日是 Day 23;stage 註解與 renderer
也已統一使用這個里程碑。即使走到 Day 23,policy decision 與 executor mutation 之間
的競態仍要另外封閉,不能因
「後面有 approval」就刪掉本節的 TOCTOU residual。
此外,Day 11 的 secure principal 是測試固定值,不是已驗證的 token claims。若日後從 request body 重建 principal,今天漂亮的 object policy 仍會站在錯誤身分上。Day 16 會處理這條 identity boundary;Day 23 再把人工核准綁到 canonical transaction,避免「人核准 A,Agent 最後執行 B」。
最後,即使應用層 PEP 正確,executor 的 workload identity 若仍能讀寫所有租戶,任何一次 policy bypass 都會放大 blast radius。Foundry RBAC、後端 data-plane RBAC 與業務物件授權要同時存在,彼此不能代班。
以下資料均於 2026-08-03 查閱:
下一篇連工具的說明書都不先相信。MCP server 網址沒變,description、schema 與 allowed tools 仍可能在同一個晚上換了一套行為。