In this article
September 23, 2026
September 23, 2026

Four ways MCP authorization breaks in production

The 2026-07-28 spec tells you exactly what to do. These are the code shapes that ignore it.

Explore with AI
Open in ChatGPT
Open in Claude
Open in Perplexity

In April 2026, a pull request landed in LiteLLM that closed an authentication bypass in its MCP endpoint. When the gateway's own key validation failed, an OAuth2 passthrough fallback substituted an empty UserAPIKeyAuth() object instead of rejecting the request. Any fabricated Bearer token produced an authenticated MCP session. No valid key required. An attacker who could reach the route could list and call tools as though they had passed the gate.

That is CVE-2026-59822. It affects every version below 1.84.0, the advisory rates it 8.8 High on CVSS 4.0, and CISA added it to the Known Exploited Vulnerabilities catalog on 2 September 2026 with a 16 September remediation deadline for federal civilian agencies. KEV listings require evidence of active exploitation, not a high score alone.

The CVE itself is not the interesting part. The interesting part is that the bug is one branch of control flow, in a component nobody had classified as an identity boundary, violating a requirement the MCP specification states in plain language.

The 2026-07-28 authorization spec is unusually prescriptive. Servers MUST validate that tokens presented to them were issued for their use. Servers MUST NOT pass through the token received from the client. Clients MUST send the RFC 8707 resource parameter. Clients MUST verify PKCE support before proceeding and MUST refuse to continue if the authorization server metadata omits code_challenge_methods_supported. Very little is left to interpretation.

So when MCP authorization fails in production, it is usually not because the spec was ambiguous. It is because a small number of code shapes keep recurring. Here are four, with the spec clause each one walks past.

1. Auth that degrades open

The LiteLLM bug is the cleanest example of the first shape: a failed authentication check that assigns a default identity instead of raising.

In outline, rather than in LiteLLM's actual source:

  
# Wrong: a failed check falls through to a default identity
try:
    auth = validate_key(request)
except AuthError:
    auth = UserAPIKeyAuth()      # empty, but truthy downstream

# Right: the failure is the answer
try:
    auth = validate_key(request)
except AuthError:
    if not server_config.oauth2_passthrough_enabled:
        raise HTTPException(status_code=401)
    auth = passthrough_identity(request)
  

An empty auth object is not the same thing as no auth object. Downstream code reads it as a caller who simply has no limits attached: no budget, no allowed-models list, no team scoping. The request proceeds with the permissions of nobody in particular, which in practice means the permissions of the process handling it.

This shape shows up wherever passthrough and local auth share a code path. A gateway has to decide whether a credential is for it or merely passing through it, and when it cannot tell, the safe answer is 401. The real fix gates the fallback correctly: a helper named _target_servers_use_oauth2 now permits the empty-auth path only when every resolved target server carries an operator-configured auth_type of oauth2, and routes matching no known server pattern fail closed.

The rule generalizes. A passthrough path is entered because an operator configured it, never because another check failed.

What the spec says: MCP servers MUST validate access tokens before processing the request, and MUST take all necessary steps to ensure no data is returned to unauthorized parties.

2. Public-route checks that match the wrong thing

The same pull request closed a second bypass, which the advisory never mentions.

MCP servers need some routes to be reachable without a token. Protected resource metadata under /.well-known/ has to be fetchable by a client that does not yet have credentials, which is the whole point of discovery. So implementations grow a function that answers "is this route public?"

LiteLLM's version asked whether the string ".well-known" appeared anywhere in the request URL. Appending ?.well-known to any MCP route therefore marked it public. The fix narrows the test to the path component:

  
request.url.path.startswith("/.well-known/")
  
The URL https://mcp.example.com/mcp/tools/call?.well-known broken into scheme, host, path and query string segments. A bracket above spans only the path, labelled as the fixed check request.url.path.startswith("/.well-known/"). A bracket below spans the entire URL, labelled as the naive check ".well-known" in str(request.url), which matches anywhere in the string.

Two things worth taking from this. First, substring matching against a full URL is not a routing decision, because the query string is attacker-controlled and is part of that string. Second, and more useful for triage: you were exposed to this one whether or not you had ever enabled OAuth2 passthrough. "We don't use passthrough" was not a reason to stay on an old version.

If your server has a public-route allowlist, check what it matches against. Path, exact prefix, after normalization. Not the URL.

3. Token passthrough

The first two shapes are mistakes. This one is a decision, which makes it harder to dislodge.

Token passthrough is when an MCP server accepts a token from a client, does not verify that the token was issued for itself, and forwards it unchanged to a downstream API. It is convenient. It means you do not have to stand up an authorization server, and the downstream API already knows how to validate the token.

It is also the only MUST NOT in the authorization spec's security considerations:

"If the MCP server makes requests to upstream APIs, it may act as an OAuth client to them. The access token used at the upstream API is a separate token, issued by the upstream authorization server. The MCP server MUST NOT pass through the token it received from the MCP client."

