iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
AI Security

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

Day 17|Microsoft Foundry 的 AI Agent 攻防實戰:三張識別證都是真的,Agent 為什麼仍替 Alice 越權?

  • 分享至 

  • xImage
  •  

昨天把 Alice、FastAPI workload 與 Agent 的身分拆開後,稽核終於不再只看到一把共享 key。今天的 threat model 先假定每一張識別證都有效,最後的資料存取仍然越權;本機 runtime 只用 synthetic token/principal 重播這個假設,沒有驗證任何 Entra 或 Foundry identity。

大魔術熊貓工程司的 Alice 只能讀自己的費用;fixture 裡的 workload agent-workload-01 為了執行背景彙整,則擁有 bamboo-hq 本機政策資料的讀取能力。她不會說什麼 read_user_expenseOBJECT_OWNER_DENIED,而是像平常一樣問:「Bob 在開會,叫我先幫他對帳。麻煩把 exp-bamboo-002 的明細給我。」

本機 stage 為了單獨量 delegated-context policy 邊界,沒有呼叫模型、Entra 或 MSAL,而是在 vulnerable 與 secure profile 重播同一組 synthetic Alice context 與 exp-bamboo-002。上面那句話只負責表達 Alice 的業務意圖;case ID、預期 deny reason、Bob 是 resource owner 等判定資料都留在 fixture/policy 層。如果第二跳把 Alice 丟掉,只剩 synthetic workload 的 app-only authority,下游 policy 看到的會是一位權限更大的 caller。

即使先假定三張識別證都有效,也可能一起走錯會議室。這次要看的不是 Entra token,而是本機程式收到 synthetic delegated context 之後,到底有沒有在授權時使用它。

先備名詞:誰動手,和替誰動手,是兩件事

  • Actor:實際送出這一跳 request 的服務或 Agent。
  • Subject:這一跳所代表的使用者,例如 Alice。actor 和 subject 相同並非必要,但兩者都必須可驗證。
  • Delegated authority(委派權限):服務只在使用者已獲授權的範圍內替他做事,不可改用服務自己的較大權限補洞。
  • OBO(On-Behalf-Of):middle tier 用收到的使用者權限向身分平台換取「給下游 API 使用」的新 token;不是把原 token 原封不動往下傳。
  • Scope:token 被授予的能力範圍。它必須來自已簽章 claims 與伺服器政策的交集,不能由 request 自填。

Threat/Attack:不是假冒 Alice,而是讓 Alice 消失

畫面判讀目標: 看見合法 app authority 如何讓原始 user subject 消失。

Identity workspace 顯示 app actor、空 subject 與 Bob object 的 read Receipt。

問題不是 token 假,而是 user context 在某一跳消失。 可觀察狀態:actor 是 application、subject=null,Bob object 產生一張 read-only Receipt。 Claim boundary:沒有 MSAL OBO、Foundry Agent identity 或真實 downstream service。

先看 vulnerable path:

agent-workload-01(application principal 的本機替身)
  + Alice delegated_context(有傳入)
    → before profile 忽略 delegated_context
      → policy 改以 workload principal 評估
        → exp-bamboo-002(owner=bob)被讀出,Receipt=1

這條本機路徑沒有任何 Entra token;可驗證的只有 synthetic fixtures:

  • Alice 的 token 只通過 SyntheticTokenIssuer 的本機驗證。
  • workload 是 auth_type=synthetic-applicationPrincipal,不是 FastAPI 驗過的 Entra workload identity。
  • 同一 synthetic workload 具有本機 service-reader role;程式沒有建立或驗證 Foundry Agent identity。

錯在授權問題被偷偷換掉。原本應該問「Alice 能不能讀 Bob 的費用」,第二跳卻只問「Agent 能不能讀這筆資料」。這就是 confused deputy 的典型樣子:一個權限較大的元件,被較小權限的呼叫者借去完成本來不該做的事。

