News Weekly
0% read
Your progress · 0/5 chapters
About 33 min total
Security & CyberISSUE #4 · STORY 27 OF 40Sep 29, 2026CONFIRMED

A flaw in the MCP Python SDK let a server nominate your identity provider

Maintainers of the official MCP Python SDK published advisory GHSA-qx49-fqc8-xw99 on 28 September: a malicious MCP server could choose which authorization server received the client's secret, authorization code and PKCE verifier. The fixed releases had already shipped on 7 September under a changelog heading that says behaviour, and no CVE was assigned.

A clerk at a counter with a wall register of authorised counters cannot verify a pouch because this counter has no seal of its own, and feeds it under into a waiting hand.

Read it your way

CHAPTER 1Picked for Explorer

The 60-second version

The MCP Python SDK's OAuth client let the MCP server you were connecting to decide which login provider received your credentials. Advisory GHSA-qx49-fqc8-xw99 was published on 28 September 2026, the fixes shipped on 7 September, and no exploitation has been reported anywhere.

High severity, two scoresCVSS 3.1 base 7.5 for the unattended providers and 6.5 for the interactive one, with CWE-345 and CWE-522.
No CVE means no scannersThe advisory reads 'No known CVE', so it is absent from NVD, OSV.dev and CISA's exploited catalogue.
Which deployments are exposedOAuth clients in mcp.client.auth talking over HTTP to servers they do not fully control.
Whose SDK it is, preciselyThe repository is modelcontextprotocol/python-sdk; Cycode says MCP was donated to the Linux Foundation.
Finish this chapter for +15 XP
A wall board of job-card rows where one row under a different heading is already ticked and stamped while the unticked row beneath it has not been closed out at all.
Flip the switch

Where the trust broke

MetadataIssuer validated on every pathThe fix validates the authorization server issuer on each discovery route, not only the modern one.
ChecksCredentials bound, codes stampedBinding and audience restrictions now apply whichever discovery route the server forces.
OutcomeAttack stops at configurationAn explicit issuer= pin is the control; without it the machine providers still warn rather than refuse.
Your next move · Explorer

Reproduce the fallback path yourself

1Run an affected mcp release against a server that returns 404 for metadata, and log which token endpoint the client contacts.
2Repeat on the fixed release without issuer= and observe that the deprecation warning is silent by default in Python.
3Trace one login end to end and check whether your own logs hold any evidence of a substituted provider.

Switch your reading mode at the top to see a different next move.

Tap to open

Things to keep an eye on

Pop quiz · unlock the Issuer check, skipped badge

Did it stick?

0/3
What did the advisory GHSA-qx49-fqc8-xw99 let a malicious MCP server do?+20 XP
Why did the credential-binding check pass while the attack was running?+20 XP
Why would ordinary vulnerability tooling fail to surface this package?+20 XP
Your call · +5 XP

If your agent can pick and connect to MCP servers on a user's behalf, what do you govern first?

Research dossierThe full research, labeled and sourcedCONFIRMED1 source · 33 min read · 20 sections
Story identity

FACT. On 2026-09-28 the maintainers of the official Python SDK for the Model Context Protocol published GitHub Security Advisory GHSA-qx49-fqc8-xw99, titled "OAuth client could send credentials to an authorization server chosen by the MCP server". The advisory is published in the repository Security tab of modelcontextprotocol/python-sdk, and the affected package is mcp on PyPI.

FACT. Severity is High, CVSS 3.1 base score 7.5, vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N. The advisory states the rating is "scored for the unattended providers"; scored for the interactive OAuthClientProvider, where a person must start the sign-in, the advisory gives 6.5 with vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N.

FACT. The advisory's CVE field reads "No known CVE". Weaknesses are classified as CWE-345 (Insufficient Verification of Data Authenticity) and CWE-522 (Insufficiently Protected Credentials). The advisory credits eight reporters: Aviral2642, AlexMelanFromRingo, Gal3m, mrostamipoor, YuvalElbar6, yuvalelbar, RoniGurvich, mahmoudalbustan1.

FACT. The advisory was published by the GitHub user maxisbey, dated Sep 28, 2026.

INDEPENDENT EVIDENCE. Cycode, the reporting security firm, published the technical write-up under the title "Cycode Uncovers Account Takeover in MCP Python SDK", authored by Yuval Elbar, originally published 2026-09-28 and last updated 2026-09-29. The researcher is creditable in the advisory: both yuvalelbar and YuvalElbar6 appear in the advisory's credits list. The Hacker News covered it on 2026-09-29 (Swati Khandelwal) and Forkast published an analysis on 2026-09-30 (Heath Callahan).

INDEPENDENT EVIDENCE — database/registry status, verified directly. I checked four independent vulnerability registries while researching:

RegistryURL checkedResult

FACT — no exploitation reported. Neither the advisory nor Cycode reports any attack using the flaw, and neither Forkast nor The Hacker News reports one. Evidence status for "exploited in the wild" is therefore negative / unknown, not positive.

On the "Anthropic's official MCP SDK" framing

