[KongchangAI]
· 4 min read· 2,071 words

MCP Security Storm: OAuth SDK Vulnerability Testing and Server Hardening Guide

MCP Security Storm: OAuth SDK Vulnerability Testing and Server Hardening Guide

Multiple high-severity MCP OAuth vulnerabilities surfaced in 2026, rooted in clients blindly trusting servers' self-described identity; this article covers the CVEs, reproduces the leaks, and provides client and server hardening checklists.

From September to October 2026, four security incidents erupted across the MCP ecosystem, three sharing one root cause: one party trusting another's self-described identity. Official TypeScript and Python SDKs had issuer confusion flaws letting malicious MCP servers steal client secrets and refresh tokens without interaction; the Rust SDK lacked resource checks; LiteLLM auth bypass entered CISA KEV. The author's local reproduction confirms upgrading alone is insufficient—1.31.0 and 1.32.1 still leak without expectedIssuer, and old stored tokens are still sent. The MCP 2026-07-28 auth spec already requires resource indicators, audience validation, PKCE S256, and issuer checks; incidents stem from implementations skipping these steps. The article ends with detailed client and server hardening checklists, including per-tool scopes with deny-by-default, tools/list filtering, SSRF-safe metadata fetching, hashed token storage, and auditable revocation.

The MCP protocol faced a major security reckoning in 2026. Official SDKs issued back-to-back high-severity OAuth vulnerability advisories, LiteLLM authentication bypass was added to CISA's Known Exploited Vulnerabilities catalog, and prompt injection chains between agents were exposed. The protocol specification itself is fine—the problem lies in implementations: clients blindly trust servers, and servers skip audience validation. This article lays out the real CVE numbers, reproduction results, and hardening measures, with all data drawn from public advisories and hands-on testing.

Original MCP Security in 2026 screenshot

Four Security Incidents in One Month, All With a Single Root Cause

From late September to early October, four security issues erupted across the MCP ecosystem, three of which share the same root cause: one party trusting another party's self-description of its identity.

The most serious was the issuer confusion vulnerability in the official TypeScript and Python SDKs. In MCP's OAuth flow, the client asks the MCP server which authorization server to use for login via protected resource metadata. Affected SDK versions did not bind stored credentials to the authorization server that issued them. A malicious or compromised MCP server could claim its own authorization server and directly receive the client's client_secret, signed client assertion, or saved refresh_token—all with no user interaction. The TypeScript advisory was published on September 30, 2026, with the Python version following on September 28.

The Rust SDK's rmcp prior to version 2.0.0 lacked resource checks and did not verify that the resource in server metadata matched the connected server. A malicious server could redirect the OAuth flow to a real authorization server and obtain a token intended for a real resource.

LiteLLM's MCP endpoint allowed requests with forged Authorization headers to fall into the OAuth2 pass-through path and be treated as authenticated. CISA added it to the Known Exploited Vulnerabilities catalog on September 2, 2026.

The agent-to-agent prompt injection reported by Ars Technica revealed a structural problem: a compromised internal agent passed injected instructions to other agents that trusted it, and in some scenarios this led to server-side request forgery (SSRF).

AdvisoryComponentAffected VersionsFixed VersionSeverity
CVE-2026-104850 / GHSA-6qxp-vccf-f47hTypeScript SDK and client1.12.0 to 1.30.1; client 2.0.0 to 2.1.x1.31.0; 2.2.0High, CVSS 7.5
GHSA-qx49-fqc8-xw99Python mcp1.9.1 to 1.29.x; 2.0.0a1 to 2.1.x1.30.0; 2.2.0High, CVSS 7.5
CVE-2026-63127 / GHSA-33f5-2c5q-wgwjRust rmcpBefore 2.0.02.0.0High, CVSS 8.2
CVE-2026-59822 / GHSA-7488-6r32-c95qLiteLLM MCP endpointBefore 1.84.01.84.0High, CISA KEV

Older 2025 vulnerabilities also deserve attention. mcp-remote once passed a server-provided authorization URL directly to the system shell (CVE-2025-6514, CVSS 9.6), and early MCP Inspector ran an unauthenticated proxy capable of executing commands (CVE-2025-49596). The pattern never changes: everything an MCP server sends is untrusted input—including metadata, URLs, and tool output.

Local Reproduction: Upgraded, and Secrets Still Leak

