Skip to content

+ Intertrace Threat ResearchTLP:CLEAR6 sources

Threat research10 min read

The confused deputy in MCP OAuth proxies: consent that belongs to someone else

An MCP server that fronts a third-party API with one static OAuth client can hand a user's access to a client they never approved. The eight questions a reviewer should put to any MCP OAuth proxy, answered from the specification and two fixed FastMCP advisories.

How attacks on AI agents work in practice, from public incidents and our own test corpora, and the controls that stop them.

October 16, 2026 · 10 min read

MCPOAuthConfused DeputyAuthorizationCVE-2026-27124
Contents
  1. 01Which client did this user actually approve?
  2. 02How does consent for one client get spent by another?
  3. 03Is asking for consent per client enough?
  4. 04What must the proxy bind, and where is each binding enforced?
  5. 05Can a token issued for another service get through?
  6. 06Would we see the attack while it happened?
  7. 07What tests prove the bindings hold?
  8. 08If every test passes, is the access appropriate?

Consider an illustrative software company whose platform team runs one MCP proxy in front of GitHub for all its coding agents, with open dynamic client registration. An engineer, Dana, approved GitHub's consent screen once, in March. In October an outsider registers a client called Triage Helper with a redirect URI on their own host and posts a link in a public issue: connect the triage bot here. She clicks it while logged into GitHub, GitHub sees she already authorized the company's OAuth application and does not ask again, and within a minute the attacker holds an MCP token and is listing her private repositories. Every audit line on GitHub says it was her, through the company's own application.

Nothing in that story was forged. The proxy was the deputy: it held her authority and spent it for a requester it never checked. Our position is that an MCP OAuth proxy should be reviewed as a list of bindings, each enforced at a named point in the code and proved by a test, and that per-client consent alone is not enough. Here are the questions that review should ask.

Which client did this user actually approve?

Often the honest answer is: the proxy, once, months ago, from a different client. The MCP security guidance defines the setup [1]. A proxy connects MCP clients to a third-party API and acts as a single OAuth client to it. The third-party authorization server may not support dynamic client registration, so the proxy uses one static client ID for every request, whichever MCP client started it. Meanwhile MCP clients register with the proxy dynamically, each getting its own client ID.

So there are two relationships. Toward MCP clients, the proxy is the authorization server and issues its own codes and tokens. Toward the third party, it is one client among many. FastMCP's advisory describes this shape with Entra ID: the FastMCP-hosted server lists itself as the authorization server in its resource metadata, issues codes to MCP clients, and delegates to Entra ID as an OAuth client [3]. The third party sees one application. Only the proxy knows how many clients sit behind it.

One static client ID, many clients behind itFig. 1
Dana's MCP clientconsented in MarchAttacker's clientregistered minutes agoREDIRECT_URI = ATTACKER.EXAMPLEMCP proxyone static client IDstatic IDThird-party ASconsent cookie: proxyCONSENT SKIPPEDPer-client consent at the proxyASK BEFORE FORWARDINGthe third party remembers consent for the proxy's client ID, not for the MCP client behind it · only the proxy can tell them apart

Dana's client and an attacker's newly registered client both reach the third-party authorization server through the proxy's single static client ID. Consent recorded for that ID is skipped the second time, so only per-client consent at the proxy can tell the two apart.

The specification names four conditions that together make the attack possible: a static client ID with the third party, dynamic registration of MCP clients, a consent cookie the third party sets after the first authorization, and no per-client consent at the proxy before forwarding [1]. Many real deployments meet all four by default. The sequence:

  1. A user connects normally, and the third party records consent for the proxy's static client ID, typically in a cookie on the user's browser [1].
  2. An attacker registers a new MCP client with the proxy, giving a redirect URI on a host the attacker controls.
  3. The attacker sends the user a link that starts an authorization request at the proxy for the attacker's client.
  4. The proxy forwards to the third party with its static client ID. The third party sees the consent cookie and skips the consent screen [1].
  5. The proxy exchanges the third party's code, mints its own MCP authorization code, and redirects the browser to the attacker's registered URI.
  6. The attacker exchanges that code for an MCP token and calls the proxy's tools as the user, with the user's third-party access [1].

