Post

Finding the Confused Deputy in Your Own Agent: A Taxonomy and Hands-On Test

The oldest bug in cloud security is back because the caller is now a language model. Here is a taxonomy of every confused deputy vector in your agent, a hands-on audit method, and the one-line fix that usually closes it.

Finding the Confused Deputy in Your Own Agent: A Taxonomy and Hands-On Test

In 1988, Norman Hardy published a short paper called “The Confused Deputy” in the ACM SIGOPS Operating Systems Review. The story was based on events at Tymshare, a commercial timesharing company. Its compiler needed permission to write statistics into a protected system directory. It also let users name a file for debugging output.

Someone supplied the name of the system’s billing file.

The user couldn’t write to that file. The compiler could. When the compiler opened the requested path, the operating system checked the compiler’s authority rather than the caller’s intent. The compiler then overwrote the billing information.

Nobody compromised the compiler. It used real authority for the wrong purpose, because the request carried a filename and no trustworthy statement of whose authority should apply to it.

Hardy wrote that up 38 years ago, and the answer hasn’t changed since: whatever exercises authority has to prove it is acting for the right party, for the right reason.

Swap the compiler for an LLM agent and the problem looks uncomfortably current. It is also worse, because the caller is now a language model that reads untrusted text and doesn’t produce the same output twice. Knowing who the caller is stopped being enough.

Why language models make it worse

The old version of this attack needed a user to craft the malicious input by hand. The attacker had to know what the privileged program could do, then find a way to hand it the right filename, URL or parameter.

Language models remove that step. Nobody has to instruct the agent to misbehave. The agent reads a document, an email, a web page carrying an instruction it should not trust, and acts on it. To the agent there is no difference between a legitimate parameter and a planted one. It reads text, reasons over it, emits a tool call.

And the tool call validates. The credential is scoped and short-lived. The delegation chain is intact end to end. Nobody asked for any of it.

This is not theoretical. Microsoft assigned CVE-2026-21520 to an indirect prompt injection in Copilot Studio. Capsule Security found the issue on November 24, 2025; Microsoft patched it on January 15, 2026, and it was reported in depth that April. Capsule Security called assigning a CVE to a prompt injection in an agentic platform highly unusual. VentureBeat’s headline was blunt: “Microsoft patched a Copilot Studio prompt injection. The data exfiltrated anyway.” The underlying problem is 38 years old, and the patch does not touch it.

In March 2026, the TeamPCP supply chain campaign compromised LiteLLM, Trivy, Checkmarx KICS, and the Telnyx SDK. LiteLLM is a gateway that thousands of enterprises put in front of their AI providers, and its whole job is holding API keys for dozens of services at once. That makes it about the densest credential target in a typical stack. Estimated harvest across the campaign: 500,000+ corporate identities. It worked because LiteLLM keeps long-lived credentials with broad access in one place, which is precisely the arrangement that turns a confused deputy into a bad afternoon.

The taxonomy: four vectors, four fixes

Every tool parameter the model fills in is a claim about authority that nobody verified. Here are the four categories where this happens in practice:

Untrusted content flowing through an LLM into a tool call, with four caller-supplied parameter vectors mapped to their fixes

The first three fixes are structural and close the vector outright. The fourth is partial, and the diagram says so: RFC 8693 preserves the delegation chain but earlier actors remain advisory.

1. Caller-supplied resource identifiers

The most common pattern. An agent tool accepts a patient_id, account_number, project_id, or document_id as a parameter. The LLM extracts this value from user input or retrieved content and passes it to the tool. The tool executes the action against that resource.

The vulnerability: The tool validates that the agent is authorized to call the tool. It does not validate that the resource identifier belongs to the user who initiated the request.

The fix: Derive the resource identifier from the session or workload identity, not from the caller.

Quarkslab demonstrated this with a medical assistant lab. The vulnerable tool accepted a patient_id from the LLM:

1
2
3
# Vulnerable: LLM controls the patient ID
def get_patient_medical_history_tool_vulnerable(patient_id: str) -> dict:
    return _fetch_patient_data(patient_id)

The fix was one line:

1
2
3
4
# Secure: derive from session
def get_patient_medical_history_tool_secure():
    current_user_id = session["user_id"]
    return _fetch_patient_data(current_user_id)

Or, if you need the LLM to work with specific resources:

1
2
3
4
5
6
# Secure: validate that the LLM-supplied ID matches the session
def get_patient_medical_history_tool_secure(patient_id: str) -> dict:
    current_user_id = session.get("user_id")
    if patient_id != current_user_id:
        return {"error": "Unauthorized"}
    return _fetch_patient_data(patient_id)

2. Caller-supplied scope

An agent receives an OAuth token or scope from an external source: another agent, a tool response, or content the agent retrieved. The agent then uses that scope to make API calls.

The vulnerability: That scope was issued for a different purpose, to a different party, under different constraints. Reusing it here can quietly grant more than anyone intended.

The fix: On every tool call, check the token’s audience, the field naming which service the token was minted for. And never hand a token straight through to the next service down the line. The MCP Authorization specification requires both: an MCP server accepts only tokens issued for itself, checks the audience, and does not forward the client’s token unchanged.

3. Caller-supplied tenancy

In multi-tenant systems, the tenant_id or organization_id is a parameter that determines which data partition the agent operates on. If the LLM controls this parameter, it can access data from any tenant.

