Model Context Protocol · AI Security

Official MCP Python SDK Let Malicious Servers Steal OAuth Secrets, and Upgrading Isn't Enough

Data graphic: MCP server steals OAuth secrets. CVSS 7.5 High 6.5 for the interactive provider ; affected mcp 1.9.1–1.29.1 and 2.0.0–2.1.1, fixed in 1.30.0 / 2.2.0, M2M providers must also pass issuer=. An attack path runs from the MCP client to a malicious MCP server to the auth server it picked, which receives the client_secret and PKCE verifier.
OP

AI security researcher · Updated Oct 2, 2026, 7:33 AM EDT

A flaw in the official MCP Python SDK let a hostile MCP server redirect OAuth client secrets. Fixed in 1.30.0 and 2.2.0, but M2M providers also need issuer=.

The official Python SDK for the Model Context Protocol let a malicious MCP server decide where a client's OAuth credentials went. A client built on the mcp package that connected to a hostile or compromised server over HTTP could be steered into sending its client_secret, its authorization code and its PKCE code_verifier to a token endpoint the attacker controlled. With PrivateKeyJWTOAuthProvider, it handed over a signed client assertion instead.

The maintainers published advisory GHSA-qx49-fqc8-xw99 on 28 September 2026, rated High at CVSS 7.5. No CVE had been assigned as of 29 September. The fixed releases are mcp 1.30.0 for the 1.x line and 2.2.0 for the 2.x line.

The part most teams will miss is that upgrading is not the whole fix. For the two machine-to-machine providers, ClientCredentialsOAuthProvider and PrivateKeyJWTOAuthProvider, the advisory is blunt: "upgrading changes nothing until you also pass issuer=". Those are the providers agent backends and unattended pipelines use, and they are the ones the 7.5 score was calculated for.

Who is affected

You are exposed if both of these are true:

  • Your application uses the SDK as an MCP client over HTTP, with OAuthClientProvider, ClientCredentialsOAuthProvider, PrivateKeyJWTOAuthProvider or the deprecated 1.x RFC7523OAuthClientProvider as the transport's auth handler.
  • It may connect to an MCP server you do not fully trust while holding credentials for a legitimate authorization server: a pre-provisioned secret or signing key, or a stored client registration.

Affected versions, per the advisory:

LineAffectedFixedWhat was missing
1.x1.9.1 through 1.29.11.30.0No issuer check and no credential binding on any path
2.x2.0.0 through 2.1.12.2.0Both missing on the legacy fallback (no protected resource metadata) and on a 403 insufficient_scope answer
Bothevery version before the fixneeds issuer=The two machine-to-machine providers had no way to name their authorization server

The 1.x exposure dates back to 1.9.1, which reached PyPI on 22 May 2025. The 2.x line was affected from 2.0.0, released on 28 July 2026, through 2.1.1, released on 25 August 2026.

Not affected: MCP servers built with the SDK, stdio clients, and clients that attach their own tokens or headers.

How a server steals the credentials

An MCP client that needs to log in asks the server it is connecting to where its authorization server is. The SDK trusted that answer too much. According to the advisory, it failed in two ways:

  • it did not validate the issuer in the authorization server metadata on every discovery path, and
  • it did not bind stored or pre-provisioned client credentials to the authorization server they belong to.

That gave a hostile server two routes. It could name its own authorization server in its protected resource metadata. Or it could publish no protected resource metadata at all, push the client onto the legacy fallback, and serve metadata that names the user's real authorization server as the issuer while pointing the token endpoint somewhere else. Either way the client built a token request with real credentials and sent it to the attacker.

The Hacker News reported that Cycode, the security firm that reported the flaw, demonstrated the full exchange in a test: with the stolen credentials it requested a valid access token from the real login service, and that token carried whatever permissions the app had been granted. A client secret stays valid until someone rotates it. Handing over the PKCE code_verifier also defeats the protection meant to stop a stolen authorization code being reused.

The severity depends on the provider. The 7.5 score assumes no user interaction, which is how the two machine-to-machine providers run. With the interactive OAuthClientProvider a person has to start the sign-in, and the advisory scores that case at 6.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N). Cycode told The Hacker News that the page the person approves is the genuine login page, so nothing looks wrong.

