Skip to content
Betav0.62.0
All articles

MCP TypeScript SDK OAuth flaw: credentials followed the wrong issuer

A high-severity MCP TypeScript SDK flaw let the server choose which authorization server got client secrets. Patch, bind and rotate.

By The SpanDock team

4 min read

Diagram of an MCP client sending OAuth credentials to an attacker authorization server named by a malicious MCP server

On 30 September 2026 the Model Context Protocol TypeScript SDK maintainers published advisory GHSA-6qxp-vccf-f47h: in affected versions the OAuth client let the MCP server decide which authorization server received the client's OAuth credentials, and those credentials were not tied to the issuer they belonged to. The rating is CVSS 7.5 high (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N). Say high, not critical. Upgrading alone is not enough if stored tokens lack an issuer or bundled providers omit expectedIssuer.

Context: what was wrong

The advisory says a malicious or compromised MCP server could name its own authorization server. With no user interaction, the client would send it the refresh_token and client_secret from an earlier sign-in, or the client_secret or signed assertion configured on a bundled provider.

Affected packages and versions:

  • @modelcontextprotocol/sdk 1.12.0 through 1.30.1 (patched in 1.31.0+)
  • @modelcontextprotocol/client 2.x before 2.2.0 for bundled providers without expectedIssuer, credentials without issuer, direct fetchToken() calls, and providers that read storage through certain schemas (patched in 2.2.0+, with @modelcontextprotocol/core 2.2.0+ if imported directly)

You are in scope if the application uses the SDK's OAuth client over HTTP (authProvider, withOAuth(), auth() or fetchToken()) and may connect to an MCP server you do not fully trust while holding credentials for a legitimate authorization server.

Not affected: MCP servers built with the SDK, and stdio clients.

A Cloud Security Alliance research note on a related Python SDK OAuth pattern (also dated 30 September 2026) describes the same structural risk: the MCP server is often the first party to tell the client where to send credentials, and SDKs that treat that metadata as trustworthy by default break down once agents connect to third-party servers. That note is about the Python OAuth discovery path, not this TypeScript advisory; use it for the pattern, not for TypeScript version numbers.

Analysis: patch is necessary but not sufficient

The fixed client records the authorization server as issuer on saved credentials and does not send them to a different one. The user signs in again, or the call throws.

The advisory is clear that upgrading is not enough in several cases:

  • Bundled providers (ClientCredentialsProvider, PrivateKeyJwtProvider, StaticPrivateKeyJwtProvider, CrossAppAccessProvider) still need expectedIssuer (for example expectedIssuer: 'https://auth.example.com'). Without it they still use whichever authorization server the MCP server names.
  • Credentials saved without issuer (including everything 1.x saved before 1.31.0) still go to whichever server is named at first use. Add issuer, or clear them so users sign in again.
  • Custom OAuthClientProvider implementations must persist issuer from saveTokens() / saveClientInformation() and include it for pre-registered credentials.

Not covered by the fix: a new interactive sign-in still goes to the authorization server the MCP server names; only complete sign-ins for servers you trust. Direct calls to refreshAuthorization() / exchangeAuthorization(), and 2.x with skipIssuerMetadataValidation: true, are also called out.

If an affected client may have connected to an untrusted MCP server, rotate the client secret or signing key and revoke tokens.

This is a different failure mode from the Python SDK's cross-origin redirect of custom headers (advisory GHSA-5h93-6whr-6q8j). Do not merge the two into one "MCP OAuth" story in a risk register.

After the upgrade, operators still have work that a version bump alone does not finish. Bundled providers that omit expectedIssuer continue to trust whatever issuer the MCP server advertises. Stored credentials that were saved without an issuer binding should be cleared and obtained again from a known authorization server. If any untrusted MCP server was connected while the vulnerable range was installed, treat client secrets and refresh tokens as compromised and rotate them.

Interactive first-time sign-in remains a separate trust decision: the user may still be sent to an authorization server named by the MCP server. The patch binds stored credential reuse more tightly; it does not make every new OAuth handshake safe against a malicious server. Only connect MCP servers your organisation has reviewed, and prefer a gateway that holds connector credentials off the laptop.

What it means for engineering and security leaders

MCP clients are OAuth clients with a hostile metadata path. Conventional OAuth often hard-codes the issuer. MCP clients learn an issuer from a server that may be untrusted. That is closer to the SSRF and untrusted-URL problems in MCP servers and SSRF than to a normal SaaS login bug.

Inventory HTTP OAuth clients, not only servers. Teams that "only run MCP servers" are out of scope for this advisory. Teams that ship IDE plugins, agent hosts or gateways that speak OAuth to remote MCP servers are in scope.

Treat connector allowlists as credential policy. After the 2026-07-28 MCP specification changes for operators, issuer binding belongs next to tool permissions and approval flows.

Inventory every @modelcontextprotocol/sdk and @modelcontextprotocol/client pin. Agent harnesses, IDE extensions and internal MCP clients often pin the SDK transitively. A single outdated leaf package is enough.

Treat issuer metadata as untrusted input. The same habit that stops SSRF against internal URLs should stop OAuth clients from sending secrets to whatever authorization_servers field a remote MCP server returns.

Practical takeaways for this week

  • Upgrade @modelcontextprotocol/sdk to 1.31.0+ or @modelcontextprotocol/client (and core if needed) to 2.2.0+.
  • Set expectedIssuer on every bundled provider; fail closed if metadata names another issuer.
  • Clear or migrate tokens stored without issuer; force re-auth where you cannot migrate.
  • Rotate client secrets and revoke refresh tokens for any client that may have contacted an untrusted MCP server.
  • Restrict OAuth MCP clients to servers you trust until the upgrade and rotation are done.
  • Separate this advisory from Python redirect-header work so owners and due dates stay distinct.

Where SpanDock fits

The SpanDock MCP gateway gives every computer's AI tools one MCP server on SpanDock, connects on your server to the organisation's connectors, keeps connector credentials on the server, and records every call. The MCP gateway is on the Premium and Enterprise plans, and in the 30-day trial. Pair that with privacy expectations for where secrets live. For a first connector rollout, see set up the MCP gateway.

Sources

  1. OAuth client could send credentials to an authorization server chosen by the MCP server (GHSA-6qxp-vccf-f47h)
  2. The MCP Python SDK OAuth Flaw and the Cost of Implicit Trust (CSA research note)
Cookie categories

Details: Cookie Policy.