Every token is valid, every redirect matched a registered URI, and the third party behaved as designed. That is why the specification puts the fix on the proxy: proxies using static client IDs must obtain user consent for each dynamically registered client before forwarding to the third party [2].

No, and FastMCP's two advisories show why. The first, GHSA-c2jp-c369-7pvx, published in October 2025, covers FastMCP's Entra ID integration [3]. An attacker could register their own client and get an authorization URL redirecting to their host; a victim who had previously consented to FastMCP's static Entra ID application and clicked it would finish the flow without prompts, sending the code to the attacker's client. The consent screen showed only the first time a user used the application. Versions before 2.13.0 were affected [3].

The second, CVE-2026-27124, published in March 2026, is subtler [4]. FastMCP's OAuth proxy callback did not confirm that the browser presenting a valid state and code was the one that had just given consent. Because GitHub skips its consent page for previously authorized applications, an attacker could authorize their own client, capture the resulting GitHub authorization URL, and lure a victim who was logged into GitHub and had connected an MCP client to the same server; the proxy then issued a code to the attacker's client [4]. Versions before 3.2.0 were affected. The advisory recommends browser-bound consent, such as a consent cookie the callback validates, and notes it was verified only with GitHub but that any provider skipping consent for returning users may be affected [4].

The first fix makes the proxy ask. The second shows the answer must be tied to the browser session that gave it and checked at the callback. The specification's state rules cover exactly this: set the state value only after the user approves the proxy's consent screen, set it immediately before redirecting to the third party, validate it at the callback, and make it single-use with a short lifetime [1].

What must the proxy bind, and where is each binding enforced?

Every control here ties together two things the attacker wants to separate. For each row, a reviewer should be able to point to the line of code and the test.

Bindings an MCP OAuth proxy must keep, and what breaks without each
BindingWhat it ties togetherRequirementFailure without it
Per-client consentUser, MCP client ID and third-party scopesRegistry of approved client IDs per user, checked before forwarding [1][2]A new client inherits an old client's consent [3]
Consent screen contentWhat the user sees and what will happenName the client, list scopes, show the redirect URI, CSRF and anti-framing protection [1]The user approves an attacker's redirect without seeing it
Browser-bound stateThe consent decision and the callbackState set only after consent, validated at callback, single-use, short-lived [1]A consent the attacker obtained is replayed through the victim's browser [4]
Redirect URIThe client's registration and the code's destinationExact string match, no wildcards, re-register to change [1][5]Codes reach a lookalike or altered URI
Token audienceA token and the one server it is forClients send the resource parameter; servers validate the audience [2][6]A token issued for another service is accepted
No passthroughInbound tokens stay at the proxy; upstream tokens are separateMust not accept or forward tokens not issued to the server [1][2]Downstream APIs trust tokens nobody validated; audit trails show the wrong caller
Issuer per requestAn authorization request and the server that answers itClients that use several authorization servers must defend against mix-up [5]A code or token from an attacker's authorization server is accepted

Consent cookies have their own rules: a __Host- prefix, the Secure, HttpOnly and SameSite=Lax attributes, a signature or server-side session, and binding to the specific client ID rather than to a general fact that the user consented [1]. A cookie that only records that consent happened somewhere rebuilds the original flaw inside your own proxy.

Do not let the client's name carry the decision. With Client ID Metadata Documents, an attacker can present a legitimate client's metadata URL as their client ID with a localhost redirect, so the consent screen shows the real client's name; authorization servers must display the redirect URI's hostname [2]. In the opening example, the redirect host, plainly not the company's, is what Dana needed to see. Teach users that the redirect host is the part that matters.

Can a token issued for another service get through?

Consent reuse moves authority from one client to another. Token passthrough moves it from one service to another. The guidance defines it as an MCP server accepting a client's token without validating that it was issued to the server, and passing it to the downstream API [1]. The listed risks: audience-dependent rate limits and validation are bypassed, the server cannot tell clients apart, downstream logs show the wrong identity, and a stolen token can use the server as an exfiltration proxy [1].

