Rightshold™ Sign up

Answers · MCP security

Our app is an MCP client that signs users in with OAuth and connects to MCP servers we don't all control. Could a malicious or compromised MCP server get our stored refresh tokens or client secret, and what do the TypeScript SDK fixes and the 2026-07-28 spec change about that?

Last updated

TL;DR: Yes, with an unpatched client it could, and without the user doing anything. In MCP the server tells the client which authorization server to use, and the MCP TypeScript SDK's advisory GHSA-6qxp-vccf-f47h (CVE-2026-104850, high, published September 30, 2026) says that before the fix, credentials weren't tied to the authorization server they belong to: a malicious or compromised MCP server could name its own authorization server, and the client would send it the refresh_token and client_secret stored from an earlier sign-in, or the client_secret or signed assertion configured on a bundled provider. It's fixed in @modelcontextprotocol/sdk 1.31.0 and @modelcontextprotocol/client 2.2.0, which record the authorization server as issuer on saved credentials and refuse to send them anywhere else, but credentials you saved before upgrading, bundled providers without expectedIssuer and custom storage still need changes. A second advisory, GHSA-6prh-2h8m-c8cw (CVE-2026-111769), covers redirects: before sdk 1.32.0 and client 2.3.0, a cross-origin 307 or 308 re-sent custom headers such as an X-API-Key, the mcp-session-id and OAuth token request bodies with their refresh_token and client_secret. The 2026-07-28 MCP spec now makes issuer binding a rule for every client, not just this SDK.

What should I check, step by step?

1. Upgrade to @modelcontextprotocol/sdk 1.32.0 or later (1.x), which covers both the issuer and redirect fixes, or @modelcontextprotocol/client 2.3.0 or later (2.x), plus @modelcontextprotocol/core 2.2.0 or later if you import it directly.

  1. Find credentials your OAuthClientProvider saved before the upgrade (files, keychain, database), including everything 1.x saved before 1.31.0. They have no issuer, so they still go to whichever authorization server is named at first use. Add the issuer to each record or clear them so users sign in again.
  2. If you use ClientCredentialsProvider, PrivateKeyJwtProvider, StaticPrivateKeyJwtProvider or CrossAppAccessProvider, pass expectedIssuer, for example expectedIssuer: 'https://auth.example.com'. Without it they still use whichever authorization server the MCP server names.
  3. If you wrote your own OAuthClientProvider, save exactly what saveTokens() and saveClientInformation() are given, including issuer, and include issuer in what clientInformation() returns for pre-registered credentials.
  4. Don't set redirectPolicy: 'follow' on transports. If a server or authorization server redirects to another origin on purpose, configure the final URL instead. On versions you can't upgrade yet, pass a fetch that refuses redirects: (url, init) => fetch(url, { ...init, redirect: 'error' }).
  5. Avoid calling refreshAuthorization() or exchangeAuthorization() directly and don't run 2.x with skipIssuerMetadataValidation: true; the advisory lists those as not covered by the fix.
  6. Treat a new interactive sign-in as a trust decision. The fix doesn't cover it: a first sign-in still goes to the authorization server the MCP server names, so only complete sign-ins for servers you trust, and show the user which authorization server they're about to sign in to.
  7. If an affected client may have connected to an MCP server you don't fully trust, or a redirect may have reached a host you don't control, rotate the client secret or signing key and any API keys it held, and revoke its tokens.

How could a malicious MCP server get an MCP client's refresh token or client secret?

An MCP server that needs authorization points the client at its authorization server through OAuth 2.0 Protected Resource Metadata (RFC 9728); the MCP authorization spec says clients MUST use that metadata for authorization server discovery. That's useful for honest servers and means the server, not the client, chooses the destination.

What does the MCP 2026-07-28 spec require for issuer validation and issuer-bound credentials?

The 2026-07-28 changelog clarifies that client credentials are bound to the authorization server that issued them: clients MUST key persisted credentials by the issuer identifier, MUST NOT reuse them with a different authorization server, and MUST re-register when the authorization server changes (SEP-2352). That's the rule the SDK fix implements, now written for every client.

Does binding MCP client credentials to the issuer stop every OAuth attack from a malicious server?

Binding stops a server from redirecting credentials you already hold for another authorization server. It can't stop a malicious server from sending the user to its own authorization server for a fresh sign-in, and the advisory says so: a new interactive sign-in still goes to the authorization server the MCP server names. What protects users there is the client showing which authorization server and account they're about to sign in to, and the user saying no to ones they don't recognize.

How do I ask Rightshold about this (free, no key)?

Add the MCP server https://www.rightshold.com/mcp to Claude, Cursor, ChatGPT, Grok or VS Code, then ask: “This MCP server I'm about to add wants me to sign in with an authorization server I haven't used before. Ask the account owner to approve that exact server and authorization server first, and keep a record, so my client never completes a new OAuth sign-in nobody chose.”

One-click setup

{
  "tool": "request_human_approval",
  "arguments": {
    "action": {
      "tool": "mcp oauth sign-in",
      "mcp_server": "https://mcp.example.com/mcp",
      "authorization_server_issuer": "https://auth.example.net"
    },
    "summary": "Complete a first OAuth sign-in for MCP server https://mcp.example.com/mcp with authorization server https://auth.example.net",
    "expires_in_minutes": 30
  }
}

Rightshold records approvals your agent asks it for and reports what an MCP endpoint serves. It doesn't hold, store or inspect your OAuth tokens or client secrets, scan your client code or detect compromise. Advisory details are as published by the MCP TypeScript SDK maintainers on GitHub and spec text is from the MCP 2026-07-28 changelog and authorization page, read on November 11, 2026.

What are the sources?

Related

Not legal advice. JSON · Markdown · All answers

Get a free API key in one call

No card and no email needed. 100 free credits; the license and policy checks stay free without a key.

curl -s -X POST https://www.rightshold.com/v1/accounts/signup -H "Content-Type: application/json" -d '{"accept_terms":true}'
export RIGHTSHOLD_API_KEY=rs_...   # the api_key from the reply (shown once)
curl -s https://www.rightshold.com/v1/licenses/check -H "Authorization: Bearer $RIGHTSHOLD_API_KEY" \
  -H "Content-Type: application/json" -d '{"license":"MIT","intended_use":{"commercial_use":true}}'