The advisory lists CWE-345 (insufficient verification of data authenticity) and CWE-522 (insufficiently protected credentials). Neither the advisory nor Cycode reports any attacks using the flaw.

Why upgrading alone leaves agent backends exposed

In 1.30.0 and 2.2.0 the client works out which issuer it expects before it fetches any authorization server metadata, refuses metadata whose issuer differs with OAuthFlowError, and binds stored registrations to that issuer. That closes the hole for the interactive provider, and only for it: three of the four affected provider classes are not fixed by upgrading alone.

ClientCredentialsOAuthProvider and PrivateKeyJWTOAuthProvider are different. They carry a pre-provisioned secret or key and never ask a person anything, so the SDK cannot infer which authorization server those credentials belong to. Until you tell it, the fixed versions still follow whichever authorization server the MCP server advertises.

Data graphic: Upgrading isn't enough. 3 of 4 OAuth provider classes are not fixed by upgrading alone: the interactive OAuthClientProvider is fixed, the two machine-to-machine providers stay exposed until issuer= is passed, and the deprecated RFC7523 provider has no issuer option and must be replaced.

After upgrading to 1.30.0 or 2.2.0, only the interactive provider is protected without a code change. Source: GHSA-qx49-fqc8-xw99.

The release notes add the parameter and a warning when it is missing, but the warning is easy to miss:

  • On 1.30.0 it is a standard DeprecationWarning, which Python hides by default.
  • On 2.2.0 it is an MCPDeprecationWarning, and the 2.2.0 notes say 3.0 will require issuer=.
  • The deprecated 1.x RFC7523OAuthClientProvider has no issuer option at all. The advisory says to move to one of the two providers above.

The fix is one argument. This is the SDK's own example from the 2.2.0 source:

provider = ClientCredentialsOAuthProvider(
    server_url="https://api.example.com",
    storage=my_token_storage,
    client_id="my-client-id",
    client_secret="my-client-secret",
    issuer="https://auth.example.com",
)

With issuer set, the provider only builds a token request from metadata discovered for that exact issuer, and the flow stops with OAuthFlowError if the MCP server leads anywhere else.

A quiet disclosure

The issuer checks shipped on 7 September 2026, in the 1.30.0 and 2.2.0 release notes, listed under "Behaviour changes" rather than as a security fix.

Data graphic: timeline. mcp 2.0.0 released 28 Jul 2026; 2.1.1, the last affected 2.x release, 25 Aug; the fix ships in 1.30.0 and 2.2.0 on 7 Sep; the advisory is published 28 Sep, 21 days later.

The issuer checks shipped on 7 September as a behaviour change; the advisory explaining them came 21 days later. Sources: GitHub releases, PyPI, GHSA-qx49-fqc8-xw99.

The advisory followed 21 days later on 28 September, the same day Cycode published its write-up, and it credits eight reporters. Anyone who upgraded in early September already has the interactive fix; anyone running the machine-to-machine providers still needs the code change.

This is the same trust problem ThreatFrontier covered in The Agent Control Plane and Zero Trust for MCP: an agent that connects to a tool server should not let that server decide where its credentials go.

What to do

  1. Upgrade mcp to 1.30.0 or later on 1.x, or 2.2.0 or later on 2.x (pip install -U mcp).
  2. Pass issuer= to every ClientCredentialsOAuthProvider and PrivateKeyJWTOAuthProvider, set to your authorization server's issuer URL. Search your code for both class names; upgrading without this changes nothing for them.
  3. Replace RFC7523OAuthClientProvider on 1.x with one of those two providers, since it cannot take an issuer.
  4. Turn the warning into an error in CI. Run tests with -W error::DeprecationWarning on 1.x so a provider without issuer= fails the build instead of being silently ignored.
  5. Clear stored registrations once. A registration stored without an issuer stays unbound, including everything 1.x stored before 1.30.0. Clear stored OAuth client information so the client registers again, or set issuer on any pre-registered client record you placed in storage yourself.
  6. Rotate if exposed. If a client may have connected to an untrusted MCP server, rotate its client secret and revoke its tokens at the authorization server.
  7. On older versions, the advisory gives no workaround other than connecting OAuth-enabled clients only to MCP servers you trust.

Sources