Two tokens, two audiences, no passthroughFig. 2
TOKEN A · AUD = PROXYTOKEN B · AUD = APIMCP clientholds token AMCP proxyvalidates A, holds BUpstream APItrusts only BPASSTHROUGH OF TOKEN A · MUST NOTeach hop has its own token for its own audience · a proxy that forwards the token it received erases who the caller was

The client's token is issued for the proxy and stops there; the proxy holds a separate token issued for the upstream API. Forwarding the client's token upstream, the dashed path, is forbidden because it erases who the caller was.

The specification closes this from both ends [2]. Clients must send the RFC 8707 resource parameter, naming the MCP server's canonical URI, in authorization and token requests. Servers must validate that tokens were issued for them and must not accept or transit any others. An upstream call uses a separate token from the upstream authorization server. RFC 8707 exists so an authorization server can audience-restrict tokens to the resource named [6], and RFC 9700 says access tokens should be audience-restricted to a specific resource server, which limits what a leaked token can do [5].

Would we see the attack while it happened?

Only if the proxy logs the right joins. Record every client registration with its redirect URIs, every consent decision with user, client ID and scopes, and every authorization code issued with the client it went to, never logging codes, tokens or full callback URLs. Then:

  • Check that every issued code maps to a consent event for the same user and client. A code without matching consent is the signature of this attack.
  • Alert when a newly registered client receives a code for a user within minutes of registration, especially from a redirect host not seen before.
  • Count tokens rejected for the wrong audience. A sudden rise means someone is presenting tokens issued for another service.
  • Track upstream actions per MCP client ID, not only per user, so investigators can tell which client used the access.

What tests prove the bindings hold?

  • Authorize client A for a test user, register client B with a different redirect host, and start a flow for B in the same browser. The proxy should show its own consent page naming B and its redirect host before contacting the third party [1].
  • Authorize your own client, capture the third-party authorization URL, and open it in a second test user's browser. The callback should reject it because the state is not bound to that browser [4].
  • Change one character of a registered redirect URI, replay a state value, and send a callback with no state. All three should be rejected [1][5].
  • Present a token issued for a different resource and expect a 401; present a raw third-party token and confirm it is neither accepted nor forwarded [2].
  • Revoke a client's consent and confirm its tokens stop working and a new flow asks again.
  • If you use Client ID Metadata Documents, present a legitimate client's metadata URL with a localhost redirect and confirm the consent page shows the redirect host prominently [2].

If every test passes, is the access appropriate?

No. Passing says the tested proxy and configuration keep the bindings. It does not cover provider behavior you have not exercised, and a correct OAuth flow still gives the agent everything the user's grant allows. The bindings live in the proxy's OAuth code and have to be fixed there. What sits around the connection is a separate question. In Intertrace, tenancy comes from the runtime key a client presents, not the request body, the first call to an unapproved MCP server asks a person once, and every tool call is decided with its arguments, with outbound messages and admin changes always asking. Each decision records the rule and layer behind it, so an investigator can see which agent used a connection and when.

Sources

Reporting, research and specifications this piece draws on. Links go to third-party sources; analysis and recommendations are Intertrace’s own.

  1. 01Security Best Practices (specification version 2025-11-25) · Model Context Protocol
  2. 02Authorization (specification version 2025-11-25) · Model Context Protocol
  3. 03FastMCP Auth Integration Allows for Confused Deputy Account Takeover (GHSA-c2jp-c369-7pvx) · GitHub Advisory Database
  4. 04FastMCP: Missing Consent Verification in OAuth Proxy Callback Facilitates Confused Deputy Vulnerabilities (CVE-2026-27124) · GitHub Advisory Database
  5. 05RFC 9700: Best Current Practice for OAuth 2.0 Security · IETF
  6. 06RFC 8707: Resource Indicators for OAuth 2.0 · IETF

About this desk

Intertrace Threat Research

Threat research

How attacks on AI agents work in practice, from public incidents and our own test corpora, and the controls that stop them.

All 18 pieces by Intertrace Threat Research

Dated October 16, 2026 · added to intertrace.ai October 17, 2026

+ Runtime enforcement

See these controls on your own agent traffic.

Intertrace sits inline between your agents and the models, tools and MCP servers they call, and decides every call before it runs.

Keep reading

All insights →