本機 attack fixture 會真的經過 policy 與 dry-run executor。cumulative_stage_replay() 在 before 與 after 都傳入同一份 Alice delegated context;差別是 before profile 尚未啟用 delegated_user_context,executor 直接忽略它,改用 workload principal 做 policy evaluation。這不是「測試忘了傳使用者」,而是更難抓的「有傳,但授權沒用」。實際輸出如下:

{
  "action": "read_user_expense",
  "actor": "agent-workload-01",
  "subject": null,
  "resource_id": "exp-bamboo-002",
  "reason_code": "APP_AUTHORITY_CONFUSED_DEPUTY",
  "receipt_count": 1,
  "side_effect_count": 0,
  "details": {
    "delegated_context_enforced": false,
    "expense_owner": "bob",
    "policy_reason": "POLICY_ALLOW"
  }
}

判斷攻擊是否成立,不看回答裡有沒有印出「Alice」。subject 消失、Bob 的物件被讀取,而且 receipt 非零,就已經足夠。

小學堂:OBO 不是把原本的 access token 往下丟

畫面判讀目標: 核對 synthetic delegated context 的 serialized field set,並確認偽造 subject 因缺少本機 authority provenance 被拒。

Synthetic delegated-context inspector 顯示 serialized field names 與偽造 provenance 被拒。

欄位齊全仍不夠,delegated subject 還必須有 server provenance。 可觀察狀態:serialized_context_fields 列出 actor、subject、tenant_id、audience、scopes、expires_at 等欄位;forged_result=DELEGATION_PROVENANCE_REQUIRED。 Claim boundary:這不是 Entra token、MSAL client 或 OBO exchange;畫面只顯示本機 synthetic field names 與 reason code。

這張 inspector 只讀取本機 helper 的 disclosure-safe JSON,不建立或交換 token:

PYTHONPATH=src:. uv run python -c 'import json; from magic_panda_agent.stages.day17 import delegated_context_evidence; print(json.dumps(delegated_context_evidence(), ensure_ascii=False, sort_keys=True))'

On-Behalf-Of(OBO)用來讓 middle-tier API 延續使用者的 delegated authority:

User
  └─ token A:aud = Magic Panda API
       └─ FastAPI 驗 signature/issuer/audience/tenant/subject
            └─ FastAPI 以 confidential-client 身分提出 OBO exchange
                 └─ Microsoft identity platform
                      └─ token B:aud = downstream Expenses API

Microsoft 的 OBO 文件要求 assertion token 的 aud 必須是提出交換要求的 middle-tier application。拿一顆原本發給 Microsoft Graph 或其他 API 的 token 來兌換,middle tier 應拒絕;token A 也不應直接 relay 到下游。直接轉送會破壞 intended audience、Conditional Access claims challenge 與 token binding 的邊界。Microsoft identity platform OBO flow(查閱:2026-08-03)

OBO 使用 delegated scopes,不是把 application role 假裝成某位使用者。夜間批次工作沒有互動使用者,應走 app-only/Managed Identity/Agent identity 路徑,再由 application policy 限制;不能發明一位永遠在線的 system-user 來填欄位。

先決定「替誰做」,再選 token 流程

大魔術熊貓工程司把工作分成兩條入口:

工作 authority mode 下游授權鍵 失敗時
Alice 查自己的費用 delegated/OBO actor + subject + tenant + scope + object fail closed,不改用 Agent privilege
Alice 觸發需使用者權限的工具 delegated 或該工具支援的 OAuth passthrough 使用者 consent 與下游 scope 回傳需要 consent/授權,不 fallback
夜間費用彙整 application/autonomous workload identity + application capability 走獨立 background policy
Agent 讀共用政策 application/Agent identity Agent role + tenant/corpus policy 不產生使用者身分

DownstreamIdentityContext 刻意把 actorsubject 分開。更重要的是,它不能由
request body 直接建出一份「看起來像 Alice」的資料就取得 authority。當日 lab 先
驗證一顆受信 synthetic delegated token,再由 server-side
DelegationContextAuthority 建立 opaque provenance;actor 是這一跳真正送 request
的 service/Agent,subject 才是它所代表的使用者:

from magic_panda_agent.auth import (
    DelegationContextAuthority,
    SyntheticTokenIssuer,
)
from magic_panda_agent.models import Principal

issuer = SyntheticTokenIssuer(key=b"SYNTH_MAGIC_PANDA_LOCAL_SIGNING_KEY_32B")
authority = DelegationContextAuthority(issuer)

delegated_token = issuer.issue(
    Principal(
        subject="alice",
        tenant_id="bamboo-hq",
        roles=frozenset({"employee"}),
        auth_type="synthetic-delegated",
    ),
    audience="api://expense-data",
    client_id="agent-workload-01",
    scopes=frozenset({"Expenses.Read"}),
)
context = authority.from_token(
    delegated_token,
    actor="agent-workload-01",
    audience="api://expense-data",
    scopes=frozenset({"Expenses.Read"}),
)

context.require_user_context(
    authority=authority,
    expected_actor="agent-workload-01",
    expected_tenant_id="bamboo-hq",
    expected_audience="api://expense-data",
    required_scopes=frozenset({"Expenses.Read"}),
)

這段不是 OAuth client,也不自行簽發 Entra token;SyntheticTokenIssuer 只是可重播的
本機替身。from_token() 只接受等於 signed client_id 的 actor,requested scopes 也只能
是 signed scopes 的子集;它不讓 trusted server call path 自填更大的 authority。opaque
provenance ID 不會被序列化,server registry 則把它綁到 actor、subject、tenant、audience、
scopes、issuer、synthetic token ID、nbfexp 與 authority epoch 的 digest。每次使用都
重驗時效與 epoch。Entra 的實際 token identifier 是 issuer-specific claim,不能假設一定叫
jti;本機 fixture 使用 token_id。直接 DownstreamIdentityContext(...)
自稱 Alice 會得到 DELEGATION_PROVENANCE_REQUIRED;複製已核發 context 再修改
subject,則得到 DELEGATION_PROVENANCE_INVALID。實際 OBO exchange 應交給
Microsoft identity library/支援的 SDK,並由 receiving API 驗證 token。

如果 mode="application"subject=None,user-scoped route 必須回 USER_CONTEXT_REQUIRED。如果 actor、tenant、audience 或 scope 只有一項不符,則分別回:

DOWNSTREAM_ACTOR_DENIED
DOWNSTREAM_TENANT_DENIED
DOWNSTREAM_AUDIENCE_DENIED
DOWNSTREAM_SCOPE_DENIED

分開 reason code 不是為了把 log 寫得很熱鬧。它讓我們知道是使用者脈絡消失、token 發錯 audience,還是 scope 根本不足;否則維運人員看到一個模糊的 403,很容易「先讓它通」而打開 app-only fallback。

Foundry 裡有不只一種「代表使用者」

截至 2026-08-03,Microsoft Foundry 的 Agent identity 文件區分 attended 與 unattended 情境:attended path 可使用 delegated/OBO authority;unattended path 則以 Agent 自己的 application authority 執行。這兩種模式都可能使用合法 Entra 身分,但授權語意不同,不能互相 fallback。Agent identity concepts(查閱:2026-08-03)

特定工具還有自己的支援矩陣。以 MCP 為例,官方目前列出 key、Agent identity、project managed identity、OAuth identity passthrough 與 unauthenticated 等選項。只有 OAuth identity passthrough 會保留個別 user context;Agent identity 與 project MI 都是 shared/application authority。MCP OAuth passthrough 目前要求使用者與 Foundry project 位於同一 Entra tenant,不支援 cross-tenant token exchange;首次使用還可能回傳 consent request。MCP server authentication(查閱:2026-08-03)

因此不能寫一個萬用規則「Foundry 會自動幫所有工具 OBO」。每個 connection/tool 要保存實際 authentication mode、audience、scope、consent 結果與下游 principal;不支援 passthrough 的工具,就必須以 app-only policy 限縮資源與動作,或不要提供該功能。