This framing needs care, because the popular headlines and one vendor write-up use it loosely.

  • FACT. The affected repository is modelcontextprotocol/python-sdk, published under the GitHub organisation modelcontextprotocol, not under an Anthropic organisation.
  • FACT. PyPI lists the mcp project's Homepage as https://modelcontextprotocol.io, Documentation as https://py.sdk.modelcontextprotocol.io/, Issues and Repository both pointing at modelcontextprotocol/python-sdk. There is no Anthropic-branded project URL.
  • COMPANY CLAIM. Forkast's headline reads "Anthropic's Official MCP Python SDK Had an OAuth Credential-Stealing Flaw…". Forkast's own body text is more careful: "The official Python SDK for the Model Context Protocol."
  • COMPANY CLAIM. Cycode uses "Anthropic's MCP Python SDK" in its body text and says "We reported this to Anthropic's MCP team through their security process."
  • INDEPENDENT EVIDENCE. Cycode states that MCP "is an open standard introduced by Anthropic and donated to the Linux Foundation's Agentic AI Foundation (AAF), which maintains it."
  • INTERPRETATION. "Anthropic's SDK" is shorthand for the reference SDK of a protocol Anthropic created, plus the practical reality that disclosure flowed through the Anthropic-originated MCP security process. It is not accurate to describe this as an Anthropic corporate product, an Anthropic-maintained repository, or an Anthropic bug. The maintainers who fixed it and published the advisory are the MCP Python SDK maintainers. Reporting should use "the official MCP Python SDK (mcp)" and attribute correctly.

✓

What happened?

🎓 For Explorer

FACT. The SDK's OAuth client support, in mcp.client.auth, allowed the MCP server a client was connecting to decide which authorization server received the client's OAuth credentials. Two distinct defects combined:

  1. The authorization server metadata issuer was not validated on every discovery path.
  2. Stored or pre-provisioned client credentials were not bound to the authorization server they belonged to.

FACT. A malicious or compromised MCP server could therefore direct the client's credentials to a token endpoint of its choosing, in two ways:

  • by naming its own authorization server in its protected resource metadata, or
  • by publishing none at all and serving metadata that presents the user's real authorization server as the issuer.

FACT. The attacker then received the client_secret, the authorization code, and the PKCE code_verifier meant for the real authorization server — or, with PrivateKeyJWTOAuthProvider, a signed client assertion (private_key_jwt client authentication material) rather than a static secret.

FACT. With those three values, the attacker can redeem them at the real authorization server's token endpoint and receive a genuine access token carrying whatever scopes the application was granted. The client secret is long-lived and keeps working until it is rotated.

FACT — the disclosure timeline.