The vulnerability: Same pattern as resource identifiers, but at a higher level of the access hierarchy. Getting the tenant wrong doesn’t just expose one record. It exposes an entire organization’s data.

The fix: Take tenancy from the identity the workload itself runs as. The service account behind Tenant A’s agent should be incapable of forming a valid request for Tenant B’s data, whatever the model puts in the parameter.

4. Delegation across boundaries you don’t own

This is the hardest version. Agent A, built by Company X, delegates to Agent B at Company Y, which invokes Agent C at Company Z. The particular combination may be selected at runtime rather than designed in advance by any one human.

The vulnerability: When authority crosses a boundary you don’t own, its origin, scope, and owner can disappear. Each hop can look reasonable locally. Agent B received a valid request from Agent A. Agent C received one from Agent B. The final service saw a valid credential from Agent C. Every system can explain the hand immediately before it. Nobody can explain the whole chain.

Steve Zenone at Morphic calls this “authority laundering.” Not fraud, necessarily. Not even deliberate concealment. It’s what happens when authority passes through enough intermediaries that its origin, limits, and accountable owner become difficult to reconstruct.

The fix: OAuth 2.0 Token Exchange (RFC 8693) with the act claim, which records each actor in the chain inside the token itself. Be honest about how far that gets you. RFC 8693 says access decisions are made on the current actor and the token’s top-level claims. The earlier actors are there for the record, not for enforcement. You get provenance. You do not get proof that every hop before this one was legitimate.

The audit method: three steps

Every MCP deployment should take these three steps. It will take less than an afternoon.

Step 1: Enumerate tool schemas

List every tool your agents can call. For each one, classify every parameter as supplied (the LLM fills it in) or derived (the system provides it from session, workload identity, or configuration).

A simple spreadsheet works. Columns: tool name, parameter name, parameter type, supplied-or-derived, risk level (high if it names a resource, medium if it names a scope, low otherwise).

The SpellSmith research looked at 53 collected MCP vulnerabilities. About 81% were taint-style, meaning attacker-controlled data reaches a sensitive operation without being checked on the way, and about 75% fired at the moment a tool got invoked. So this one classification step covers most of what they found. Fifty-three is a small corpus and I’d treat the exact figures loosely, but the direction is clear enough to act on.

Step 2: Flag every supplied parameter that names a resource

For every parameter classified as “supplied” where the value names a resource (a patient ID, an account number, a tenant identifier, a file path, a URL), flag it as a confused deputy risk.

The same paper found that 7% of MCP tool descriptions and under 2% of parameter descriptions say anything about security at all. Almost every tool out there hands the model a resource-naming parameter with no guidance attached.

Step 3: Apply the fix per category

For resource identifiers: derive from session or validate against session. For scopes: validate audience, never passthrough. For tenancy: derive from workload identity. For delegation chains: use act claims, resource-bound tokens, short expirations.

The structural checkpoint: draft-then-commit

For anything that matters, don’t let a single tool call finish the job. Split writes in two.

The agent drafts the email-address change. Carrying it out needs a separate commit tool that reads session state, confirms the draft was created moments ago in this session, and where warranted waits for a human. An injection can produce the draft. The commit step is where it falls apart.

That works because the check is structural. The commit tool inspects session state rather than the model’s reasoning. The model can be confused, steered, or simply wrong. The session state cannot.

Lin et al. introduced the VIGIL framework, which shows a verify-before-commit protocol cuts attack success rates by more than 22% relative to state-of-the-art dynamic defenses.

What to do this quarter

Audit the tool boundary. List every write your agents can perform. For each, answer one question: does the tool check that the user asked for this, or only that the agent is allowed to call it? Everything in the second group is a confused deputy waiting for its moment.

Add intent checks to writes, worst-case first. Email changes, payments, access grants, credential resets. Start where a mistake costs the most. One practical way: hash the action’s parameters before you execute, and compare that hash against what the user actually approved. It closes the gap between what the model meant to do and what anyone authorized.

Split the dangerous actions into draft and commit. Then pair those checkpoints with scoped credentials. Identity controls and intent controls stack; neither one carries the weight alone.

The multi-agent version

It gets harder once the chain crosses providers and no single participant holds a complete record. Anita Srinivasan laid out the legal shape of that in a June 2026 Berkeley Technology Law Journal Blog article. A court may have to decide which developer, deployer, operator or tool provider caused the harm before the infrastructure can even establish which systems were involved.

The authorization hasn’t literally vanished. Credentials were accepted. Calls were permitted. Systems acted.

What vanished was the legible connection between the final act and the original grant of authority.

What’s next

This piece is the hands-on test. The next one will look at the numbers behind prompt injection defenses: what they actually achieve, what methodology caveats make most of them softer than they look, and what follows for how you should design.

References

Earlier in this series

The Security Lab Newsletter

This post is the article. The newsletter is the lab.

Subscribers get what doesn't fit in a post: the full attack code with annotated results, the measurement methodology behind the numbers, and the week's thread — where I work through a technique or incident across several days of testing rather than a single draft. The RAG poisoning work, the MCP CVE analysis, the red-teaming patterns — all of it started as a newsletter thread before it became a post. One email per week. No sponsored content. Unsubscribe any time.

Join the lab — it's free

Already subscribed? Browse the back-issues →

This post is licensed under CC BY 4.0 by the author.