你的服务持有一个云存储的 API key。用户发来一个请求,你的服务乖乖地把文件写了进去。文件落到了攻击者的 bucket 里,而不是你的,于是攻击者拿到了你的数据。你在不知不觉中成了帮凶。
这就是 confused deputy 攻击。攻击者自己无法向目标 bucket 写入数据——他们没有这个权限。但他们可以诱骗你的服务代劳,而你的服务正好拥有这个权限。你的服务就是那个 “deputy(代理人)“,它 “confused(混淆了)” 这个操作到底是为谁而做的。
Confused Deputy 攻击实际长什么样
最经典的例子是编译器。编译器以用户的权限运行,因此可以读取用户的源文件,并将目标文件写入他们的 home 目录。恶意用户给编译器投喂了一个源文件,其中包含将输出写入 /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,你去拉取,存到自己的 bucket。但如果 source_url 是 http://169.254.169.254/latest/meta-data/iam/security-credentials/ 呢?现在你正在把自己的 AWS credentials 外泄,并写入攻击者指定的路径。你的 IAM role 拥有权限,攻击者没有。你就是那个 confused deputy。
这种攻击可以推广到任何这样的系统:一个有特权的组件代表一个权限较低的请求者执行操作,而请求者可以影响该操作的参数。
为什么身份检查还不够
直觉上的修复方式是检查请求者是谁。“这个用户是否已认证?” 是的。“这个用户是否有上传文件的权限?” 是的。两项检查都通过了,攻击依然奏效。
问题在于,授权检查只验证请求者是否有权执行某个操作,并不验证该操作的目标是否适合这个请求者。用户被允许上传文件,但他们无权让你的服务去抓取自己的 metadata 并存到他们控制的路径下。权限模型把”可以使用上传接口”和”可以让服务执行任意特权操作”混为一谈了。
这就是核心故障模式:deputy 持有一个 capability(AWS credentials、文件系统访问、数据库写入权限),并根据别人的指令去使用它。如果不把 capability 绑定到原始权限持有者的具体意图上,confusion 就不可避免。
Capability-Based Security:将权限与意图绑定
Capability-based security 通过让权限不可伪造且显式化来解决这个问题。它不再问”你是谁?“然后去查你能做什么,而是用一个 token(capability)直接授予对特定资源执行特定操作的权利。
在 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 architecture 的操作系统。我们在调用 AWS API、连接 PostgreSQL、编写 HTTP handler。完整的 capability-based security 是一种设计哲学,而不是一个即插即用的库。
务实的折中方案是把 capability 的思维方式应用到服务边界上。不要给你的服务一个拥有广泛 S3 权限的单一 IAM role,而是使用 scoped presigned URL。不要让用户随意指定目的地,而是要针对该用户被允许接触的资源来验证和清理目标资源。目标不是构建一个正式的 capability OS,而是消除隐式权限(implicit authority)。
下面是同一个上传端点,用 scoped validation 取代 blanket trust 的版本:
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 和供应链流水线中。只要一个拥有提升权限的组件处理用户输入并据此行动,风险就存在。
SSRF 转 metadata exfiltration 的模式如此常见,以至于云厂商现在默认在 VPC 中屏蔽 IMDS IP。这只是缓解了一个症状,而不是病根。病根在于:一个有特权的组件无法区分自己的意图和请求者的意图。
实践中如何防范 Confused Deputy 攻击
你不需要重写架构。三个习惯就能覆盖大部分场景。
限制 credential 的作用范围。 如果你的服务需要访问 S3,就给它一个只能写入特定 prefix 的 IAM policy。如果它要访问数据库,就使用 row-level security 或为每个租户分配独立的 credential。confused deputy 的爆炸半径越小,能造成的损害就越小。
验证目标,而不仅仅是验证行为者。 授权检查应该回答两个问题:请求者是否被允许执行这个操作?该操作的目标是否在请求者的范围内?用户可以上传文件,但他们能把文件传到另一个用户的目录吗?他们能让你的服务调用任意 URL 吗?第二个问题才是 confused deputy 的藏身之处。
避免 ambient authority。 Ambient authority 指的是一个进程仅仅因为”它是谁”就拥有权限,而不是因为”它在做什么”。以 root 运行、使用 master API key、持有 database superuser 连接。这些很方便,但也很危险。拆分你的权限,使用带有明确过期时间的 temporary credential。你的权限越细粒度,就越难被混淆。
常见问题
CSRF 是一种 confused deputy 攻击吗?
某种意义上是的。用户的浏览器就是 deputy。它持有 session cookie(即 capability),并因为恶意网站的指令而执行了状态变更操作。浏览器混淆了该请求是否代表用户的真实意图。CSRF token 的作用是把 capability(cookie)绑定到特定的操作上下文上,这正是 capability-based 的修复方式。
这适用于 microservices 吗?
当然。服务间调用经常使用拥有广泛权限的共享 service account。如果 Service A 带着用户提供的参数调用 Service B,而 Service B 使用自己的特权账户去处理该参数,那么 Service B 就是 confused deputy。应该使用 scoped service token 或请求级别的授权。
SQL injection 呢?
SQL injection 是针对数据库的 confused deputy 攻击。数据库就是 deputy,它拥有读写表的权限。攻击者把指令伪装成输入喂给它。Parameterized query 通过把数据库的权限(查询结构)和用户的输入(参数)分离开来,从而修复了这个问题。
审计你的 Deputies
逐个检查你的服务,找出那些持有用户所不具备的特权的服务。对每一个服务,问问自己:用户能否诱骗该服务,让它以自己的权限做出对攻击者有利的事?如果答案是肯定的,你就有了一个 confused deputy。修复方案通常并不稀奇——往往只是限定服务愿意做什么,并拒绝超出该范围的请求。
你的服务不应该成为帮凶。确保它知道自己在听从谁的命令。