你的服務持有雲端儲存空間的 API key。一名使用者發出請求,你的服務盡責地寫入一個檔案。結果檔案卻落在攻擊者的 bucket,而不是你的,現在他們擁有了你的資料。你剛剛成為了一名無心的共犯。
這就是 confused deputy 攻擊。攻擊者本身無法寫入目標 bucket,他們缺乏這項權限。但他們可以誘騙你的服務——那個擁有權限的服務——替他們完成這件事。你的服務就是「deputy」,它對這個動作真正是為誰執行的感到「confused」。
Confused Deputy 攻擊實際長什麼樣子
最經典的例子是編譯器。編譯器以使用者的權限執行,因此可以讀取他們的原始碼檔案,並將 object 檔案寫入他們的家目錄。惡意使用者提供給編譯器一個原始碼檔案,其中包含將輸出寫到 /etc/passwd 而非編譯目錄的指令。編譯器擁有權限,使用者沒有。編譯器對它正在執行的是誰的意圖感到困惑。
現代網頁服務不斷重現這個模式。想像一個檔案上傳服務,允許使用者提供 callback URL:
# The user provides a URL. We fetch it and store it.
def upload_from_url(user_id: str, source_url: str, dest_path: str):
response = requests.get(source_url, timeout=30)
s3.put_object(Bucket="my-app-bucket", Key=dest_path, Body=response.content)
audit_log.info(f"User {user_id} uploaded to {dest_path}")
這看起來無害。使用者提供一個 URL,你去抓取,存進自己的 bucket。但如果 source_url 是 http://169.254.169.254/latest/meta-data/iam/security-credentials/ 呢?現在你正在外洩自己的 AWS credentials,並將它們寫入攻擊者命名的路徑。你的 IAM role 擁有權限,攻擊者沒有。你就是那個 confused deputy。
這種攻擊可以推廣到任何系統:一個具有高權限的元件代表權限較低的請求者執行動作,而請求者可以影響該動作的參數。
為什麼身份驗證還不夠
直覺的修復方式是確認請求者是誰。「這個使用者已驗證了嗎?」是的。「這個使用者有權上傳檔案嗎?」是的。兩項檢查都通過了,但攻擊仍然得逞。
問題在於,授權檢查只驗證 requester 是否能執行某個動作,並未驗證該動作的 target 對這位請求者來說是否合適。使用者被允許上傳,但他們不應該讓你的服務去抓取自己的 metadata,並存放在他們控制之下。權限模型將「可以使用上傳端點」與「可以讓服務執行任意的高權限動作」混為一談。
這就是核心故障模式:deputy 持有一個 capability(AWS credentials、檔案系統存取權、資料庫寫入權限),並根據他人的指示來使用它。如果不將 capability 與原始權限持有者的具體意圖綁定,confusion 是不可避免的。
Capability-Based Security:將權限與意圖綁定
Capability-based security 透過讓權限不可偽造且明確來解決這個問題。與其問「你是誰?」然後查詢你可以做什麼,capability 是一個 token,直接傳達在特定資源上執行特定動作的權利。
在 capability system 中,上傳服務不會持有通用的 S3 write key。它會持有一個 capability,只能寫入 特定 路徑,且範圍限於 特定 使用者。使用者的請求會包含一個 capability token,指明確切的目的地。服務會驗證 token 的密碼學簽名與其範圍,而非將使用者身份對照全域權限表。
以下是一個使用 scoped token 的簡化範例:
import hashlib
import hmac
import secrets
SECRET = secrets.token_bytes(32)
def mint_capability(principal: str, action: str, resource: str) -> str:
"""Create an unforgeable capability token scoped to a specific action and resource."""
payload = f"{principal}:{action}:{resource}"
sig = hmac.new(SECRET, payload.encode(), hashlib.sha256).hexdigest()[:16]
return f"{payload}:{sig}"
def verify_capability(token: str, expected_action: str, expected_resource: str) -> bool:
"""Verify a capability token matches the expected action and resource."""
try:
principal, action, resource, sig = token.rsplit(":", 3)
except ValueError:
return False
payload = f"{principal}:{action}:{resource}"
expected_sig = hmac.new(SECRET, payload.encode(), hashlib.sha256).hexdigest()[:16]
return hmac.compare_digest(sig, expected_sig) and action == expected_action and resource == expected_resource
# The user receives a capability that only allows writing to their own prefix.
user_id = "alice"
scoped_token = mint_capability(user_id, "write", f"uploads/{user_id}/report.pdf")
# Later, the service verifies the capability before acting.
if not verify_capability(scoped_token, "write", "uploads/alice/report.pdf"):
raise PermissionError("Capability does not match requested action or resource")
s3.put_object(Bucket="my-app-bucket", Key="uploads/alice/report.pdf", Body=data)
關鍵洞見:capability token scoped_token 無法被重播用來寫入 uploads/bob/salary.xlsx。它在建立時就與特定資源綁定。服務不會根據是誰在請求來決定允許什麼,而是根據 token 明確授權的內容來決定。
當 Capability System 不切實際時
我們大多數人並不是在建造具有純 capability 架構的作業系統。我們呼叫 AWS API、與 PostgreSQL 溝通、撰寫 HTTP handler。完整的 capability-based security 是一種設計哲學,而非即插即用的函式庫。
務實的折衷做法是將 capability 思維應用到你的服務邊界。與其給服務一個具有廣泛 S3 權限的單一 IAM role,不如使用 scoped presigned URL。與其讓使用者任意命名目的地,不如針對該使用者被允許接觸的範圍來驗證並清理目標資源。目標不是打造一個正式的 capability OS,而是消除隱性權限。
以下是同一個上傳端點,使用 scoped validation 取代全面信任:
import re
# A regex that constrains what a user can name as a destination.
ALLOWED_DEST_PATTERN = re.compile(r"^uploads/(?P<user_id>[a-z0-9_-]+)/[a-zA-Z0-9_.-]+$")
def upload_from_url(user_id: str, source_url: str, dest_path: str):
match = ALLOWED_DEST_PATTERN.match(dest_path)
if not match or match.group("user_id") != user_id:
raise ValueError("Invalid destination path")
# The source URL is also restricted. No internal IPs, no metadata endpoints.
parsed = urlparse(source_url)
if parsed.hostname in ("169.254.169.254", "localhost", "127.0.0.1"):
raise ValueError("Forbidden source URL")
response = requests.get(source_url, timeout=30)
s3.put_object(Bucket="my-app-bucket", Key=dest_path, Body=response.content)
這在正式意義上不是一個 capability system,但它應用了相同的原則:服務的權限不是一張空白支票,而是被限定在一組特定的動作中,這些動作是請求者被允許調用的。
為什麼這個模式不斷出現
Confused deputy 漏洞出現在 OAuth flow、serverless function、webhook handler 和 supply-chain pipeline 中。任何具有高權限的元件處理使用者輸入並據此行事時,風險就存在。
SSRF 到外洩 metadata 的模式如此常見,以至於雲端供應商現在預設在 VPC 中封鎖 IMDS IP。這只是針對一個症狀的緩解,而非根治病因。病因在於:具有高權限的元件無法區分自己的意圖與請求者的意圖。
實務上如何預防 Confused Deputy 攻擊
你不需要重寫整個架構。三個習慣就能涵蓋大多數情況。
限定你的 credentials 範圍。 如果你的服務要與 S3 溝通,就給它一個只能寫入特定 prefix 的 IAM policy。如果要與資料庫溝通,就使用 row-level security 或為每個 tenant 提供獨立的 credentials。Confused deputy 的爆炸半徑越小,造成的損害就越少。
驗證目標,不只是驗證行動者。 授權檢查應該回答兩個問題:請求者是否被允許執行這個動作,以及該動作的目標是否在請求者的範圍內?使用者可以上傳檔案,但他們能將檔案上傳到另一位使用者的目錄嗎?他們能讓你的服務呼叫任意 URL 嗎?第二個問題正是 confused deputy 的藏身之處。
避免 ambient authority。 Ambient authority 指的是一個行程僅僅因為它是誰就擁有權限,而非因為它正在做什麼。以 root 執行、使用 master API key、持有資料庫 superuser 連線。這些很方便,也很危險。拆分你的權限,使用具有明確到期時間的 temporary credentials。你的權限越細緻,就越難被混淆。
常見問題
CSRF 是 confused deputy 攻擊嗎?
某種程度上是的。使用者的瀏覽器就是 deputy。它持有 session cookie(即 capability),並因為惡意網站的指示而執行改變狀態的動作。瀏覽器對這個請求是否代表使用者的意圖感到困惑。CSRF token 的作用就是將 capability(cookie)綁定到特定的動作上下文,這正是 capability-based 的修復方式。
這也適用於 microservices 嗎?
當然。服務間呼叫通常使用具有廣泛權限的 shared service account。如果 Service A 帶著使用者提供的參數呼叫 Service B,而 Service B 使用它的高權限帳號對該參數採取行動,那麼 Service B 就是 confused deputy。應使用 scoped service token 或針對特定請求的授權。
SQL injection 呢?
SQL injection 是針對你資料庫的 confused deputy 攻擊。資料庫就是 deputy,它擁有讀寫資料表的權限。攻擊者將指令偽裝成輸入餵給它。Parameterized query 透過將資料庫的權限(query 結構)與使用者的輸入(參數)分離來解決這個問題。
審查你的 Deputies
檢視你的所有服務,找出那些持有使用者所沒有的權限的服務。對每一個服務自問:使用者是否能誘騙這個服務,使其以對攻擊者有利的方式使用它的權限?如果答案是肯定的,你就有一個 confused deputy。修復方式通常並不稀奇,通常只是需要限定服務願意執行的範圍,並拒絕超出該範圍的請求。
你的服務不應該成為共犯。確保它知道自己在聽從誰的命令。