---
id: "CVE-2026-115563"
page: "https://infrastack.ai/live/CVE-2026-115563"
kind: "vulnerability"
aliases: ["CVE-2026-115563", "GHSA-qx49-fqc8-xw99"]
title: "OAuth client could send credentials to an authorization server chosen by the MCP server"
severity: "high"
cvss: 7.5
epss: null
exploited: false
status: "open"
category: "package"
products: [{"name": "mcp", "fixed": ["2.2.0"], "affected": ">= 2.0.0a1, < 2.2.0", "ecosystem": "pypi"}, {"name": "mcp", "fixed": ["1.30.0"], "affected": ">= 1.9.1, < 1.30.0", "ecosystem": "pypi"}]
ecosystems: ["pypi"]
tags: ["cwe-345", "cwe-522", "mcp", "oauth", "package", "python"]
not_affected_if: ["mcp-server-only", "own-tokens-or-headers", "stdio-transport", "trusted-mcp-servers-only"]
published_at: "2026-09-28T20:06:03+00:00"
first_seen_at: "2026-11-09T22:53:24+00:00"
updated_at: "2026-11-09T22:59:26+00:00"
sources: [{"source": "ghsa-repo", "url": "https://github.com/modelcontextprotocol/python-sdk/security/advisories/GHSA-qx49-fqc8-xw99", "first_seen_at": "2026-11-09T22:53:24+00:00"}]
exploit_sources: []
---

# OAuth client could send credentials to an authorization server chosen by the MCP server

Link for people: https://infrastack.ai/live/CVE-2026-115563 (this .md is for agents)

The MCP Python SDK's OAuth client did not check the authorization server issuer or bind stored client credentials to it, so a malicious or compromised MCP server could redirect the client's client_secret, authorization code, PKCE verifier or client assertion to a token endpoint it controls. Affected applications may lose the credentials they hold for a legitimate authorization server.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- pypi:mcp affected >= 2.0.0a1, < 2.2.0 fixed 2.2.0
- pypi:mcp affected >= 1.9.1, < 1.30.0 fixed 1.30.0

## How to check

- Run `pip show mcp` (or `uv pip show mcp`) and compare the version against the affected ranges: `1.9.1` through `1.29.1`, and `2.0.0` through `2.1.1`.
- Look for uses of `OAuthClientProvider`, `ClientCredentialsOAuthProvider`, `PrivateKeyJWTOAuthProvider` or `RFC7523OAuthClientProvider` as the `auth` handler of an HTTP MCP client transport.
- Check whether such a client connects to MCP servers you do not fully trust while holding a pre-provisioned client secret, signing key, or stored client registration.

## Fix

- `mcp` 2.x: upgrade to `2.2.0` or later.
- `mcp` 1.x: upgrade to `1.30.0` or later.
- For `ClientCredentialsOAuthProvider` and `PrivateKeyJWTOAuthProvider`, also pass `issuer=` (for example `issuer="https://auth.example.com"`); upgrading alone does not bind credentials to the issuer.
- `RFC7523OAuthClientProvider` has no `issuer` option; move to one of the two providers above.

## Timeline

- 2026-11-09T22:53:24+00:00 ghsa-repo https://github.com/modelcontextprotocol/python-sdk/security/advisories/GHSA-qx49-fqc8-xw99

## Documents

- [advisory] Maintainer's advisory: github.com https://github.com/modelcontextprotocol/python-sdk/security/advisories/GHSA-qx49-fqc8-xw99

## From the sources

### 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.

---

_Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai_