Date (2026)Event
2026-07-28mcp 2.0.0 published (PyPI) — introduced protected-resource-metadata issuer checks on the modern discovery path only
2026-08-24mcp 1.29.1 published — last affected 1.x release
2026-08-25mcp 2.1.1 published — last affected 2.x release
2026-08-26Fix PR opened on the 2.x line: #3398
2026-09-02Fix commit d060b36e merged on main (#3398); backport PR #3431 opened on the 1.x line
2026-09-07mcp 1.30.0 and mcp 2.2.0 published — the fix ships, labelled "Behaviour changes", not a security fix
2026-09-28Cycode write-up originally published; GitHub advisory GHSA-qx49-fqc8-xw99 published
2026-09-29Cycode write-up last updated; The Hacker News coverage — event date for this story
2026-09-30Forkast analysis published
2026-10-02mcp 2.3.0 published (PyPI)

INTERPRETATION — the 21-day gap is the story. The patched code was in production registries on 7 September. The advisory did not exist until 28 September. For three weeks the safe version was available and nobody, including every automated scanner, knew it was the safe version — because the changelog entry sat under a heading that says "behaviour".


Δ

What changed?

FACT — machine-readable affected ranges as published in the advisory:

  • >= 1.9.1, < 1.30.0 — patched in 1.30.0
  • >= 2.0.0a1, < 2.2.0 — patched in 2.2.0

FACT — narrative ranges as stated in the advisory and Cycode's write-up:

  • 1.9.1 through 1.29.1: "no issuer check and no credential binding on any path. The entire flow is open."
  • 2.0.0 through 2.1.1: "both missing whenever the server publishes no protected resource metadata (the legacy fallback) or answers 403 insufficient_scope."
  • Both lines, on every path: ClientCredentialsOAuthProvider and PrivateKeyJWTOAuthProvider "had no way to name the authorization server their credentials belong to."

FACT — precision note on the gap. The 2.x line was not uniformly vulnerable. The v2.2.0 release notes state that the protected-resource-metadata path "has done this since 2.0". So 2.0.0–2.1.1 were partially hardened: a cooperating MCP server that publishes protected resource metadata with a wrong issuer was already rejected. What was still broken on 2.x was (a) the legacy fallback path and (b) the 403 insufficient_scope step-up path — precisely the two paths an attacker chooses. And the machine-readable range >= 2.0.0a1 includes the 2.0.0 alpha builds, which the narrative "2.0.0 through 2.1.1" does not spell out.

FACT — is 1.30.0 a backport line? Yes. The v1.30.0 release notes open: "Maintenance release of the 1.x line. 2.x is the current line; 1.x docs are at https://py.sdk.modelcontextprotocol.io/v1/." The fix PRs mirror each other: #3398 on 2.x ("Validate the authorization server metadata issuer on every discovery path") and #3431 on 1.x ("[v1.x] Validate the authorization server metadata issuer on every discovery path"). PyPI shows 1.x and 2.x releases interleaved throughout 2026 (73 releases total), so 1.x is a genuinely maintained maintenance branch, not an abandoned one.

FACT — what the fixed versions actually do. Per the advisory, the fixed client also:

  • works out which issuer it expects before fetching any authorization server metadata, on every path;
  • refuses metadata whose issuer differs, raising OAuthFlowError;
  • binds stored registrations to that issuer, and discards one bound to a different issuer so the client registers again.

Per the v1.30.0 and v2.2.0 release notes, both also: reject on 5xx/429 from protected resource metadata instead of falling back to legacy endpoints; return a non-insufficient_scope 403 to the caller instead of retrying; and follow HTTP redirects only within the endpoint's origin (#3448 on 1.x, #3397 on 2.x, commit c6762e82).

FACT — new behaviour is opt-in-warned, not enforced. Both release notes add a new deprecation warning: ClientCredentialsOAuthProvider / PrivateKeyJWTOAuthProvider without issuer=. The advisory states the advisory-warned state "becomes required in 3.0", and on 1.30.0 it is a DeprecationWarning, which Python hides by default.

FACT — the deprecated 1.x provider is a dead end. RFC7523OAuthClientProvider "has no issuer option". The advisory instructs teams to move to one of the other two providers. Separately, the commit history shows RFC7523OAuthClientProvider was removed from the main line on 2026-07-25 (7163d826, PR #3169), consistent with the advisory describing it as a "1.x" provider.


A wall board of job-card rows where one row under a different heading is already ticked and stamped while the unticked row beneath it has not been closed out at all.
↔

Before → Change → After

🎓 For Explorer
Before
An MCP client using the SDK's OAuth providers performed authorization-server discovery and then trusted the result. The issuer-validation helper existed. The credential-binding label existed. Audience restriction existed. But all three consumed the same input, and on the legacy fallback path that input was **never produced**, so:
```python
if self.context.auth_server_url is not None:
validate_metadata_issuer(asm, self.context.auth_server_url)
```
When `auth_server_url` was `None` — because the server answered the discovery request with a 404 — the guard short-circuited and the check **did not run**. It did not fail closed. It simply never executed.
→
After
Metadata whose `issuer` is not the expected one is rejected with `OAuthFlowError: Authorization server metadata issuer mismatch`.
Stored client registrations carry an issuer label; a registration bound to a different issuer is discarded and the client re-registers.
Legacy-fallback and step-up paths are covered, closing the two holes 2.0.0 had left.
Redirects are confined to the endpoint's own origin.
`issuer=` exists on the two machine-to-machine providers, is warned about when absent, and becomes mandatory in 3.0.
Registered clients persisted before the upgrade remain **unbound** and must be cleared once by hand.
⚙

How it works

FACT — the intended, safe discovery sequence (RFC 8414 / RFC 9728 style, as implemented):

  1. The MCP client asks the MCP server "who handles your logins?" The server answers with protected resource metadata pointing at an authorization server URL (auth_server_url).
  2. The SDK fetches that authorization server's configuration metadata (/.well-known/oauth-authorization-server).
  3. The SDK checks that the configuration's issuer field — the login provider's self-declared identity — matches the URL obtained independently in step 1. A malicious server that lies about who its login provider is is caught here.

FACT — the fallback path (legacy). If step 1 fails (the MCP server returns 404), the SDK asks the MCP server itself for the login configuration directly. On that path auth_server_url is None, so the step-3 guard does not run.

FACT — how the attacker satisfies the second control with a lie. The SDK's credential-binding check compares the issuer label on stored credentials against oauth_metadata.issuer — the very field the attacker controls, because the first check never ran. The attacker simply sets issuer to the name of the victim's real identity provider (Google, Okta, Azure AD). The binding check asks "do these credentials belong to this login provider?" and is told yes.

FACT — the third control fails for the same reason. Audience binding stamps the authorization code with "valid only at server X". The SDK only stamps it on the modern discovery path. On the forced fallback path there is no audience stamp, so the authorization code works anywhere.

FACT — why this is practically exploitable and not theoretical. The attacker's configuration points authorization_endpoint at the genuine identity provider. The user is redirected to the real Google / Okta / Azure AD login page at the real URL with a real TLS certificate. There is no phishing page and nothing to notice. After approval, the redirect returns the authorization code to the client; the SDK then POSTs code + client_secret + code_verifier to the token_endpoint from the attacker's metadata. The attacker replays that exact bundle at the real provider's token endpoint and receives a valid access token.

INDEPENDENT EVIDENCE — Cycode's three-configuration proof. Cycode built three test configurations to isolate which control matters:

ConfigurationIssuer check ranAttack blockedCredentials stolen
Attack (server returns 404 for discovery)NoNoYes
Control (server also provides a discovery response)YesYesNo
Escape (server provides discovery naming itself)Yes, passesNoNo

The Control case is the decisive one: identical attack, the only difference being that the server also answered the discovery request. That single difference makes the issuer check run, the mismatch is detected, and the flow aborts before any credential is sent. In the Escape case the attacker passes the issuer check by naming itself, but then the discovered URL points at the attacker's own origin, which does not match the issuer label on the victim's stored credentials — so the binding check discards those credentials and forces a fresh registration, leaving the attacker with a throwaway identity that has no standing at the victim's real provider.

FACT. Cycode states the proof of concept "does this against a real authorization server with PKCE enforcement" and that the full chain was demonstrated end to end, including calling the userinfo endpoint to confirm takeover.


!

Why it matters?

🎓 For Explorer

FACT. MCP is the integration layer through which AI applications reach external tools and data. This flaw is in the client half of that layer — the half enterprises deploy inside their own trust boundary, holding their own corporate credentials.

INTERPRETATION. The structural problem is that in an agent stack, "which server am I allowed to talk to" and "which identity provider do my credentials belong to" are conflated into one untrusted input. A protocol that lets an untrusted counterparty nominate your own identity provider, while skipping the validation that would catch the lie, is a full credential-interception primitive, not a bug class. A single approved click on a genuine login page yields a valid access token plus a reusable client secret.

FACT. The unattended providers are the severe case. ClientCredentialsOAuthProvider and PrivateKeyJWTOAuthProvider run machine-to-machine with no login page and no person present — hence the 7.5 with UI:N. In these, the user-interaction mitigation does not exist at all.

INTERPRETATION — the trust-boundary inversion. Conventional supply-chain risk is code you install executing with your privileges. Here the risk is a server you connect to at runtime extracting credentials from the client that connects. The attack surface is the network and the agent's own tool-selection, not the build pipeline.

FACT — silent to the victim and to monitoring. Cycode and Forkast both note there is no login-prompt anomaly, no failed login, no MFA challenge, no suspicious-IP alert. From the authorization server's perspective the exchange is legitimate: correct client secret, valid code, valid PKCE verifier.


✦

What became possible?

🎓 For Explorer

FACT — direct outcomes of the primitive:

  • Full OAuth account takeover of the identity the MCP client was acting for, demonstrated end to end by Cycode.
  • Theft of a long-lived client secret, usable to mint new tokens at will, independent of the victim.
  • Theft of a signed client assertion (private_key_jwt), i.e. the authentication material itself, in the PrivateKeyJWTOAuthProvider case.
  • Indefinite persistence via refresh tokens, if the authorization server issues them.
  • Lateral movement to every downstream service the stolen OAuth client is authorised to reach.

INTERPRETATION — attack routes that remove the user-interaction caveat entirely. Cycode argues the 6.5 "requires user interaction" score understates risk because the interaction is approving a real login page — the one action users are trained to perform without hesitation. Cycode then names three routes where even that choice is not really the user's:

  • Registry / catalogue poisoning. A typosquatted or compromised MCP server directory entry points at the attacker's server. The user picks it from a list that looks curated.
  • Prompt injection in agent workflows. Where an agent selects and connects to MCP servers on the user's behalf, a prompt injection from any tool in the chain can steer it at the attacker's server. The user never chose the server, and if the framework auto-presents or auto-approves login flows, the human barrier dissolves.
  • DNS hijack or internal compromise. If the attacker controls where a legitimate server name resolves, the client connects believing it is talking to a trusted server, and the login flow starts automatically.

INTERPRETATION. The prompt-injection route is the one specific to the AI-agent era. It converts a network-security bug into a model-behaviour vulnerability with no user in the loop at all — the same class of risk as tool-poisoning and confused-deputy attacks, but with credential theft as the payload instead of data exfiltration.

INTERPRETATION — the escalation path. Account takeover is the foothold. What makes it worse is everything reachable afterwards, and in an enterprise a single OAuth client is often authorised against multiple downstream services — cloud APIs, internal tools, data stores. One stolen secret opens doors across the organisation.


◎

Implications

Technical

FACT — a five-layer defence, three layers of which were bypassed. The advisory and Cycode together identify four distinct controls, all present in the code, all fed the same unverified input:

ControlPurposePre-fix status
Authorization server metadata issuer validationCatch a server lying about who its login provider isPresent but skipped when auth_server_url was None
Credential binding to issuerStop credentials labelled for provider X being sent to provider YPresent but compared against an attacker-controlled field
Audience binding on the authorization codeMake a stolen code useless elsewherePresent but only applied on the modern discovery path
PKCE code_verifierMake a stolen authorization code unusableFully implemented — and rendered meaningless because the attacker receives it too
Redirect confinementStop off-origin redirectsAbsent in the affected versions; added in the fix releases as a separate behaviour change

FACT — the generic bug pattern is fail-open on attacker-controlled absence. Cycode's framing: "a safety check that only runs when certain data is present, where the attacker controls whether that data is present. Skip the check, and the unchecked value doesn't just go unverified — it flows into the next safety check as trusted input and actively satisfies it." This is a design-level lesson, not an implementation slip: all the controls were there, and every one of them said "looks good".

FACT — CWE mapping. CWE-345 (Insufficient Verification of Data Authenticity) captures the unvalidated issuer. CWE-522 (Insufficiently Protected Credentials) captures the credential handling outcome.

FACT — I:N in the CVSS vector is a scoping artefact, not a claim of read-only impact. The vector scores impact on the vulnerable component, not on the third-party services the stolen token then reaches. Cycode's own analysis — stolen token carries "whatever scopes the client was originally granted", potentially "read and write access to user data, access to internal APIs, admin operations" — describes integrity impact at the destination service.

FACT — MCP specification work underneath. The commit history for src/mcp/client/auth/oauth2.py shows the surrounding security architecture: SEP-2352 (client credential binding, PR #2933, 2026-06-20), SEP-2468 / RFC 9207 (iss authorization-response validation, PR #2921, 2026-06-20), SEP-2350 (step-up re-authorization, PR #2931), plus the "restore same-origin issuer binding" fix in PR #2946 and PR #3397 for redirect confinement. The vulnerability lived at the seam between these separately-landed hardening efforts.

FACT — a related hardening PR is still open. PR #3263 — "fix(client/auth): discover AS metadata before cold-start token refresh, with issuer-binding check" — was opened 2026-08-07 and was still open at the time of research. That is a signal that the token-refresh path in the OAuth client continued to be hardened after the September fix.


Developer

FACT — who is affected. You are affected if both hold:

  1. your application uses the SDK as an MCP client over HTTP with OAuthClientProvider, ClientCredentialsOAuthProvider, PrivateKeyJWTOAuthProvider, or the deprecated 1.x RFC7523OAuthClientProvider as the transport's auth handler; and
  2. it may connect to an MCP server you do not fully trust while holding credentials for a legitimate authorization server.

FACT — who is not affected: MCP servers built with the SDK; stdio (local) clients; clients that attach their own tokens or headers.

FACT — the exact upgrade-and-pin sequence:

  1. Upgrade to mcp 1.30.0 on the 1.x line or 2.2.0 on the 2.x line or later.
  2. If you use ClientCredentialsOAuthProvider or PrivateKeyJWTOAuthProvider, pass issuer=. The advisory's wording is unambiguous: "upgrading changes nothing until you also pass issuer=" (for example issuer="https://auth.example.com"). Without it "they still follow whichever authorization server the MCP server advertises."
  3. Do not rely on the warning to tell you. On 1.30.0 the missing-issuer notice is a DeprecationWarning, and Python hides DeprecationWarning by default outside __main__. CI that treats warnings as errors would catch it; a normal production run will not.
  4. Leave RFC7523OAuthClientProvider. It has no issuer= option at all. Migrate to ClientCredentialsOAuthProvider or PrivateKeyJWTOAuthProvider with issuer= set.
  5. Clear stored OAuth client information once. A registration stored without an issuer stays unbound — this "includes everything 1.x stored before 1.30.0". If you placed a pre-registered client in storage yourself, set issuer on that record instead.
  6. If you may already have been exposed: rotate the client secret and revoke tokens at the authorization server. Revoking tokens alone is insufficient if the secret itself is not rotated, because the secret is what mints new tokens.

FACT — pre-upgrade posture. On affected versions "there is no workaround other than connecting OAuth-enabled clients only to MCP servers you trust."

INTERPRETATION — the practical developer takeaway. An SDK upgrade is not a security control here. The issuer= pin is the control, and it is opt-in with a default-off warning that Python suppresses. Teams that read the release note under "Behaviour changes", upgraded, and did not set issuer= are still vulnerable on the two machine-to-machine providers, and there is no runtime signal telling them so.


Enterprise

FACT — exposure is invisible to standard tooling. I verified this directly against the registries in section 1:

  • No CVE. No NVD record (keyword search returned 0 results).
  • Not in the global GitHub Advisory Database. github.com/advisories/GHSA-qx49-fqc8-xw99 returns 404, against a control GHSA on the identical URL pattern that returns 200.
  • Not in OSV.dev. 404, no aliases.
  • Not in CISA KEV. No entry in the 2026-10-02 catalog of 1,733 vulnerabilities — and none is possible without a CVE ID.
  • The fix shipped 21 days before the advisory, under a changelog heading reading "Behaviour changes", with the fix PRs merged 2026-09-02.

INTERPRETATION. This is the enterprise lesson of the story. Any patch-management process driven by CVE feeds, SCA/SBOM tooling, or a vulnerability-driven SLA would not have surfaced this. mcp was not flagged; mcp 1.30.0 and 2.2.0 were not flagged; the changelog entry read as a compatibility note. The correct enterprise posture is to treat GHSA IDs and upstream security advisories in AI/agent SDKs as a first-class tracking feed, not as a derivative of CVE data.

INTERPRETATION — scope inversion of CVSS. A "High, no code execution, confidentiality only" score is unlikely to escalate a ticket inside a large security organisation. That under-rates it, because the CVSS scope is the SDK. The consequence lands in the identity provider and every downstream service the stolen token can reach — cloud accounts, internal APIs, CI/CD, data stores.

FACT — the identity blast radius. Cycode: the access token the attacker mints "carries whatever scopes the client was originally granted"; in enterprise environments "a single OAuth client often has access to multiple downstream services, so one stolen secret opens doors across the organization"; and "an attacker with a valid access token can reach any service the victim's client was authorized to talk to… storage buckets, databases, deployment pipelines, or other MCP servers the victim is trusted by."

FACT — detection is the hard problem. No failed login, no suspicious IP, no MFA challenge. The attacker's exchange is indistinguishable from a legitimate one at the authorization server.

INTERPRETATION — the governance question. If agents can select and connect to MCP servers on a user's behalf, then an agent's tool-selection decision becomes a security decision, and prompt injection becomes a credential-theft vector. Enterprise MCP governance therefore cannot stop at "which servers are allowlisted" — it needs to cover which identity each server is permitted to authenticate against, which is exactly the issuer= pin the SDK now offers and leaves optional until 3.0.


Strategic

INTERPRETATION — MCP has become a protocol-layer attack surface, not a model-layer one. Forkast makes the point sharply: prior incidents in this space (Azure AI Foundry's authentication bypass, the CARBONATO botnet with an AI agent as C2, Salesforce Agentforce's SalesBleed) targeted models and agent behaviour. This one targets "the plumbing between them". As protocols carry credentials across the agent boundary, they inherit the full weight of identity security — and they inherit it before the governance, standards bodies, and CVE processes have caught up.

FACT — the protocol's governance reality. MCP is an open standard introduced by Anthropic and donated to the Linux Foundation's Agentic AI Foundation, which maintains it (Cycode). The SDK repository sits under the modelcontextprotocol GitHub organisation and its PyPI homepage is modelcontextprotocol.io.

INTERPRETATION — a three-layer accountability gap. No CVE means no national CERT record, no KELV/KEV pathway, no vendor advisory fan-out. The advisory lives in one repository tab. A consortium-run protocol with a Linux Foundation home has neither a CVE assignment process nor a security-advisory distribution channel comparable to, say, npm or PyPI advisories. For a protocol becoming a default integration layer, that is a structural gap, not a one-off.

INTERPRETATION — a pattern worth generalising beyond MCP. Cycode's closing observation is the strategic finding: "All the controls were there. The issuer validation function existed. The credential binding existed. The audience restriction existed. They were all fed the same unverified input, and they all said 'looks good.'" This is a fail-open-on-attacker-controlled-absence anti-pattern, and it is common wherever a security check is written as if trusted_input_is_present: validate(trusted_input) rather than validate(resolve_expected()). Any agent framework, plugin loader, or tool broker with a similar shape has the same bug.

INTERPRETATION — hardening is being shipped as "behaviour changes". Both fixed releases bundled genuine security controls — issuer validation, redirect confinement, refusing legacy fallback on 5xx/429 — under a "Behaviour changes" heading, alongside session-timeout defaults. That is a release-engineering norm that quietly deprioritises security. Until AI SDKs adopt security-specific changelog sections and coordinated CVE assignment, defenders must read release notes as prose rather than trusting scanners.

PREDICTION. A CVE for GHSA-qx49-fqc8-xw99 is likely eventually — the advisory format already supports one, and GHSA records are commonly backfilled into CVE once a CNA assigns it. But that is a low-confidence prediction, and until it happens every automated dependency-control surface continues to show this package as clean.

PREDICTION. issuer= will become mandatory in mcp 3.0, as the advisory states. Expect a second wave of upgrade friction at that point, and expect a measurable number of teams to be discovering for the first time that their machine-to-machine OAuth providers were following whatever the MCP server advertised.


⚠

Risks & limitations

Risks

FACT — residual risk after following the full advisory:

Residual riskStatus
Malicious MCP server still obtains a credentialBlocked once issuer= is set and a fixed version is used
Login-page realismUnchanged — the genuine provider page is still shown; PKCE does not help because the verifier is also stolen
Stored registrations unboundRequires a one-time manual clear; skipped clears silently preserve the pre-fix state
Token revocation without secret rotationInsufficient — the long-lived secret still mints tokens
SSRF-like redirectionMitigated only from 1.30.0 / 2.2.0 onward (redirect confinement)
3.0 issuer= enforcementFuture; until then the pin is opt-in

FACT — "cleared the storage" is not a small ask. The advisory says stored registrations "include everything 1.x stored before 1.30.0". Any client that persisted a dynamic client registration before the upgrade is still holding an unbound record, and re-registration at an attacker-chosen server is not detectable from the client side.

INTERPRETATION — the silent-failure risk is the largest one. Three of the four recovery steps are silent on success. An upgrade that leaves issuer= unset emits a DeprecationWarning Python hides by default. A missed storage clear produces no error. A missed secret rotation produces no alert. A team can complete every visible checklist item and remain exposed.

INTERPRETATION — attack-surface economics. Severity is high but the cost to the attacker is a single connection attempt and one click on a page that is genuinely the real one. No phishing infrastructure, no lookalike domain, no malware. This is a low-cost, high-conversion attack, which is precisely the profile that shows up in real campaigns once word spreads — even though nothing has been exploited yet.


Limitations
  • No exploitation data. Neither the advisory nor Cycode reports any attack using the flaw, and I found no independent exploitation report. Everything about real-world impact is a capability assessment, not a measured outcome.
  • No CVE means no downstream ecosystem corroboration. Because the record does not exist in the global GitHub Advisory Database, OSV.dev, or NVD, I have no vendor-side telemetry, no EPSS score, no affected-package download counts, and no independent confirmation from a third-party scanner vendor.
  • No KEV status, and none is obtainable. I confirmed absence in the 2026-10-02 catalog, but a KEV listing is structurally impossible without a CVE ID. Treat "not in KEV" as "not applicable", not as "not exploited".
  • Affected-population size is unknown. I have no figure for how many Python applications use mcp.client.auth over HTTP against untrusted servers. The 73 PyPI releases and 24.5k GitHub stars indicate an actively maintained, widely adopted package but are not adoption data.
  • Cycode's claims are vendor claims. The full-chain account-takeover demonstration, the code snippets, and the three-configuration table all come from the reporting vendor's own write-up and PoC. I verified that the advisory corroborates the version ranges, the issuer= remediation, the severity, and the CWE classification; I did not independently re-run the PoC. Forkast and The Hacker News independently corroborate the mechanism and the version table at the level of reporting, not reproduction.
  • The I:N/A:N in the CVSS vector may understate downstream impact, but I have no basis to publish a corrected score.
  • Release-note content is my direct read of the GitHub release API for v1.30.0 and v2.2.0; I did not audit the full diff of PRs #3398 and #3431, nor PR #3263, which is still open.
  • Governance statements are attributed, not adjudicated. Cycode's claim that MCP was donated to the Linux Foundation's Agentic AI Foundation is a company statement; I verified the repository and PyPI project URLs but did not independently verify the foundation's own records.

?

Open questions

  1. Will a CVE be assigned, and by whom? Without one, the fix is invisible to every CVE-driven control. Who is the CNA for a Linux-Foundation-hosted protocol SDK?
  2. Why did the fix ship three weeks before the advisory, under "Behaviour changes"? Was this a deliberate coordinated-disclosure decision, or an accidental pre-announcement that the disclosure timeline then had to work around?
  3. How many production clients are actually affected? No one has published an adoption figure for mcp.client.auth over HTTP.
  4. Will issuer= be enforced by default before 3.0, or only warned about? A DeprecationWarning that Python hides is a weak control; a FutureWarning or a hard default would be stronger. This is an open question for the maintainers.
  5. What about other SDKs and other protocol layers? The advisory covers mcp.client.auth in Python. The equivalent OAuth client implementations in the TypeScript/JS SDKs, and in other agent-protocol implementations, were not part of this disclosure and were not checked here.
  6. Does the still-open PR #3263 (AS metadata discovery before cold-start token refresh, with issuer-binding check) close a residual gap? It was open at research time.
  7. How should agent frameworks handle MCP OAuth now? If a model chooses the server, the issuer= pin is the only thing preventing credential theft — is there emerging best practice for constraining that?
  8. Should MCP itself mandate credential-to-issuer binding at the specification level, rather than leaving it as an SDK option that becomes mandatory only in 3.0?
  9. Is there any evidence of exploitation that predates disclosure? Cycode's stated impact window is the whole affected version range, which is long. A retrospective log review at authorization servers would be the only way to know.
  10. What is the real distribution of client registrations in the wild — dynamic (stored and unbound) versus pre-provisioned with issuer set? This determines how many teams are exposed after upgrading without the pin.

↗

What happens next?

🎓 For Explorer

FACT — stated in the advisory and release notes:

  • issuer= becomes required in mcp 3.0.
  • On affected versions there is no workaround beyond trusting your MCP servers.

PREDICTION — near term (weeks to months):

  • Teams that upgraded on 7–28 September from the release notes without reading the advisory will upgrade again when someone notices the issuer= requirement, or when 3.0 forces it. Expect a second, larger migration wave.
  • A CVE assignment is plausible but not assured; the GHSA record is already in the format that backfills into one.
  • PR #3263 (authorization-server metadata discovery before cold-start token refresh, with issuer-binding check) was still open at research time, so further OAuth-client hardening in mcp.client.auth is likely.

PREDICTION — medium term (6–18 months):

  • The first real exploitation campaign, if one occurs, is likely to arrive via one of Cycode's named routes rather than a novel technique — poisoned MCP registry entries, or prompt injection steering an agent to an attacker-controlled server. Expect it to look like a supply-chain compromise rather than an OAuth exploit.
  • Other language SDKs for MCP will face equivalent discovery-hardening work, and the absence of a CVE here may mean equivalent issues elsewhere have not been surfaced at all.
  • issuer=-style credential-to-issuer binding is likely to become a normative requirement in the MCP specification rather than an SDK-level convenience, driven by exactly this case.
  • Detection remains the unsolved part: the attack is silent by design, and no standard control currently distinguishes a stolen-but-valid OAuth exchange from a legitimate one.

What would change this assessment: evidence of exploitation; a CVE assignment pulling in third-party telemetry; a successor finding in the TypeScript SDK or in MCP itself; or mcp 3.0 shipping issuer= as a hard requirement rather than a warning.


★

Editorial takeaway

🎓 For Explorer

MCP's OAuth flaw is not interesting because a popular library had a bug. It is interesting because it shows what happens when an agent protocol starts carrying enterprise credentials before the surrounding machinery catches up — and the machinery did not catch up at all.

Three facts define the story. The controls were all present: issuer validation existed, credential binding existed, audience restriction existed. They were all fed the same unverified input, so they all said "looks good". The fix shipped on 7 September under a heading called "Behaviour changes", and the advisory did not appear until 28 September — and even then it exists only in one repository tab, with no CVE, no NVD record, no OSV record, and structurally no possibility of a CISA KEV listing. For three weeks the safe version of the package was sitting in every registry and every dependency scanner reporting it as ordinary.

The deeper structural point is the trust-boundary inversion. Conventional supply-chain risk is code you install executing with your privileges. This is a server you connect to at runtime extracting credentials from the client that connects — with the victim approving a completely genuine login page at the genuine identity provider, so there is nothing to notice and nothing for monitoring to flag. And the identity obtained is not the user's: it is the application's OAuth client, which in an enterprise is frequently authorised against far more than the single server it was talking to.

That makes the honest framing "the reference SDK of an MCP that Anthropic created and donated to the Linux Foundation's Agentic AI Foundation", maintained in the modelcontextprotocol organisation and disclosed through the MCP security process — not "Anthropic's SDK". The popular headline is wrong about ownership, and it is the ownership question that matters here, because a protocol without a CVE-assignment path or a coordinated advisory channel has nowhere to route a disclosure.

The actionable residue is unglamorous and immediate: upgrade to 1.30.0 or 2.2.0, then — for the two machine-to-machine providers — set issuer=, because the upgrade alone does nothing there and the warning that would tell you is a DeprecationWarning Python hides by default. Clear stored registrations. Rotate the secret and revoke the tokens if an affected client ever met a server you did not fully trust. And for anyone building on this ecosystem: track GHSA-level advisories for agent SDKs directly, because the scanners are not going to tell you, and "no CVE" here meant no signal at all for the entire time the flaw was live.


Key figures, consolidated

ItemValueSource
Advisory IDGHSA-qx49-fqc8-xw99GitHub advisory
CVENone assigned ("No known CVE")GitHub advisory; NVD keyword search returned 0
CWECWE-345, CWE-522GitHub advisory
CVSS 3.1 (unattended providers)7.5 High — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:NGitHub advisory
CVSS 3.1 (interactive provider)6.5 — AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:NGitHub advisory
Packagemcp (PyPI)GitHub advisory; PyPI
Affected (machine-readable)>= 1.9.1, < 1.30.0 and >= 2.0.0a1, < 2.2.0GitHub advisory
Affected (narrative)1.9.1–1.29.1 and 2.0.0–2.1.1GitHub advisory; Cycode
Fixed versionsmcp 1.30.0, mcp 2.2.0GitHub advisory; release notes
1.30.0 published2026-09-07T14:03:59Z (GitHub) / 14:34:14Z (PyPI)Release API; PyPI JSON
2.2.0 published2026-09-07T15:53:57Z (GitHub) / 16:06:19Z (PyPI)Release API; PyPI JSON
Fix commits / PRsd060b36e (2.x, PR #3398); PR #3431 (1.x backport)GitHub commits + PR search
Advisory published2026-09-28GitHub advisory
Reporting write-upCycode, originally 2026-09-28, updated 2026-09-29Cycode
Reporters credited8GitHub advisory
CISA KEVNo entry (catalog 2026.10.02, 1,733 vulns)CISA KEV feed
Affected providersOAuthClientProvider, ClientCredentialsOAuthProvider, PrivateKeyJWTOAuthProvider, deprecated 1.x RFC7523OAuthClientProviderGitHub advisory
Not affectedSDK-built MCP servers, stdio clients, clients attaching their own tokensGitHub advisory; Cycode

Research sources

Unverified Sources (1)
Unverified
Sources deliberately excluded - `https://gbhackers.com/anthropic-mcp-python-sdk-flaw/` — linked from the Forkast article and described as "affected versions". Not retrieved and therefore not used; its content is superseded by the primary advisory and the three independent sources above. - Any CISA, NVD or CVE record for this flaw — none exists to retrieve; see sources 11, 12 and 13. - Reports of exploitation or of Known Exploited status — none found. Recorded as a negative finding, not as a gap in sourcing.
URL unavailable