
The Confused Deputy and OAuth Weaknesses in MCP
For three days we attacked the descriptions tools carry. Today we go one layer down, to the thing a tool actually holds when it runs: the token, the identity, the authorization. This is the execute stage from Day 2, examined at the level of who the agent is when it acts, and it is where the single highest leverage finding in most agent assessments lives.
We named the confused deputy on Day 2 and kept pointing at it all week. The GitHub MCP incident worked because the server’s token carried blanket access. The shared service token in the Day 6 map amplified every chain. Cross server attacks on Day 10 were a confused deputy by another route. Today we make it the main subject, add the OAuth problems that MCP specifically introduced, and attack both in the lab.
The confused deputy, precisely
A confused deputy is a program with more authority than the party asking it to act, that can be tricked into using that authority on the asker’s behalf. The classic 1980s example is a compiler that can write to any directory being asked by a user to write output to a protected file the user could not touch directly. The compiler is the deputy. Its authority gets borrowed.
An agent is the most eager deputy ever built. It holds tokens to databases, email, repositories, and payment systems, and it takes instructions from whatever enters its context, including content an outsider wrote. Every indirect injection in this series is, at its core, a confused deputy attack: the attacker cannot reach the database, but the agent can, and the attacker controls what the agent does.
State it as a rule. The agent’s effective authority is whatever its tokens grant, and the agent will lend that authority to whoever successfully instructs it. If the token is broad and the instruction channel is open, which is the default, the deputy is confused by design.
Why one token is the amplifier
Here is the architectural choice that makes everything worse, and it is nearly universal because the alternative is work.
Most MCP deployments authenticate the server to the backend with a single service credential, shared across all users. The support agent in Day 6 held one token to the production database for the entire support team. When any staff member uses the agent, the queries run as that one token.
The consequence is brutal and simple. The agent can reach everything that token can reach, regardless of who is asking. There is no per user scoping at the point of execution. So a successful injection does not get the attacker the current user’s access. It gets them the union of all access, the full scope of the shared token.
This is why on Day 6 the engagement summary said any successful injection operates at the privilege of the entire support function, not the individual user. Cross customer data theft, chain 1, worked precisely because lookup_customer run under the shared token could read any customer, not just the ticket’s owner. The injection supplied the intent. The broad token supplied the reach.
Scope the token and chain 1 shrinks from read any customer to read this customer, which is often no finding at all. That is why token scope is the highest leverage fix in the whole map, and therefore the finding to lead with.
Build and prove it in the lab
Your Day 7 lookup_customer already has the vulnerability, the comment said no check that this is the customer who owns the current ticket. Today we make the fix explicit so you can see the before and after, which is exactly what a good report shows.
The vulnerable version, as built:
@mcp.tool()
def lookup_customer(customer_id: str) -> str:
"""Look up a customer profile by id."""
# VULN: confused deputy. No binding to the current ticket's owner.
c = _load("customers.json").get(customer_id)
return json.dumps(c) if c else "customer not found"
Run chain 1 from Day 8, the T-501 ticket that tells the agent to look up C-1002 and C-1003. It succeeds because the tool will look up anyone.
Now scope it to the request. The key idea: the tool must derive the allowed customer from the trusted context, the ticket being handled, not from an argument the agent chose under attacker influence.
@mcp.tool()
def lookup_customer(customer_id: str, ticket_id: str) -> str:
"""Look up the customer profile for the customer who owns the given ticket."""
ticket = _load("tickets.json").get(ticket_id)
if not ticket:
return "ticket not found"
owner = ticket["customer"]
if customer_id != owner:
# the deputy refuses to borrow authority for a different customer
return f"access denied: ticket {ticket_id} belongs to {owner}, not {customer_id}"
c = _load("customers.json").get(customer_id)
return json.dumps(c) if c else "customer not found"
Re run T-501. The injected instruction to pull C-1002 and C-1003 now returns access denied, because the authority is bound to the ticket, not to whatever the agent was talked into requesting. The injection still fires. The agent still tries. The token refuses.
Sit with that distinction, because it is the whole philosophy of week four arriving early. You did not stop the injection. You made a successful injection reach nothing. That is the only kind of defense that holds against an adversary who will always eventually get text into context.
The OAuth problems MCP introduced
Beyond the shared token, MCP added its own authorization headaches as it bolted auth onto a protocol that launched without much of it. Three worth knowing.
Tokens with the wrong audience. When an MCP server accepts a token and then uses it, or a derived token, to call a downstream API, the token’s audience and scope matter enormously. A token minted for one purpose being accepted for another is a classic confused deputy at the OAuth layer. The MCP authorization guidance exists largely to pin down that tokens must be bound to the intended audience and not blindly passed along, because early implementations did pass them along.
Over scoped consent. When a user authorizes an MCP server via OAuth, the consent screen often requests broad scopes because the server wants to cover every tool it might offer. The user clicks approve. Now the server, and anything that can influence the agent using it, holds far more than the task needed. Over scoped grants are excessive agency delivered through a legitimate consent flow.
Token theft becomes total. Because the agent concentrates many capabilities, a single stolen MCP token or session can be worth more than any one user account. The token is a skeleton key by construction. This raises the stakes on every storage and transport weakness in the server, and it is why the plaintext token and loose session handling found in many deployed servers are not minor hygiene issues.
A caveat in keeping with this week: the MCP auth spec and its guidance are actively evolving, and specific requirements have changed between revisions. Treat the three problems above as the durable concepts and confirm the current normative requirements against the latest MCP authorization specification before citing a specific rule in a report.
Where this sits in the trifecta and OWASP
On the trifecta, this is the amplifier on condition A. The broad token does not create the valuable target, it widens it from one customer’s data to everyone’s. On the OWASP agentic categories, it is squarely excessive agency and identity and privilege issues, and it is the condition that turns a medium finding into a critical one across the board. Fix it and many separate findings downgrade at once, which is exactly the leverage you want to communicate.
Detection and defense, the honest version
Ordered by effectiveness.
Scope authority to the request, derived from trusted context. The lab fix above, generalized. Every high value tool should bind its reach to something the agent did not choose under influence: the ticket owner, the authenticated user, the resource already in scope. This is the single most valuable control against the entire injection class, because it caps the blast radius of every successful injection at once.
Per user credentials, not one service token. Where the backend supports it, the agent should act as the current user, so its reach is naturally limited to that user’s access. This removes the union of all access problem at the source.
Bind tokens to audience and minimize scope. Follow current MCP authorization guidance so tokens are not over scoped at consent and not accepted across audiences. Review what an OAuth consent actually granted versus what the tools need.
Protect the token like the skeleton key it is. Encrypt at rest, short lifetimes, rotation, tight session handling. Because concentration makes one token worth many accounts, the usual token hygiene is higher stakes here.
Log actions with the acting identity. Every tool call should record who it acted as and what it reached, so a confused deputy event is reconstructable. This is the monitoring gap from Day 6, and it is what lets you prove scope was respected or catch when it was not.
The pattern across the week holds once more. You do not win by detecting the malicious instruction. You win by ensuring that when the instruction succeeds, the authority it borrows is too small to matter.
Homework for Day 11
Against the lab:
- Confirm the vulnerable
lookup_customer lets T-501 read C-1002 and C-1003. This is your before.
- Apply the ticket bound scoping fix. Re run T-501 and confirm the cross customer reads now return access denied while the legitimate C-1001 lookup still works. This is your after.
- Do the same for
create_refund: bind the refund to the ticket’s customer and an amount ceiling, so an injected refund to an attacker destination is refused at the tool, not just discouraged in the prompt.
- Write the before and after as a report finding: the injection is unchanged and still fires, the impact dropped from all customers to none, and explain why that is the right place to fix it.
- List every tool in your own real agent and, for each, write what identity it acts as and whether its reach is bound to the request or to the shared token. Count how many are unbound.
Point 2 is the demonstration that matters: same attack, same injection, reach reduced to nothing by scoping alone. That before and after is the most persuasive thing you can put in front of a team, because it shows the cheap fix beating the unsolvable problem.
Day 12 closes the protocol week with enumeration and fingerprinting: how to profile an MCP target from the outside, pull everything we have attacked this week into a repeatable recon process, and know a server’s weaknesses before you send a single payload.