TL;DR

The reference Python implementation of the Model Context Protocol shipped an OAuth client that did not check who the authorization server was. An MCP server the client connected to could name its own authorization server in its protected-resource metadata — or publish none and serve metadata that presented the user's real provider as the issuer on a fallback path — and the SDK would send that server the client secret, the authorization code, the PKCE code_verifier, or the signed JWT assertion it should only ever have sent to the real login provider. That is account takeover of whatever the client used those credentials for. The vendor advisory (published 2026-09-28) rates it High, CVSS 7.5 for the unattended providers and 6.5 for the interactive one; there is no CVE yet. Fixes are mcp 1.30.0 and 2.2.0 — both on PyPI since 2026-09-07, three weeks before disclosure, with the issuer checks listed in release notes as behaviour changes. Upgrading alone is not enough for ClientCredentialsOAuthProvider and PrivateKeyJWTOAuthProvider: you must pass issuer=, or the check has nothing to compare against until 3.0 makes the argument mandatory. Servers built on the SDK, stdio clients, and clients that manage their own tokens are not affected.

What happened

The SDK's OAuth client support performs discovery: connect to an MCP server, read its protected-resource metadata to learn which authorization server to use, fetch that server's metadata, then run the flow. The advisory describes two gaps:

  • 1.9.1 through 1.29.1 — no issuer validation and no credential binding on any discovery path. The MCP server decides where credentials go.
  • 2.0.0 through 2.1.1 — checks were added on the main path but were missing on the 404-fallback path and the 403 step-up path. Cycode's write-up: "when a malicious MCP server returns a 404 to discovery requests, the SDK never verifies the login provider's identity before accepting its configuration."

Impact by provider (advisory wording): a compromised or malicious server "could intercept client secrets, authorization codes, PKCE verifiers, or signed assertions by either advertising its own authorization server or manipulating metadata responses." For the interactive OAuthClientProvider the user must complete a login, hence 6.5; for the unattended ClientCredentialsOAuthProvider and PrivateKeyJWTOAuthProvider the credential is sent with no interaction, hence 7.5. The deprecated 1.x RFC7523OAuthClientProvider is in the affected list too.

Who this reaches: every MCP client built on the Python SDK that talks to remote HTTP servers with OAuth — agent frameworks, gateways, and the "connect your agent to this SaaS" integrations that hold a real client secret for the SaaS. Connecting to one untrusted or later-compromised MCP server was enough. Not affected: MCP servers built with the SDK, stdio transports, and clients that obtain and manage tokens themselves.

Timeline, as the sources state it: reported through Anthropic's security process by eight researchers (credited on the advisory); 1.30.0 and 2.2.0 published 2026-09-07 (PyPI upload dates confirmed this sweep) with the issuer checks described as behaviour changes rather than a security fix; vendor advisory and Cycode's analysis on 2026-09-28; The Hacker News on 09-29. No exploitation is reported. Note the pattern this repo keeps recording: a three-week window in which the fix was public, the reason was not, and anyone pinning an older mcp had no signal to move.

The TypeScript SDK's advisory tab (checked 2026-09-30) carries no equivalent entry; that is not evidence it is unaffected, only that nothing has been published.

Am I affected?

# Which mcp is installed, and does anything you run use an OAuth provider?
pip show mcp 2>/dev/null | grep -i '^version'
pip index versions mcp | head -1                     # 2.2.0 is latest as of 2026-09-30
grep -rn "OAuthClientProvider\|ClientCredentialsOAuthProvider\|PrivateKeyJWTOAuthProvider\|RFC7523OAuthClientProvider" --include=*.py . | head

# Affected: 1.9.1 <= mcp <= 1.29.1, or 2.0.0 <= mcp <= 2.1.1, AND one of the providers above is used
# AND the client connects over HTTP to servers you do not fully control.
# After upgrading: do the two unattended providers pass issuer=?
grep -rn "ClientCredentialsOAuthProvider(\|PrivateKeyJWTOAuthProvider(" --include=*.py -A6 . | grep -c "issuer="

Stored client registrations from before the upgrade were made under the old rules; the advisory says to clear them.

