Your service has an API key to your cloud storage. A user sends a request. Your service dutifully writes a file. The file lands in the attacker’s bucket, not yours, and now they have your data. You just became an unwitting accomplice.

This is a confused deputy attack. The attacker cannot write to the target bucket themselves. They lack the permission. But they can trick your service, which has the permission, into doing it for them. Your service is the “deputy.” It is “confused” about who the action is really for.

What a Confused Deputy Attack Actually Looks Like

The classic example is a compiler. The compiler runs with your user’s privileges, so it can read their source files and write object files to their home directory. A malicious user feeds the compiler a source file that contains directives to write output to /etc/passwd instead of the build directory. The compiler has the authority. The user does not. The compiler is confused about whose intent it is carrying out.

Modern web services recreate this pattern constantly. Consider a file upload service that lets users provide a 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}")

This looks innocent. The user provides a URL. You fetch it. You store it in your bucket. But what if source_url is http://169.254.169.254/latest/meta-data/iam/security-credentials/? Now you’re exfiltrating your own AWS credentials and writing them to a path the attacker named. Your IAM role has the authority. The attacker does not. You are the confused deputy.

The attack generalizes to any system where a privileged component performs an action on behalf of a less-privileged requester, and the requester can influence the parameters of that action.

Why Identity Checks Are Not Enough

The instinctive fix is to check who the requester is. “Is this user authenticated?” Yes. “Is this user authorized to upload files?” Yes. Both checks pass, and the attack still works.

The problem is that authorization checks verify whether the requester can perform an action. They do not verify whether the target of the action is appropriate for that requester. The user is allowed to upload. They are not allowed to make your service fetch its own metadata and store it under their control. The permission model conflates “can use the upload endpoint” with “can make the service perform arbitrary privileged actions.”

This is the core failure mode: the deputy holds a capability (AWS credentials, file system access, database write access) and uses it based on instructions from someone else. Without binding the capability to the specific intent of the original authority holder, confusion is inevitable.

Capability-Based Security: Binding Authority to Intent

Capability-based security fixes this by making authority unforgeable and explicit. Instead of asking “who are you?” and then looking up what you’re allowed to do, a capability is a token that directly conveys the right to perform a specific action on a specific resource.

In a capability system, the upload service would not hold a blanket S3 write key. It would hold a capability to write to specific paths, scoped to specific users. The user’s request would include a capability token that names the exact destination. The service would verify the token’s cryptographic signature and its scope, not the user’s identity against a global permission table.

Here’s a simplified example using scoped tokens:

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)

The key insight: the capability token scoped_token cannot be replayed to write to uploads/bob/salary.xlsx. It is bound to a specific resource at creation time. The service does not decide what is allowed based on who is asking. It decides based on what the token explicitly authorizes.

When Capability Systems Are Impractical

Most of us are not building operating systems with pure capability architectures. We’re calling AWS APIs, talking to PostgreSQL, and writing HTTP handlers. Full capability-based security is a design philosophy, not a drop-in library.

The practical middle ground is to apply capability thinking to your service boundaries. Instead of giving your service a single IAM role with broad S3 permissions, use scoped presigned URLs. Instead of letting users name arbitrary destinations, validate and sanitize the target resource against what that user is allowed to touch. The goal is not a formal capability OS. The goal is eliminating implicit authority.

Here is the same upload endpoint with scoped validation instead of 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)

This is not a capability system in the formal sense. But it applies the same principle: the service’s authority is not a blank check. It is scoped to a specific set of actions that the requester is allowed to invoke.

Why This Pattern Keeps Reappearing

Confused deputy vulnerabilities show up in OAuth flows, serverless functions, webhook handlers, and supply-chain pipelines. Any time a component with elevated privileges processes user input and acts on it, the risk is present.

The SSRF-to-metadata-exfiltration pattern is so common that cloud providers now block the IMDS IP by default in VPCs. That is a mitigation for one symptom, not the disease. The disease is a privileged component that cannot distinguish its own intent from the requester’s intent.

Preventing Confused Deputy Attacks in Practice

You do not need to rewrite your architecture. Three habits cover most cases.

Scope your credentials. If your service talks to S3, give it an IAM policy that can only write to a specific prefix. If it talks to a database, use row-level security or separate credentials per tenant. The smaller the blast radius of a confused deputy, the less damage it can do.

Validate the target, not just the actor. Authorization checks should answer two questions: is the requester allowed to perform this action, and is the target of the action within the requester’s scope? A user can upload files. Can they upload files to another user’s directory? Can they make your service call arbitrary URLs? The second question is where confused deputies live.

Avoid ambient authority. Ambient authority is when a process has permissions simply because of who it is, not because of what it is doing. Running as root. Using a master API key. Holding a database superuser connection. These are convenient and dangerous. Split your privileges. Use temporary credentials with explicit expiration. The more granular your authority, the harder it is to confuse.

FAQ

Is CSRF a confused deputy attack?

Yes, in a sense. The user’s browser is the deputy. It holds a session cookie (the capability) and performs a state-changing action because a malicious site told it to. The browser is confused about whether the request represents the user’s intent. CSRF tokens work by binding the capability (the cookie) to a specific action context, which is exactly the capability-based fix.

Does this apply to microservices?

Absolutely. Service-to-service calls often use a shared service account with broad permissions. If Service A calls Service B with a user-provided parameter, and Service B uses its privileged account to act on that parameter, Service B is the confused deputy. Use scoped service tokens or request-specific authorization.

What about SQL injection?

SQL injection is a confused deputy attack on your database. The database is the deputy. It has the authority to read and write tables. The attacker feeds it instructions disguised as input. Parameterized queries fix this by separating the database’s authority (the query structure) from the user’s input (the parameters).

Audit Your Deputies

Go through your services and identify the ones that hold privileges users do not. For each one, ask: could a user trick this service into using its authority in a way that benefits the attacker? If the answer is yes, you have a confused deputy. The fix is rarely exotic. It is usually a matter of scoping what the service is willing to do, and refusing requests that fall outside that scope.

Your service should not be an accomplice. Make sure it knows whose orders it is following.