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.
- 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.
- 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.
- 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.
- 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' }).
- 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.
- 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.
- 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.
- The advisory says affected versions let the MCP server decide which authorization server received the client's OAuth credentials, because credentials weren't tied to the authorization server they belong to. Affected: sdk 1.12.0 through 1.30.1, and client 2.0.0 through 2.1.0 only for bundled providers without expectedIssuer, credentials stored or supplied without issuer, direct calls to fetchToken(), and providers that read storage back through OAuthTokensSchema or OAuthClientInformationSchema. You're affected if your app uses the SDK's OAuth client over HTTP (an authProvider on a transport, withOAuth(), or direct auth() or fetchToken() calls) and may connect to an MCP server you don't fully trust while holding credentials for a legitimate authorization server. MCP servers built with the SDK and stdio clients aren't affected.
- The redirect advisory is a second path to the same secrets. Before the fix, the SDK's HTTP client transports and OAuth requests followed redirects to any origin, so the other origin received custom headers and the mcp-session-id, and on a 307 or 308 the request body too. The advisory notes current fetch runtimes already drop the Authorization header across origins; the risk was in everything else. Patched versions follow redirects only within the same origin, and not at all in browsers.
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.
- It also adds issuer identification from RFC 9207: authorization servers SHOULD include the iss parameter in authorization responses, and MCP clients MUST validate a present iss against the recorded issuer before redeeming the authorization code (SEP-2468). RFC 9207 (March 2022) defines iss as a countermeasure to mix-up attacks, where a client that talks to several authorization servers is tricked into sending a code or token from an honest one to one the attacker controls; it notes a client that only ever talks to one authorization server isn't exposed, but becomes exposed when it adds a second. A general-purpose MCP client talks to as many authorization servers as there are servers its users add.
- The same release formally deprecates Dynamic Client Registration in favor of Client ID Metadata Documents and requires clients to set application_type when they do use DCR (SEP-837). DCR remains available for backward compatibility.
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.
- Binding also doesn't help with credentials that never carried an issuer. Old stored tokens and pre-registered client information keep the old behavior until you add the issuer or clear them, so the upgrade isn't finished until that storage is migrated.
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.”
{
"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
}
}- Needs a key for your account. It hashes the action details and returns an approval id and a one-time token for the owner. Only the account owner can decide it, with an account key; delegated agent keys are refused, so the assistant can't approve its own request. Anyone can check the signed decision free with verify_human_approval, including that the server and issuer match what was approved.
- precall_check (free, no key) reports what that MCP server's tool definitions look like now, whether they changed from a recorded fingerprint, hidden-text findings and standing security flags, before you decide.
- Neither tool reads the server's OAuth metadata, checks your SDK version or stored credentials, or stops your client from signing in. Upgrade the SDK, migrate stored credentials to carry an issuer, and keep redirects off.
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?
- MCP TypeScript SDK: GHSA-6qxp-vccf-f47h, OAuth client could send credentials to an authorization server chosen by the MCP server (CVE-2026-104850)
- MCP TypeScript SDK: GHSA-6prh-2h8m-c8cw, HTTP client transports and OAuth requests followed cross-origin redirects and re-sent custom headers and request bodies (CVE-2026-111769)
- MCP specification 2026-07-28: Key Changes (SEP-2352 issuer-bound credentials, SEP-2468 iss validation, DCR deprecation)
- MCP specification 2026-07-28: Authorization
- IETF: RFC 9207, OAuth 2.0 Authorization Server Issuer Identification
Related
- Should an MCP server still support Dynamic Client Registration, or switch to Client ID Metadata Documents?
- Can connecting to a malicious remote MCP server run code on your machine? (mcp-remote CVE-2025-6514)
- Keeping API keys and tokens out of mcp.json and claude_desktop_config.json
- MCP OAuth wrong-audience tokens: what they are and how to defend
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}}'