If you are affected

  1. Upgrade to mcp ≥ 2.2.0 (or ≥ 1.30.0 on the 1.x line).
  2. Add issuer="https://<your-real-authorization-server>" to every ClientCredentialsOAuthProvider and PrivateKeyJWTOAuthProvider — without it the patched versions emit a deprecation warning and the binding is incomplete; 3.0 will refuse to start without it.
  3. Clear stored OAuth client registrations so clients re-register against a validated issuer.
  4. If the client ever connected to an MCP server you did not control: rotate the client secret, rotate the private key behind the JWT assertion, and revoke tokens at the real provider — the credential may already be in someone else's hands. See playbooks/if-an-mcp-server-was-malicious.md and playbooks/rotating-cloud-credentials.md.

Prevention

  • Treat every remote MCP server as an attacker-controlled peer in the OAuth flow: the client must pin the issuer and bind credentials to it, never learn it from the resource it is about to authenticate to. This is the client-side twin of the mcp-remote discovery SSRF batch and the Obot dynamic-client-registration bug — the same protocol edge, three implementations.
  • Keep mcp on latest, unpinned within a major: the fix shipped 21 days before the advisory did.
  • Give agent integrations per-server client credentials so a compromised server can only redeem its own; see prevention/mcp-hygiene.md and prevention/credential-hygiene.md.

Update — 2026-10-04: the TypeScript SDK had the same bug (CVE-2026-104850, GHSA-6qxp-vccf-f47h, 7.5; @modelcontextprotocol/sdk 1.12.0–1.30.1 → 1.31.0, @modelcontextprotocol/client 2.0.0–2.1.0 → 2.2.0) — and between 09-28 and 10-02 the two official SDKs published six more advisories: unbounded sessions (CVE-2026-59951) and request bodies, $ref fetches from tool output schemas, cross-origin redirects that re-send API keys and OAuth token requests, and a shared task store with no session binding

The 09-30 write-up above noted that the TypeScript SDK's advisory tab carried no equivalent entry. It was published the same day: GHSA-6qxp-vccf-f47h / CVE-2026-104850 (2026-09-30, High 7.5) — "credentials were not tied to the authorization server they belong to," so a malicious or compromised MCP server could name its own authorization server and receive refresh tokens, client secrets and signed assertions with no user interaction. Affected @modelcontextprotocol/sdk 1.12.0 – 1.30.1 (fixed 1.31.0, npm 2026-09-28) and @modelcontextprotocol/client 2.0.0 – 2.1.0 (fixed 2.2.0). Same post-upgrade requirement as the Python fix: for bundled providers pass expectedIssuer, clear tokens saved without issuer metadata, rotate client secrets if a server you did not control was ever connected. Seven reporters credited. Both official SDKs, both transports, one bug — any MCP client, agent or gateway built on either should be on mcp ≥ 1.30.0 / 2.2.0 or @modelcontextprotocol/sdk ≥ 1.31.0 with an explicit issuer.

Six further advisories landed on the two tabs in the same five days, all vendor-published, none with press coverage; grouped by what an attacker gets:

  • A malicious server makes your client fetch things. Python GHSA-rwrf-2pqf-9j8j (09-28, 6.5): when validating tool results against an outputSchema, the client's JSON Schema resolver fetched unresolved $ref URLs — http://, https:// and file:// — from the client's network position, synchronously and without a timeout, so a server could also freeze the session. mcp 1.10.0–1.29.1 / 2.0.0–2.1.1 → 1.30.0 / 2.2.0. Partial mitigation: skip tools whose schema carries $id or an external $ref.
  • A redirect carries your credentials to another origin. TypeScript GHSA-6prh-2h8m-c8cw (10-02, 6.5): HTTP client transports and OAuth requests followed redirects to any origin and re-sent custom headers (X-API-Key, mcp-session-id) and bodies; on 307/308 that included OAuth token requests carrying refresh_token and client_secret. sdk ≤ 1.31.0 → 1.32.0 (npm 2026-10-02), client 2.0.0–2.2.0 → 2.3.0; workaround fetch(url, {...init, redirect: 'error'}). Python GHSA-5h93-6whr-6q8j (10-02, 5.9): same for the HTTP and SSE client transports — custom headers and bodies cross origins; a lone Authorization bearer is dropped by the HTTP library. → 1.30.0 / 2.2.0; workaround follow_redirects=False.
  • Anyone can take your server offline. Python GHSA-84m7-p3x7-pcfv / CVE-2026-59951 (09-30, 7.5): Streamable HTTP servers in the default stateful mode never reclaimed sessions and kept each initialize payload, so ordinary unauthenticated requests grow memory without bound until restart. mcp 1.8.0–1.29.1 / 2.0.0–2.1.1 → 1.30.0 / 2.2.0; stateless_http=True on ≥ 1.12.0 is the workaround, and the fix adds session_idle_timeout / max_sessions. Python GHSA-fmmv-w9g8-j3gc (09-30, 7.5): HTTP server transports and OAuth endpoints read request bodies with no size limit (2–3× the request size in memory); before 1.29.1 and 2.0.0/2.0.1 → 1.29.1 / 2.1.0, default cap now 4 MiB (max_request_body_size).
  • Tenants see each other's work. TypeScript GHSA-22jm-h49p-29qw (10-02, 8.6): the experimental tasks feature never checked which session created a task, so "clients sharing one InMemoryTaskStore could list, read and cancel each other's tasks." sdk 1.24.0–1.31.0 → 1.32.0; the feature is deprecated — drop taskStore or give each session its own store.

