Both Official MCP SDKs Let a Server Pick Where Your OAuth Credentials Go - CVE-2026-104850 and the Python Twin
- Two matching advisories from the MCP project: GHSA-qx49-fqc8-xw99 for the Python SDK (
mcp, published September 28, 2026, no CVE) and GHSA-6qxp-vccf-f47h for the TypeScript SDK (CVE-2026-104850, published September 30). Both are High, CVSS 7.5, and both describe the same flaw: the OAuth client let the MCP server decide which authorization server received the client's credentials. - A malicious or compromised MCP server could name its own authorization server, or publish no protected resource metadata and serve metadata presenting the user's real server as the
issuer. The client then sent it a client secret, an authorization code, a PKCE verifier, a signed client assertion or, in TypeScript, a refresh token from an earlier sign-in, without user interaction. - Affected: Python
mcp1.9.1 through 1.29.1 and 2.0.0a1 through 2.1.1; TypeScript@modelcontextprotocol/sdk1.12.0 to before 1.31.0 and@modelcontextprotocol/client2.0.0 to before 2.2.0. Fixed in 1.30.0 / 2.2.0 (Python) and 1.31.0 / 2.2.0 (TypeScript). MCP servers built with the SDKs and stdio clients are not affected. - Upgrading is not the whole fix. The client-credentials and private-key-JWT providers need an explicit
issuer=(Python) orexpectedIssuer(TypeScript), or they still follow whatever server the MCP server advertises. Credentials stored without an issuer, which is everything a 1.x client saved before the fix, stay unbound until cleared or annotated. - If an affected client may have connected to an untrusted MCP server, rotate its client secret or signing key and revoke its tokens. The SDKs cannot tell you whether that happened; only an inventory of which servers each client talks to can.
The short version: until the end of September, an MCP client built on either official SDK would hand its OAuth credentials to whatever authorization server the MCP server told it to use. The Python SDK's advisory, GHSA-qx49-fqc8-xw99, was published on September 28, 2026, and the TypeScript SDK's, GHSA-6qxp-vccf-f47h with CVE-2026-104850, two days later. Both are High, CVSS 7.5, both are fixed, and both say in their own text that upgrading alone does not finish the job.
We covered OAuth for MCP servers when the authorization flow was new. This is the first flaw in that flow to land in the reference client code that most agent hosts build on, rather than in one server. That is what makes it worth a post two weeks after the advisories: the affected population is not users of one package, it is every HTTP MCP client that inherited the SDK's OAuth client and lets its users add remote servers.
What to do now
- Upgrade: Python
mcpto 1.30.0 (1.x) or 2.2.0; TypeScript@modelcontextprotocol/sdkto 1.31.0 or@modelcontextprotocol/clientto 2.2.0 (plus@modelcontextprotocol/core2.2.0 if imported directly). - Pin the issuer on the bundled credential providers:
issuer=forClientCredentialsOAuthProviderandPrivateKeyJWTOAuthProviderin Python;expectedIssuerforClientCredentialsProvider,PrivateKeyJwtProvider,StaticPrivateKeyJwtProviderandCrossAppAccessProviderin TypeScript. Replace the deprecated PythonRFC7523OAuthClientProvider, which has no issuer option. - Clear or annotate stored credentials. Registrations and tokens saved without an issuer stay unbound after the upgrade, including everything a 1.x client saved before it. Clearing them forces a fresh sign-in; a custom provider should save
issueralongside tokens and client information. - Rotate if exposure is possible. If a client may have connected to an untrusted MCP server while holding credentials, rotate its client secret or signing key and revoke its tokens at the authorization server.
- Keep interactive sign-ins to trusted servers. The fix does not change where a new sign-in goes.
How the redirection works
An MCP client connecting to an HTTP server with OAuth discovers where to authenticate from metadata the server publishes. The SDKs made two mistakes in that discovery. First, the issuer in the authorization server metadata was not validated on every path. Second, credentials the client already held, whether a secret provisioned at build time or a registration saved from an earlier sign-in, were not bound to the authorization server they came from. Two routes followed from that:
- The malicious server names its own authorization server in its protected resource metadata, and the client goes there with the credentials it holds.
- The malicious server publishes no protected resource metadata, which drops the client into a legacy fallback, and then serves authorization server metadata that presents the user's real server as the
issuer. The client believes it is talking to the legitimate issuer and sends it what it has.
What arrives at the attacker's token endpoint depends on the provider in use: a client_secret, an authorization code, a PKCE code_verifier, a signed client assertion from the private-key-JWT provider, and in the TypeScript SDK the refresh token and client secret from an earlier sign-in, sent without any user interaction. The Python advisory scores it 7.5 with no user interaction and notes a 6.5 variant for the interactive provider, where a person has to start the sign-in. In the 2.x Python line the gap was confined to the legacy fallback and to a 403 insufficient_scope response; in 1.x it was every path.
What the fixed versions do, and what they leave to you
The fixed clients determine the expected issuer before fetching any authorization server metadata, refuse metadata whose issuer differs (Python raises OAuthFlowError), and bind stored registrations and tokens to an issuer, discarding a registration bound to a different one so the client registers again. The Python 1.x release notes add that a 5xx or 429 on protected resource metadata now stops the flow rather than falling back.
The part that will be missed is the configuration. The bundled client-credentials and private-key-JWT providers had no field for the authorization server their credentials belong to, so the fix adds one and asks you to fill it in. Omit it in Python and you get a DeprecationWarning, which the interpreter hides by default; it becomes mandatory in 3.0. Credentials already in storage carry no issuer and stay unbound until cleared. And the TypeScript advisory lists three things that remain outside the fix: a new interactive sign-in still goes wherever the MCP server points, direct calls to refreshAuthorization() and exchangeAuthorization() are not covered, and skipIssuerMetadataValidation: true switches the check off. A team that bumps the dependency and ships has done the smaller half.
The two ends of one trust problem
This autumn's HTTP MCP advisories sort into two piles. On the server side, the LiteLLM MCP endpoint accepted invalid bearer tokens and MCP-Atlassian fell back to global credentials, a class we wrote up: an unauthenticated caller gets someone else's access. On the client side, this pair: the client gives its own secrets to the wrong party. Gateways that pass OAuth through sit in the middle and inherit both. The 2026-07-28 specification tightened the authorization text; the reference clients needed to catch up with it, and now have.
What neither advisory can tell you is whether the condition that makes the flaw exploitable ever held in your environment: an affected client, holding real credentials, connected to a server you did not control. Server configurations live in agent settings on developer machines, added one at a time, and a registry entry that changes hands turns a trusted server into an untrusted one without anyone editing a file. The rotation decision rests on an inventory question.
How Anomity surfaces and governs it
See it. The Anomity Endpoint Sensor inventories the MCP servers configured in every agent on every managed machine, including the remote HTTP servers each client is pointed at, so "which clients could have reached a server we do not control, and which credentials do we therefore rotate" is a query rather than a survey. Cloud discovery covers the Google Workspace and GitHub OAuth grants held by AI applications, the other place a stolen agent credential shows up. Metadata only; secrets are redacted on the endpoint.
Enforce before the call runs. On agents that expose a pre-execution hook, such as Claude Code PreToolUse, Anomity can allow, deny or log a tool call before it runs, which is a control on what a connected server can make the agent do rather than on the OAuth handshake itself. Keep the record. Configuration changes and decisions land in a 90-day audit trail and route to SIEM, Slack, email or Jira. Anomity is SOC 2 Type II and complements gateway, EDR, DLP and GRC tooling; it does not replace them. For the full control set, see MCP security explained, and to see which remote MCP servers your fleet actually talks to, book a 30-minute demo.
Frequently asked questions
What is the flaw in one sentence?
The OAuth client in both official MCP SDKs trusted the MCP server it was connecting to for the identity of the authorization server, and did not bind stored or pre-provisioned credentials to the server they belonged to, so a hostile MCP server could redirect the client's secrets to a token endpoint it controlled.
Which versions are affected and which are fixed?
Python: mcp 1.9.1 through 1.29.1 on every discovery path, and 2.0.0a1 through 2.1.1 on the legacy fallback path (no protected resource metadata) and on 403 insufficient_scope responses. Fixed in 1.30.0 and 2.2.0. TypeScript: @modelcontextprotocol/sdk 1.12.0 up to but not including 1.31.0, and @modelcontextprotocol/client 2.0.0 up to but not including 2.2.0. Fixed in 1.31.0 and 2.2.0, with @modelcontextprotocol/core 2.2.0 if you import it directly. Servers built with either SDK and clients using stdio transport are not affected.
Who is actually exposed?
An application is exposed when both conditions hold: it uses the SDK as an HTTP MCP client with the OAuth client provider or one of the bundled credential providers as its auth handler, and it may connect to an MCP server it does not fully trust while holding credentials for a legitimate authorization server, whether a pre-provisioned secret or signing key or a client registration saved from an earlier sign-in. That describes most agent hosts that let users add remote MCP servers.
Why is upgrading not enough?
Three reasons the advisories spell out. The client-credentials and private-key-JWT providers had no way to name the authorization server their credentials belong to, so after the upgrade they need an explicit issuer, passed as issuer= in Python or expectedIssuer in TypeScript; in Python omitting it only emits a DeprecationWarning, which the interpreter hides by default, and it becomes required in 3.0. Credentials and registrations stored without an issuer remain unbound, including everything a 1.x client saved before the fix, so they must be cleared so users sign in again, or annotated with the issuer. And the deprecated Python RFC7523OAuthClientProvider has no issuer option at all, so it has to be replaced.
What does the fix not cover?
The TypeScript advisory is explicit. A new interactive sign-in still goes to whatever authorization server the MCP server names, so complete one only with a server you trust. Direct calls to refreshAuthorization() and exchangeAuthorization() are outside the fix, as is 2.x running with skipIssuerMetadataValidation set to true. The fix binds existing credentials to their issuer; it does not make an untrusted server safe to sign in to.
Is this related to the LiteLLM and MCP-Atlassian authentication bypasses?
They are neighbours, not the same bug. The LiteLLM MCP endpoint accepted invalid bearer tokens and MCP-Atlassian fell back to global credentials over HTTP; those are server-side failures that let an unauthenticated caller use someone else's access. This is a client-side failure: the client gives its own credentials to the wrong party. Together they describe the two ends of the same trust problem in HTTP MCP, which the 2026-07-28 specification's authorization work was meant to tighten.
How does Anomity help with this?
The Anomity Endpoint Sensor inventories the MCP servers configured in every agent on every managed machine, including the remote HTTP servers each client is pointed at, so the question of which clients could have reached an untrusted server, and therefore which credentials to rotate, is a query rather than a survey. Cloud discovery covers the Google Workspace and GitHub OAuth grants held by AI applications. On agents that expose a pre-execution hook, such as Claude Code PreToolUse, Anomity can allow, deny or log a call before it runs, and decisions land in a 90-day audit trail that routes to SIEM, Slack, email or Jira.




