Blog

The LiteLLM MCP auth bypass, and the one request that shows it

· 7 min read

On 2 September 2026, CISA added CVE-2026-59822 to its Known Exploited Vulnerabilities catalog. LiteLLM’s MCP endpoint let anyone in who sent a bearer token, and any token would do. The flaw is in LiteLLM’s code, but the symptom is one any MCP server can have, and one request from outside shows it. mcpscore 1.25.0 now sends that request.

What went wrong in LiteLLM

LiteLLM is a gateway: it sits between applications and model providers, and between agents and MCP servers. Its MCP endpoint checks the Authorization header against LiteLLM’s own API keys. Some upstream MCP servers, Atlassian among them, expect their own OAuth token in that same header, so LiteLLM added a fallback: if the header is not a LiteLLM key, treat it as a token for the upstream server and pass it through.

The fallback ran for every server, not only the ones configured for OAuth. In outline, per the advisory:

# The shape of the bug, simplified from the advisory
try:
    identity = await check_litellm_key(authorization)
except AuthError:
    # Maybe it is an OAuth token for an upstream server.
    identity = AnonymousUser()  # every server, not only OAuth ones

A failed key check turned into an anonymous session, so a made-up header could list and call every tool behind the gateway and reach the services connected through it. The fix in 1.84.0 runs the fallback only when every server the request targets is configured for OAuth, and fails closed when the target is unknown.

Why patching to 1.83.9 is not enough

Two more LiteLLM CVEs made the security news in October, and the widely shared advice was to upgrade to at least 1.83.9. That advice is right for those two and leaves the MCP bypass open: CVE-2026-59822 affects every release before 1.84.0, so 1.83.9 through 1.83.14 are still exposed.

Recent LiteLLM CVEs, the release that fixes each, and whether it involves MCP
CVEFlawFixed inCISA KEVMCP
CVE-2026-59822MCP endpoint accepts any bearer token1.84.0YesYes
CVE-2026-59823Server-side request forgery via user_config1.83.9NoNo
CVE-2026-35029Privilege escalation via the config endpoint1.83.0NoNo

The two newer CVEs are not about MCP, and no outside audit can see them. The third is the one CISA has evidence of attackers using, and its symptom is visible from outside.

What the spec requires

The MCP authorization spec has said the same thing since 2025-06-18, in Token Handling, and the 2026-07-28 release keeps it word for word:

Invalid or expired tokens MUST receive a HTTP 401 response.

A server that serves a request carrying a token it never issued has not just answered with the wrong status code. Its auth check did not run, or ran and was ignored. Either way, the cause is inside the server, but the result shows on the wire.

The check: one made-up token

The new rule, auth_invalid_token_rejected, runs on any HTTP server that refuses a request without a token. mcpscore sends the same request again, a tools/list, with Authorization: Bearer mcpscore-invalid-token- followed by a random UUID. No real server issued that token, so the only correct answer is a refusal.

  • Fail: any 2xx. The server served the request, and the rule is HIGH, worth three points.
  • Pass: 401, 403, or another 4xx whose WWW-Authenticate header carries error="invalid_token" (RFC 6750 §3.1). A refusal other than 401 passes with a note, since it misses the MUST without letting anyone in.
  • Skip: anything else, such as a 404 to both requests, a redirect, a rate limit or a 5xx.

The second pass condition is there because of a real server. GitHub Copilot’s MCP server answers a malformed token with HTTP 400 and error="invalid_token", and a token shaped like a real GitHub token with 401. Both are refusals, and failing the first at HIGH would have been a false positive.

The rule only reads. It sends no tools/call, follows no redirect with the token, and if the server opens a session anyway, closes it with a DELETE. If the first answer is an unexplained 4xx, as some older servers give to a request without a session, it repeats the check with an initialize, and counts that only when the same initialize without a token is refused.

See it fail

Our acceptance suite has a test server with the bypass built in. It answers a request without a token with a proper 401 challenge and protected resource metadata, and serves any non-empty bearer token. This is the audit of that server, with no token passed:

uvx mcpscore==1.25.0 'http://127.0.0.1:8094/mcp'
Server requires authentication — running a partial audit of the observable surface.
...
✅ The observed HTTP 401 response carries a non-empty WWW-Authenticate challenge.
...
❌ A fabricated bearer token was accepted: tools/list answered HTTP 200, where the same request without a token gets HTTP 401. Observed HTTP status: 200.
  Fix: Validate every bearer token (signature or introspection, expiry, audience) before handling the request, and answer an invalid one with HTTP 401. A failed check must never fall back to a default identity.
...
Audit finished. PARTIAL score: 13/27 from 10 of 92 checks — not comparable to a full audit.

Without credentials, mcpscore runs a partial audit of what a server behind auth shows to anyone. The new check belongs there: it needs no token, because the point is what happens to a bad one. Switch the test server to accept only its real token and the check passes, with the three points back:

# After: only the server's own token is served
✅ A fabricated bearer token was refused with HTTP 401.
...
Audit finished. PARTIAL score: 16/27 from 10 of 92 checks — not comparable to a full audit.

Before release we ran the rule against the auth-gated servers in our live acceptance set, among them Asana, Linear, Notion, Sentry, Stripe and GitHub Copilot. All of them refused the token.

What the check cannot tell you

A refused made-up token is necessary, not sufficient. Behind the front door, a server can still accept a real token issued for a different service, forward the client’s token to an upstream API, or grant more scope than the token carries. The spec forbids all three, and none of them is visible from outside. Code review and the gateway in front of the server are where those get caught.

The check also needs a refusal to compare against. If your server answers a request without a token with a 500, mcpscore cannot tell it is behind auth, and the rule does not run. Fix that first: a server behind auth should answer a missing token with 401 and a WWW-Authenticate challenge, which mcpscore checks too.

Gateways are where one bug reaches every server

A gateway holds model-provider keys, upstream tokens and access policy for every application behind it, which is why it is worth putting there. It is also why a single fallback in its auth path exposed every connected MCP server at once. Treat LLM and MCP gateways like the API gateways and identity systems they have become: patch them on the advisory, keep them off the public internet unless they need to be there, and check the auth on the gateway and on each server behind it, since a request that gets past one should still be refused by the other.

Check your server

The rule runs on every HTTP audit in mcpscore 1.25.0, on the CLI and on mcpscore.dev. Point it at the URL you give clients, without a token:

uvx mcpscore==1.25.0 'https://your-server.example/mcp'

Add --sarif to put the finding in GitHub code scanning. The token check covers part of MCP07, insufficient authentication, in the OWASP MCP Top 10; the Origin check covers another. Every rule and its citation is in the rules reference, and the release notes list what changed.