Registry check 2026-10-04: npm @modelcontextprotocol/sdk 1.30.1 (09-23), 1.31.0 (09-28), 1.32.0 (10-02); PyPI mcp latest 2.3.0 (2.2.0 and 1.30.0 carry every Python fix above). The practical reading for anyone building on either SDK: the September–October batch is the SDKs' first full audit of the client side — what a hostile server can make the client fetch, send, or leak — after a year in which every MCP advisory was about servers. Upgrade both SDKs to current, pass the issuer, and refuse redirects.

Update — 2026-10-06: the TypeScript SDK's server-side bearer authentication accepted tokens issued for a different service (GHSA-rvq5-wwqv-78pq, 6.8; @modelcontextprotocol/sdk 1.6.0 – 1.31.x → 1.32.0, server 2.0.0 – 2.2.x → 2.3.0, express → 2.0.2, server-legacy → 2.3.1) — the fix adds an expectedResource setting that upgrading alone does not turn on

Published on the TypeScript tab on 2026-10-05 (no CVE at fetch time; reporter gdegrange; CWE-863): requireBearerAuth() "lacked audience validation, allowing tokens intended for other services to be accepted." Where one authorization server issues tokens for several resource servers — the normal enterprise OAuth layout — a token minted for service A was accepted by an MCP server B built on the SDK, so a client authorized for A could call B's tools as the same principal. Scored 6.8 (AC:H/PR:L): the attacker needs a valid token for some service from the same issuer.

Upgrading is not the whole fix. The patched versions add expectedResource; to be protected you must (1) pass your server's audience to requireBearerAuth() and (2) return the token's audience from your verifyAccessToken() as AuthInfo.resource. On any version, validating the aud claim inside your own verifyAccessToken() closes it. This is the second "configuration required after upgrade" item in this file — the Python SDK's issuer= for two providers was the first — and it is the server-side counterpart of the client-side audit the 10-04 update described: the SDKs now check who a token was for, not only whether it was signed. Registry check 2026-10-06 was not repeated for this entry; the versions above are the vendor advisory's.

Update — 2026-10-08: the Python SDK had the same server-side bearer-token audience bug (GHSA-w4fh-qvv9-3v23, 6.8; mcp 1.10.0 – 1.29.1 and 2.0.0 – 2.1.1 → 1.30.0 / 2.2.0), with a validate_token_resource setting that is off by default

Published on the Python tab on 2026-10-05 (no CVE at fetch time; reporter u-ktdi; CWE-863), the Python twin of the TypeScript bearer-token bug above: a server's AuthSettings + token_verifier "could not check whether a token was issued for the server's own audience," so a token minted for another service sharing the same authorization server was accepted. Scored 6.8 (AV:N/AC:H/PR:L). As with the TS SDK, upgrading alone is not the fix: the patched releases add validate_token_resource (default off), which must be set to True with the correct resource_server_url, and verify_token() must return the audience as AccessToken.resource — both changes, or the server rejects everything; validating aud inside your own verify_token() closes it on any version. The default becomes True in 3.0. The fixed versions (1.30.0 / 2.2.0) are the same floor as the OAuth-client issuer fix this file opened with, so a project already on the patched line only needs the configuration. Status unchanged (patched).

This is now the third "configuration required after upgrade" item here and completes the pair with the 10-06 TypeScript entry: both official SDKs shipped audience-validation as an opt-in, so a reader who upgraded to close the September OAuth-client bug is not protected against the token-audience bug until they turn the new setting on.

Sources