The failure mode is the confused deputy. Your MCP server holds credentials and network reach that its callers do not. If it will forward whatever token arrives, a token minted for server A can be replayed against server B, and your server becomes the instrument for access the caller was never granted. The spec addresses the proxy variant directly: MCP proxy servers using static client IDs MUST obtain user consent for each dynamically registered client before forwarding to third-party authorization servers.

Two mechanisms stop it, and they work as a pair:

  • Audience binding. Servers MUST only accept tokens that name them in the audience claim, and MUST reject everything else. This is a validation step on the inbound side, and it is cheap.
  • RFC 8707 resource indicators. Clients MUST send the resource parameter so the authorization server knows which resource server the token is for. This is what makes audience binding possible in the first place.

When your server needs to call upstream, the correct pattern is RFC 8693 token exchange: trade the inbound token for a new, audience-bound token scoped to that specific upstream. We walk through the exchange mechanics in more detail in OAuth's On-Behalf-Of flow for AI agents.

Two diagrams comparing token flows. In the forbidden passthrough pattern, the MCP client sends Token A with audience "mcp server" to the MCP server, which forwards the same unchanged token to an upstream API that expects audience "upstream API". In the correct token exchange pattern, the MCP server validates that the audience names itself, exchanges Token A for Token B with audience "upstream API", and sends Token B upstream.

4. A static key is not an identity

The last shape is the most common and the least likely to produce a CVE, because nothing is technically broken.

Astrix Research scanned 5,205 open-source MCP servers in October 2025 and found that 53% rely on static API keys or personal access tokens that never expire, while only 8.5% use OAuth. A separate BlueRock scan of roughly 7,000 public servers found 41% required no authentication at all. The two studies disagree on specifics, because scanning open-source repositories and probing deployed remote servers measure different populations, and scanner false-positive rates in this space are high enough that any single percentage deserves suspicion. The direction is not in dispute.

What the scans actually measured
Study Sample Population measured Finding
Astrix Research
October 2025
5,205 servers Open-source MCP server repositories 53% rely on static API keys or personal access tokens that never expire. 8.5% use OAuth.
BlueRock
2026
~7,000 servers Public, deployed MCP servers 41% required no authentication at all.

A shared static key gives you no per-caller identity, no audience restriction, and no revocation that is not a full outage. You cannot revoke it for one bad actor without breaking every caller at once. You cannot tell from a log line which client made a call. You cannot scope one caller more tightly than another.

This persists because of how MCP servers get built. A team treats the server as plumbing, gives it infrastructure-grade auth, a key in an environment variable and maybe an IP allowlist, and ships. Then the same process starts brokering tool calls into ticketing systems, source control, and databases, and the blast radius widens without anyone revisiting the auth model.

A useful test: what does an attacker get from one successful request to this component? If the answer includes "a tool call against a system of record," it needs an authorization server in front of it, with per-client identity, audience-restricted tokens, and revocation.

The stateless core introduced in 2026-07-28 makes this more pressing, not less. Removing the initialize handshake and the protocol-level session pushes servers toward per-request authorization, and per-request authorization only works if something is actually issuing and validating those tokens.

How to tell whether you were exposed

For the LiteLLM CVE specifically, version is the signal, not the disclosure date. The patch shipped on 14 May 2026, seven weeks before the advisory and sixteen before the CISA listing. If you were below 1.84.0 during that window you were exposed regardless of when you heard about it. Upgrade to 1.84.0 or later, or deny /mcp/ at the edge until you can, then rotate every provider key and downstream credential the gateway could read.

A timeline of CVE-2026-59822 running from 30 April 2026, when pull request 26463 was merged, through 14 May when the fix shipped in version 1.84.0, an advisory in early July, the CISA KEV listing on 2 September, and the federal remediation deadline on 16 September. A shaded band marks the 16 weeks between the fix shipping and the KEV listing.

For your own servers, five questions:

  1. Does any authentication path assign a default or empty identity when a check fails, rather than returning 401?
  2. Does any public-route check run against the full URL rather than the normalized path?
  3. Does the server validate the aud claim, and reject tokens that do not name it, before processing the request?
  4. When the server calls upstream, does it forward the inbound token, or exchange it for an audience-bound one?
  5. Can you revoke access for one caller without breaking the others?

The first two are grep-able in an afternoon. The last three are architectural, and the answers tend to arrive together.

The gap is not the spec

MCP authorization has a well-specified correct answer. The requirements are written as MUSTs, the underlying RFCs are mature, and the 2026-07-28 revision tightened the parts that were previously vague. What the ecosystem does not have is a large population of teams who have stood up an authorization server before. MCP servers are being built by application developers, at speed, in a protocol where the fastest path to working code is a static key and a permissive default.

That is the gap these four shapes live in.

If you are building MCP servers and want per-client identity, audience-bound tokens, and revocation without writing an authorization server, that is what AuthKit does. If you are working on the enterprise side of this, where an admin authorizes the server once in the company IdP and every employee's client connects without a consent screen, start with Inside the ID-JAG, which covers the validation steps Enterprise-Managed Authorization adds on top of everything here.

Sources