The original author set up a three-process lab environment on 127.0.0.1: a legitimate authorization server (port 4401), a malicious MCP server (port 4402, returning 401 for all requests and pointing to the attacker's authorization server), and an attacker authorization server (port 4403, logging all requests). The client used StreamableHTTPClientTransport, held credentials that should only ever go to the legitimate authorization server, and connected to the malicious server.

The tests covered four configurations across three versions. The results were unsettling: with ClientCredentialsProvider but no expectedIssuer, versions 1.31.0 and 1.32.1 still leaked the client secret, printing only a deprecation warning. Stored refresh tokens without an issuer binding were still sent even by the fixed versions.

In requests received by the attacker's authorization server, the Basic header decoded to demo-client:demo-secret-for-LEGIT-AS-only, and the body carried legit-refresh-token-123. The legitimate server's secret and refresh token were handed over to the attacker in full.

With expectedIssuer configured, the 1.31.0 client refused before sending:

Error: OAuth client information is bound to authorization server http://127.0.0.1:4401
and is not presented to http://127.0.0.1:4403.

Two practical findings: first, upgrading alone is not enough—built-in providers such as ClientCredentialsProvider and PrivateKeyJwtProvider will still follow the server's choice unless expectedIssuer is explicitly passed. Second, tokens saved before the upgrade remain risky; fixed SDKs will use them as-is, so they must be cleared or migrated.

Client Fix: Three Lines of Code Stop Secret Leakage

The fix path for TypeScript clients is straightforward. Upgrade to @modelcontextprotocol/sdk@^1.31.0 or @modelcontextprotocol/client@^2.2.0, then pass expectedIssuer when creating the credential provider:

const authProvider = new ClientCredentialsProvider({
  clientId: process.env.MCP_CLIENT_ID!,
  clientSecret: process.env.MCP_CLIENT_SECRET!,
  expectedIssuer: 'https://auth.example.com',
});

If you implement OAuthClientProvider yourself, store all fields received by saveTokens() and saveClientInformation(), including the new issuer. Python users should upgrade to mcp 1.30.0 or 2.2.0 and explicitly pass issuer= in machine-to-machine scenarios.

Post-upgrade cleanup is equally critical: clear credentials saved before the upgrade, or add issuer to them; rotate client secrets and signing keys that may have leaked; revoke suspicious tokens. One easily overlooked point—new interactive logins will still redirect to any authorization server the MCP server specifies, so only complete login for servers you trust.

The Spec Already Prevented This; Implementations Didn't Keep Up

The 2026-07-28 version of the MCP authorization specification has explicit defenses against this class of attack. Clients must include resource (RFC 8707) in both authorization and token requests, set to the canonical URI of the MCP server they are connecting to. Servers must verify that tokens were issued for them and reject everything else with 401. Before redirecting, clients must record the issuer obtained from verified authorization server metadata, and check the iss returned in the redirect (RFC 9207). PKCE must be S256 only.

The companion security best practices page covers confused deputy problems, token pass-through, SSRF (including SSRF against authorization servers), session hijacking, local server compromise, authorization URL validation, mix-up attacks, localhost redirect impersonation, client ID metadata document trust policy, and scope minimization. The October SDK vulnerabilities are essentially mix-up attacks. Anyone writing MCP auth code should read that page from top to bottom.

RFC 8707 (OAuth 2.0 Resource Indicators) lets clients explicitly specify the target resource server URI in authorization and token requests, so the authorization server can restrict the token's audience to that resource. RFC 9207 (OAuth 2.0 Authorization Server Issuer Identification) requires authorization servers to return an iss parameter in the authorization response, which clients use to verify that the response really came from the authorization server they trust, preventing authorization server mix-up attacks. The MCP 2026-07-28 specification makes both mechanisms along with PKCE S256 mandatory precisely to block issuer confusion and token misuse at the protocol level.

Server Hardening: A Real-World fabwaseem.com Configuration

The original author operates an MCP server for a CMS, and blog posts are published through it. Its production configuration is hardened at several layers, with every number coming from actual operation.

Discovery and Challenge

Unauthenticated tools/list requests return 401 with a standard challenge pointing to protected resource metadata. The resource metadata specifies only one authorization server: the site itself. The authorization server metadata declares issuer, supports the iss parameter, supports only the S256 challenge method, offers only authorization code and refresh token grant types, uses public clients with no secrets, and supports dynamic client registration.

Tokens Bound to Resources

Authorization codes, code exchange, and every refresh validate that the request's resource canonically matches https://www.fabwaseem.com/api/mcp, returning invalid_target on mismatch. PKCE verifiers are compared in constant time. Authorization codes are valid for 5 minutes, access tokens for 1 hour. Refresh tokens rotate on every use, and reuse of an old token revokes the entire token family. Tokens, authorization codes, and refresh tokens are stored only as salted SHA-256 hashes.

SSRF-Safe Client Metadata Fetching

Client ID metadata documents mean the server fetches a client-specified URL—an SSRF risk. The fetcher accepts only credential-free, fragment-free https: URLs, resolves DNS itself and rejects private and loopback addresses, then connects to the checked IP while preserving the original hostname for TLS, preventing DNS rebinding from swapping in an internal address after the check. Responses are capped at 64KB with a 5-second timeout. Redirect URIs must match exactly, and plain http: is allowed only for loopback hosts.

SSRF (server-side request forgery) refers to attackers inducing a server to make requests to internal or protected networks, commonly used to probe internal services or access cloud metadata endpoints. In OAuth dynamic client registration scenarios, the server must fetch a client-provided metadata URL; without restrictions, a malicious client could specify internal addresses like http://169.254.169.254/. The core defenses are: allow only HTTPS, validate after DNS resolution that the IP is not in private/loopback/link-local ranges, connect to the validated IP while preserving the original hostname for TLS (to prevent DNS rebinding), and limit response size and timeout.

Per-Tool Scopes, Deny by Default

Each of 23 tools maps to a scope. create_post or update_post additionally requires posts:publish if it will publish articles. Gating follows three rules: tools without a mapped scope require full access, ensuring new tools are never accidentally callable by restricted tokens; batch requests are all-or-nothing, with one denial blocking the whole batch; and certain scopes are never implicit—history:undo must be explicitly granted, even for admin tokens. When listing tools with a full-access token, the server returns 22 of 23, hiding undo_history.

tools/list is also filtered, so restricted clients see only the tools they can call. Actual measurements (o200k_base tokenizer): a full-access token sees 22 tools and 7,778 tokens; posts:read sees only 4 tools and 680 tokens. A read-only agent loads 91% fewer tool-definition tokens—and cannot see write tools at all.

"Deny by default" is a fundamental access control principle: any permission not explicitly granted is denied. In an MCP server, this means every tool must map to an explicit scope, and unmapped tools can only be called with a full-access token. This is the opposite of "allow by default," which easily exposes new tools accidentally when an authorization check is missed. Deny by default also requires all-or-nothing batch requests and that sensitive operations (such as undoing history) are never implied by other scopes, shifting permission boundaries from after-the-fact remediation to enforcement in advance.

Auditing and Revocation

Every write—whether from MCP, REST, or the admin UI—records an audit event: actor type and token name, origin and tool or route, entity and field-level diff, request ID, hashed IP, and user agent. Raw IPs and token values are never stored. Events can be listed with history:read and undone with history:undo. All 15 unit tests covering scope gating, PKCE, redirect matching, resource binding, and private address blocking pass.

Checklist

When building an MCP client:

  1. Upgrade to SDK 1.31.0+, client 2.2.0+, Python mcp 1.30.0+ or 2.2.0+, and rmcp 2.0.0+.
  2. Pass expectedIssuer (TypeScript) or issuer= (Python) to every machine-to-machine provider.
  3. Clear or migrate pre-upgrade credentials and rotate potentially leaked secrets.
  4. Treat server metadata, authorization URLs, and tool output as untrusted input. Never pass a server-provided URL to a shell.
  5. Ask the user before connecting to a new server, and before performing destructive operations.

When running an MCP server:

  1. Validate token audience against the canonical resource URI, and never forward client tokens to upstream APIs.
  2. Support only PKCE S256, send iss on redirects, and declare support for it.
  3. Give every tool a scope, deny unmapped tools, filter tools/list, and check every message in a batch.
  4. Prefer public clients with PKCE and avoid client secrets that need protecting.
  5. Rotate refresh tokens, detect reuse, and store only hashes of tokens and authorization codes.
  6. Add SSRF protection when fetching client metadata URLs: HTTPS allowlist, post-DNS IP checks, pinned connections, and size and time limits.
  7. Record actor, tool, and diff for every write, and make writes undoable.
  8. Rate-limit token and registration endpoints, and keep development proxies like MCP Inspector off the public internet.

Common Mistakes

Stopping after upgrading the SDK is the most widespread problem—in testing, 1.31.0 and 1.32.1 still leaked without expectedIssuer. Keeping pre-fix tokens is equally dangerous; credentials without an issuer will be sent wherever the server points. Token pass-through breaks audience boundaries, scopes that exist only on paper are as good as absent, and trusting tool output leaves the door open to prompt injection. Exposing MCP Inspector or a local proxy on a public interface is equivalent to opening a remote shell.

The protocol specification already spells out the defenses clearly; the remaining question is whether implementers read it and did it.

#MCP security#OAuth vulnerability#issuer confusion#LiteLLM#CISA KEV#SSRF#PKCE#MCP server hardening
Share:

Related articles