Summary
In affected versions, the SDK's OAuth client support (mcp.client.auth) let the MCP server a client connected to decide where the client's OAuth credentials were sent:
- the authorization server metadata
issuer was not validated on every discovery path, and
- stored or pre-provisioned client credentials were not bound to the authorization server they belong to.
A malicious or compromised MCP server could therefore direct them to a token endpoint of its choosing:
- by naming its own authorization server in its protected resource metadata, or
- by publishing none and serving metadata that presents the user's real authorization server as the
issuer.
It would then receive the client_secret, authorization code and PKCE code_verifier meant for that real server (or, with PrivateKeyJWTOAuthProvider, a signed client assertion).
Am I affected?
Yes, if both of these hold:
- 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:
- 1.9.1 through 1.29.1: no
issuer check and no credential binding on any path.
- 2.0.0 through 2.1.1: both missing whenever the server publishes no protected resource metadata (the legacy fallback) or answers
403 insufficient_scope.
- Both lines, on every path:
ClientCredentialsOAuthProvider and PrivateKeyJWTOAuthProvider had no way to name the authorization server their credentials belong to.
Not affected:
- MCP servers built with the SDK
- stdio clients
- clients that attach their own tokens or headers
The rating is scored for the unattended providers. With the interactive OAuthClientProvider a person has to start the sign-in; scored that way it is 6.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N).
Fix
Upgrade to mcp 2.2.0 (2.x) or 1.30.0 (1.x) or later.
If you use ClientCredentialsOAuthProvider or PrivateKeyJWTOAuthProvider, upgrading changes nothing until you also pass issuer= (for example issuer="https://auth.example.com"):
- Without it they still follow whichever authorization server the MCP server advertises.
- Omitting it now emits a deprecation warning, and it becomes required in 3.0. On 1.30.0 it is a
DeprecationWarning, which Python hides by default.
- The deprecated 1.x
RFC7523OAuthClientProvider has no issuer option. Move to one of these two providers.
In the fixed versions the client also:
- works out which issuer it expects before fetching any authorization server metadata, on every path;
- refuses metadata whose
issuer differs, with OAuthFlowError;
- binds stored registrations to that issuer, and discards one bound to a different issuer so the client registers again.
After upgrading:
- Stored registrations: a registration stored without an issuer stays unbound. That includes everything 1.x stored before 1.30.0. Clear stored OAuth client information once so the client registers again. If you placed a pre-registered client in storage yourself, set
issuer on that record instead.
- Past exposure: if such a client may have connected to an untrusted MCP server, rotate its client secret and revoke its tokens at the authorization server.
On earlier versions there is no workaround other than connecting OAuth-enabled clients only to MCP servers you trust.
Summary
In affected versions, the SDK's OAuth client support (
mcp.client.auth) let the MCP server a client connected to decide where the client's OAuth credentials were sent:issuerwas not validated on every discovery path, andA malicious or compromised MCP server could therefore direct them to a token endpoint of its choosing:
issuer.It would then receive the
client_secret, authorization code and PKCEcode_verifiermeant for that real server (or, withPrivateKeyJWTOAuthProvider, a signed client assertion).Am I affected?
Yes, if both of these hold:
OAuthClientProvider,ClientCredentialsOAuthProvider,PrivateKeyJWTOAuthProvideror the deprecated 1.xRFC7523OAuthClientProvideras the transport'sauthhandler;Affected versions:
issuercheck and no credential binding on any path.403 insufficient_scope.ClientCredentialsOAuthProviderandPrivateKeyJWTOAuthProviderhad no way to name the authorization server their credentials belong to.Not affected:
The rating is scored for the unattended providers. With the interactive
OAuthClientProvidera person has to start the sign-in; scored that way it is 6.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N).Fix
Upgrade to
mcp2.2.0 (2.x) or 1.30.0 (1.x) or later.If you use
ClientCredentialsOAuthProviderorPrivateKeyJWTOAuthProvider, upgrading changes nothing until you also passissuer=(for exampleissuer="https://auth.example.com"):DeprecationWarning, which Python hides by default.RFC7523OAuthClientProviderhas noissueroption. Move to one of these two providers.In the fixed versions the client also:
issuerdiffers, withOAuthFlowError;After upgrading:
issueron that record instead.On earlier versions there is no workaround other than connecting OAuth-enabled clients only to MCP servers you trust.