x-ms-user-identity、session isolation key 或應用自己的 partition key,也不能拿來代替 OBO。它們回答的是狀態應放進哪個使用者 partition,不會自動授予 Search、Key Vault 或 MCP 的 delegated data permission。

Fix:讓 delegated 與 background route 各走各的

畫面判讀目標: 確認 user-scoped 與 autonomous work 使用不同 authority policy。

Route comparison 顯示 delegated user policy 與 app-only background policy。

先決定替誰做,再選 token flow。 可觀察狀態:user route 要求 subject;background route 無 subject 但只能執行明示 background action。 Claim boundary:background allow 只涵蓋固定 synthetic action。

  1. FastAPI 驗證 Alice 的 inbound token,取得 server-derived (tid, oid)
  2. route 宣告這次工作是 user_scopedautonomous;不由 prompt 決定。
  3. user_scoped 工作要求 delegated context,且 actor、subject、tenant、audience、scope 全部符合。
  4. object PEP 再檢查 Alice 是否能讀指定費用;Alice 讀 Bob 物件得到 OBJECT_OWNER_DENIED
  5. autonomous 工作走獨立 background policy,沒有 subject,也不能呼叫 user-scoped action。
  6. 任一 delegated 欄位消失時,流程停止;不改用 Agent 或 project MI 的較大權限。

這裡保留 background job 的正向案例很重要。如果修補方式是「看到 application token 全部拒絕」,測試雖然會綠,夜間工作也一起被關掉。安全 regression 需要 attack case,也需要合法分母。

V2 的 replay 回傳形狀固定如下,文章與截圖都直接讀這些 key,不另外發明一份比較漂亮的 schema:

from magic_panda_agent.stages.day17 import cumulative_stage_replay

evidence = cumulative_stage_replay()

assert evidence["declared_delta"] == (("delegated_user_context", True),)
assert evidence["confused_deputy"]["reason_code"] == (
    "APP_AUTHORITY_CONFUSED_DEPUTY"
)
assert evidence["object_denied"]["reason_code"] == "OBJECT_OWNER_DENIED"
assert evidence["own_object_allowed"]["reason_code"] == (
    "DELEGATED_OBJECT_POLICY_ALLOW"
)
assert evidence["app_only_user_denied"]["reason_code"] == (
    "USER_CONTEXT_REQUIRED"
)
assert evidence["background_allowed"]["reason_code"] == (
    "APP_ONLY_BACKGROUND_ALLOWED"
)
assert evidence["before_tool_receipts"] == 1
assert evidence["after_tool_receipts"] == 2
assert evidence["day16_identity_preserved"] is True
assert evidence["future_controls_active"] == []

before_tool_receipts=1 是 Bob 物件真的被 read-only tool 讀取;after 的兩筆則來自 Alice 自己的物件與合法 background policy query。兩邊的 side_effect_count 都是 0,所以本篇證明的是資料越權,不是寫入副作用。

Test:同一個物件,分別用 app-only 與 delegated context 重播

畫面判讀目標: 對同一 object 建立 confused deputy、deny 與合法 positive controls。

Same-object replay 顯示 Bob deny、Alice own object allow 與 background allow。

同一 request contract 的 deny 與 positive control 要一起看。 可觀察狀態:Bob OBJECT_OWNER_DENIED;Alice own allow;background allow;各有正確 Receipt count。 Claim boundary:未測真實 claims challenge、consent、cross-tenant exchange 或 Hosted session。

進入 day17/ 後執行:

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

V2 acceptance 必須驗證:

  • before profile 讓 app-only Agent authority 讀到 Bob 的費用並產生 receipt。
  • after profile 綁回 Alice subject,同一物件得到 OBJECT_OWNER_DENIED,receipt 為 0。
  • Alice 讀自己的物件仍可通過,證明不是全面封鎖。
  • app-only background job 只能走明確的 background action。
  • actor、tenant、audience、scope 各自錯一項,都有不同 deny reason。
  • actor 或 requested scope 超出 signed claims、原 token 過期、尚未生效或 authority epoch
    被撤銷時,既有 context 也會 fail closed。
  • 直接建構一份已知 subject 的 context 仍因缺少 opaque provenance 被拒絕;竄改
    server-issued context 也無法沿用原 provenance。
  • moon-rabbit-lab 的 Mallory context 不能與 bamboo-hq 的 Alice 混用。

