あなたのサービスはクラウドストレージへのAPIキーを持っています。ユーザーがリクエストを送信します。あなたのサービスは忠実にファイルを書き込みます。ファイルはあなたのバケットではなく、攻撃者のバケットに着地し、彼らはあなたのデータを手に入れました。あなたは知らぬ間の共犯者になったのです。
これがconfused deputy attackです。攻撃者自身はターゲットのバケットに書き込むことはできません。彼らには権限がありません。しかし、権限を持つあなたのサービスを騙して、代わりに実行させることができるのです。あなたのサービスが「deputy(代理人)」です。その行為が本当に誰のためのものかを「confused(混乱)」しているのです。
Confused Deputy Attackの実際の姿
古典的な例はコンパイラです。コンパイラはユーザーの権限で動作するため、ソースファイルを読み込み、ホームディレクトリにオブジェクトファイルを書き込むことができます。悪意のあるユーザーは、ビルドディレクトリではなく/etc/passwdに出力を書き込む指示を含むソースファイルをコンパイラに渡します。権限を持つのはコンパイラです。ユーザーにはありません。コンパイラは、誰の意図を実行しているのか混乱しているのです。
現代のWebサービスはこのパターンを絶えず再現しています。ユーザーがコールバック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を提供し、あなたが取得して、自分のバケットに保存します。しかし、source_urlがhttp://169.254.169.254/latest/meta-data/iam/security-credentials/だったらどうでしょうか?今、あなたは自分のAWS認証情報をexfiltrateし、攻撃者が命名したパスに書き込んでいます。権限を持つのはあなたのIAMロールです。攻撃者にはありません。あなたがconfused deputyなのです。
この攻撃は、特権を持つコンポーネントが権限の低いリクエスターに代わってアクションを実行し、リクエスターがそのアクションのパラメータに影響を与えられるあらゆるシステムに一般化されます。
なぜIDチェックだけでは足りないのか
本能的な修正は、リクエスターが誰かを確認することです。「このユーザーは認証されていますか?」はい。「このユーザーはファイルをアップロードする権限がありますか?」はい。両方のチェックを通過しても、攻撃は依然として機能します。
問題は、認可チェックがリクエスターがアクションを実行できるかどうかを検証するだけであることです。アクションのターゲットがそのリクエスターにとって適切かどうかは検証しません。ユーザーはアップロードが許可されています。しかし、あなたのサービスに自分のメタデータを取得させ、彼らの管理下に保存させることは許可されていません。権限モデルは「アップロードエンドポイントを使える」と「サービスに任意の特権アクションを実行させられる」を混同しています。
これが核心的な障害モードです:deputyはcapability(AWS認証情報、ファイルシステムアクセス、データベース書き込みアクセス)を保持し、他者からの指示に基づいてそれを使用します。capabilityを元の権限持有者の具体的な意図に結びつけなければ、混乱は避けられません。
Capability-Based Security:権限を意図に結びつける
Capability-based securityは、権限を改ざん不可能で明示的なものにすることでこれを修正します。「あなたは誰ですか?」と尋ねてから何が許可されているかを調べるのではなく、capabilityは特定のリソースに対して特定のアクションを実行する権利を直接伝えるトークンです。
Capabilityシステムでは、アップロードサービスは包括的なS3書き込みキーを保持しません。特定のパスに書き込むcapabilityを保持し、特定のユーザーにスコープされます。ユーザーのリクエストには、正確な宛先を示すcapabilityトークンが含まれます。サービスは、グローバルな権限テーブルに対するユーザーのIDではなく、トークンの暗号署名とそのスコープを検証します。
スコープ付きトークンを使用した簡略化された例を示します:
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トークンscoped_tokenは、uploads/bob/salary.xlsxに書き込むためにリプレイすることはできません。作成時に特定のリソースに結びつけられています。サービスは、誰が尋ねているかに基づいて何が許可されるかを決定しません。トークンが明示的に認可する内容に基づいて決定します。
Capabilityシステムが非現実的な場合
私たちのほとんどは、純粋なcapabilityアーキテクチャのオペレーティングシステムを構築しているわけではありません。AWS APIを呼び出し、PostgreSQLと通信し、HTTPハンドラーを書いています。完全なcapability-based securityは設計思想であり、ドロップインライブラリではありません。
実用的な中間地帯は、capabilityの考え方をサービス境界に適用することです。広範なS3権限を持つ単一のIAMロールをサービスに与えるのではなく、スコープ付きのpresigned URLを使用します。ユーザーに任意の宛先を命名させるのではなく、そのユーザーが触れてよいものに対してターゲットリソースを検証・サニタイズします。目標は形式的なcapability OSではありません。暗黙の権限を排除することです。
以下は、包括的な信頼の代わりにスコープ付き検証を行う同じアップロードエンドポイントです:
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システムではありません。しかし、同じ原則を適用しています:サービスの権限は白紙の小切手ではありません。リクエスターが呼び出すことを許可された特定のアクションセットにスコープされています。
なぜこのパターンは繰り返し現れるのか
Confused deputyの脆弱性は、OAuthフロー、サーバーレス関数、webhookハンドラー、サプライチェーンパイプラインに現れます。特権を持つコンポーネントがユーザー入力を処理してそれに基づいて動作するたびに、リスクは存在します。
SSRFからmetadata exfiltrationへのパターンは非常に一般的なため、クラウドプロバイダーはVPC内でIMDS IPをデフォルトでブロックするようになりました。これは一つの症状に対する緩和策であり、病気そのものではありません。病気とは、自分自身の意図とリクエスターの意図を区別できない特権コンポーネントです。
実践でのConfused Deputy Attackの防止
アーキテクチャを書き換える必要はありません。3つの習慣でほとんどのケースをカバーできます。
認証情報をスコープ化する。 サービスがS3と通信する場合、特定のプレフィックスにしか書き込めないIAMポリシーを与えましょう。データベースと通信する場合は、row-level securityを使用するか、テナントごとに別の認証情報を使用しましょう。Confused deputyのblast radiusが小さければ小さいほど、与える被害は少なくなります。
アクターだけでなくターゲットも検証する。 認可チェックは2つの質問に答えるべきです:リクエスターはこのアクションを実行することを許可されていますか?アクションのターゲットはリクエスターのスコープ内にありますか?ユーザーはファイルをアップロードできます。他のユーザーのディレクトリにファイルをアップロードできますか?あなたのサービスに任意のURLを呼び出させることはできますか?2番目の質問こそが、confused deputyが潜む場所です。
Ambient authorityを避ける。 Ambient authorityとは、プロセスが何をしているかではなく、それが誰であるかという理由だけで権限を持っている状態です。rootとして実行する。マスターAPIキーを使用する。データベースのスーパーユーザー接続を保持する。これらは便利でありながら危険です。権限を分割しましょう。明示的な有効期限を持つ一時的な認証情報を使用しましょう。権限が細かければ細かいほど、混乱させるのは難しくなります。
FAQ
CSRFはconfused deputy attackですか?
はい、ある意味では。ユーザーのブラウザがdeputyです。セッションクッキー(capability)を保持し、悪意のあるサイトに言われたから状態を変更するアクションを実行します。ブラウザは、そのリクエストがユーザーの意図を表しているかどうかについて混乱しています。CSRFトークンは、capability(クッキー)を特定のアクションコンテキストに結びつけることで機能し、これはまさにcapability-basedの修正です。
これはマイクロサービスにも当てはまりますか?
もちろんです。サービス間の呼び出しでは、広範な権限を持つ共有サービスアカウントがしばしば使用されます。サービスAがユーザー提供のパラメータでサービスBを呼び出し、サービスBがその特権アカウントを使用してそのパラメータに基づいて動作する場合、サービスBがconfused deputyです。スコープ付きサービストークンまたはリクエスト固有の認可を使用しましょう。
SQL injectionはどうですか?
SQL injectionは、あなたのデータベースに対するconfused deputy attackです。データベースがdeputyです。テーブルの読み書きを行う権限を持っています。攻撃者は、入力に伪装した指示をデータベースに送り込みます。Parameterized queriesは、データベースの権限(クエリ構造)とユーザーの入力(パラメータ)を分離することでこれを修正します。
あなたのDeputyを監査する
サービスを見渡して、ユーザーが持っていない権限を保持しているものを特定しましょう。それぞれについて、ユーザーがこのサービスを騙して攻撃者に利益をもたらす形でその権限を使用させることができるかどうかを自問しましょう。答えがイエスなら、あなたにはconfused deputyがいます。修正策が珍しいものであることはめったにありません。通常、サービスが行うことをスコープ化し、そのスコープ外に落ちるリクエストを拒否することです。
あなたのサービスは共犯者であるべきではありません。誰の命令に従っているかを確実に把握させましょう。