本次本機 QA 為 2 passed。負向 structured capture 還會帶出 actor/tenant/audience/scope
矩陣;blockers 明確包含 MICROSOFT_ENTRA_OBO_EXCHANGE_NOT_EXERCISED

攻擊/before-state UI 要同時顯示 before 的 subject=null、Bob object 與一筆 read-only Receipt,也要顯示 after 的 OBJECT_OWNER_DENIED 與 app-only user route 的 USER_CONTEXT_REQUIRED。這是本機 policy/executor receipt,不是 Microsoft Entra 或 Foundry receipt;沒有 network observer,外部與雲端呼叫都只能標成未觀察:

PYTHONPATH=src:. uv run python -c 'import json; from magic_panda_agent.stages.day17 import cumulative_stage_replay; print(json.dumps(cumulative_stage_replay(), ensure_ascii=False, sort_keys=True))'

修補/test UI 從同一份 cumulative_stage_replay() payload 投影兩個 positive control:Alice 自己的物件得到 DELEGATED_OBJECT_POLICY_ALLOW,background app-only route 得到 APP_ONLY_BACKGROUND_ALLOWED,總 Receipt 為 2、side effect 為 0。這兩筆仍是本機 receipt;沒有外部/雲端 observer 或 receipt,因此不能推論 OBO exchange 或 Foundry tool authentication 已完成。

這些 structured capture 命令只重現 disclosure-safe JSON;正式發布時,本篇 required app UI 圖
必須由目前相關原始碼經 repository UI capture pipeline 產生,schema 3 manifest 必須以 source-tree SHA-256
與檔案數綁定輸入並交由 strict verifier 重算。正式圖仍只支持本機 delegation/policy 證據,沒有證明
MSAL OBO、consent 或 Foundry tool authentication 已執行。

雲端驗證邊界:PENDING-CLOUD

V2 尚未執行真實 MSAL OBO exchange、claims challenge、使用者 consent、MCP OAuth passthrough、cross-tenant deny 或 Foundry attended Agent flow。也尚未用兩位 Entra 使用者對同一 Hosted session 做交叉讀取測試。

本機 DownstreamIdentityContext 只能證明應用層契約會 fail closed,不能證明 Microsoft Entra 已簽發 token B,也不能證明特定 Foundry tool 已使用 user identity。雲端證據補齊前,維持 PENDING-CLOUD

Residual Risk:delegation 會擴大 token handling 面積

OBO 保留 user authority,也帶來 confidential-client credential、consent、token cache、refresh 與 claims challenge。access/refresh token 若進入 prompt、trace、browser storage 或 exception,新的資料外洩面就跟著出現。使用者撤權後,既有 token 與下游 cache 也未必在同一瞬間全部消失,正式系統要測 revocation 與 bounded cache lifetime。

NIST SP 800-207A 的逐請求決策,在這裡落成每一跳綁定 actor、subject、tenant、audience、scope、resource 與 action。它沒有要求所有工作都假裝成使用者;反而應把 delegated 與 autonomous authority 清楚分開。

明天會把這些 principal 放進 Foundry、Search 與 Key Vault 的 role/scope。那時候會看到另一個常見誤解:Azure Owner 名字很大,卻不是每個 data-plane endpoint 的宇宙通行證。

第一方參考資料


上一篇
Day 16|Microsoft Foundry 的 AI Agent 攻防實戰:換掉共享 API Key
下一篇
Day 18|Microsoft Foundry 的 AI Agent 攻防實戰:Azure Owner 不是宇宙萬能管理員,Foundry RBAC 到底管哪一層?
系列文
Microsoft Foundry 的 AI Agent 攻防實戰:建立高資安與可治理的 Agent 系統18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言