The MCP Python SDK fix shipped 21 days before its advisory, filed as a behaviour change with no CVE
A malicious MCP server could steer the official Python SDK's OAuth client into sending its secret, authorisation code and PKCE verifier to an endpoint the server chose. The fix shipped on 7 September as a behaviour change; the advisory followed on 28 September, and it has no CVE.
By Parminder Kumar Sharma · · 17 min read

Twenty-one days between the fix and the advisory
21 days.
That is the gap between the release that fixed this flaw and the advisory that called it one. On 7 September 2026 the maintainers of the official Model Context Protocol (MCP) Python SDK released versions 1.30.0 and 2.2.0. The 1.30.0 notes list, under the heading Behaviour changes, that "The OAuth client checks the authorization server's issuer". Neither set of notes uses the words security, vulnerability, advisory or CVE. On 28 September the maintainers published GitHub advisory GHSA-qx49-fqc8-xw99, rated High, which says that in earlier versions the client "let the MCP server a client connected to decide where the client's OAuth credentials were sent".
Put together, a change a reader could skim as housekeeping was, on the maintainers' own later account, the fix for a credential exposure in 40 stable 1.x releases and 4 stable 2.x releases (both counts derived from the PyPI release list). The fix was also public before the release: the first pull request for it was opened on 26 August, 33 days before the advisory.
What the number does not establish is most of what a reader wants to know. It is not the time from report to fix: neither the advisory nor Cycode, the security firm that says it reported the flaw, gives a report date. It does not show that anyone used the flaw: the advisory is silent on exploitation, Cycode describes a proof of concept it ran itself, and no source I read reports a victim. And it does not say whether you are exposed, because the advisory names no product that embeds the SDK. That has to be answered inside your own estate, and the checklist below is written for it.
What the advisory puts on the record
The advisory is short and specific. This table sets what it fixes on the record beside what it leaves out.
GHSA-qx49-fqc8-xw99 as published on 28 September 2026 in the modelcontextprotocol/python-sdk repository, read through the GitHub page and API
| Question | Fixed on the record | Not stated |
|---|---|---|
| Which versions | 1.9.1 to 1.29.1 on every path. 2.0.0 to 2.1.1 when the server offers no protected resource metadata or answers 403 insufficient_scope | Why 1.9.1 is the first affected release |
| Fixed in | 1.30.0 and 2.2.0, released 7 September 2026 | Any patch for earlier releases. The only workaround named is to trust the servers |
| Who is exposed | Applications using the SDK as an HTTP client with one of four OAuth providers, able to reach a server they do not fully trust while holding credentials for a real authorisation server | Any named product, vendor or count of applications |
| Who is not | MCP servers built with the SDK, stdio clients, clients that attach their own tokens or headers | Other MCP SDKs: the advisory covers the Python package only |
| Severity | High. CVSS 3.1 score 7.5 for the unattended providers, 6.5 for the interactive one | Who computed the vector. Any CVE, NVD or CNA score |
| Reporters | Eight reporter credits | The date of the first report, or whether the eight worked together |
| Exploitation | That a malicious or compromised server could direct the credentials | Any use in the wild, any victim, any affected product |
| Action | Upgrade. Pass issuer= for two providers. Clear stored registrations once. Rotate the client secret and revoke tokens if an untrusted server may have been reached | How to tell whether an untrusted server was reached |
Two details in that table deserve a sentence each. First, there is no CVE. The repository advisory carries a null CVE identifier, and an NVD keyword search found no record, when I checked on 29 September. The repository lists eight advisories since July 2025 and six have CVEs; the two without are this one and a medium-severity one published the same day. Second, the vectors are the maintainers' own. The advisory was published by the same maintainer account that authored the fix pull requests, so the 7.5 and the 6.5 are not an NVD, CISA or CNA judgement.
The order of events, from the timestamps
The timestamps come from GitHub and PyPI. Every interval below is derived by subtraction.
Timestamps from the GitHub API for the pull requests, releases and advisory. PyPI shows the same release date. The last column is derived
| Date | What happened | Days since the first row |
|---|---|---|
| 26 Aug 2026 | Pull request 3398 opened on the main branch: Validate the authorization server metadata issuer on every discovery path | 0 |
| 2 Sep 2026 | Pull request 3398 merged. The 1.x backport, 3431, is opened the same day | 7 |
| 4 Sep 2026 | Backport 3431 merged into the 1.x branch | 9 |
| 7 Sep 2026 | Releases 1.30.0 and 2.2.0 published. The notes list the change under Behaviour changes | 12 |
| 28 Sep 2026 | Advisory GHSA-qx49-fqc8-xw99 published at 20:06 UTC, the same day as Cycode's post | 33 |
Three intervals matter. The change was public for 12 days before it shipped and for 33 days before the advisory. The release-to-advisory gap is 21 days, which is the number in the title. Coordinated disclosure often runs in this order, so that users can upgrade before the detail is published. What the record cannot show is how many did. It also cannot show whether the pull request text, which already described the missing check in plain terms, was read by anyone with bad intent.
What the client was supposed to check
The check is not new, and it is not the SDK's invention. RFC 8414, published in June 2018, says the issuer value in authorisation server metadata must be identical to the issuer identifier used to build the address it was fetched from, and that "If these values are not identical, the data contained in the response MUST NOT be used." The MCP authorisation specification dated 28 July 2026 repeats the rule and gives the case a client must refuse: metadata fetched from a host called attacker.example that names a different, honest issuer. In its words, "If they differ, the client MUST NOT use the metadata."
The same specification has a second rule that this flaw also touches. Clients that use pre-registered credentials, or keep credentials obtained by dynamic registration, must associate them with the authorisation server that issued them, keyed by its issuer identifier, and must not reuse them when the server changes. The specification also leaves the choice of authorisation server to the client, not to the MCP server that advertises one. It calls authorisation itself optional for MCP implementations, so none of this binds a client that does no OAuth. But once a client does, the checks are mandatory.
One caution on dating. The 2025-11-25 revision, which I also read, tells clients to try several well-known addresses for discovery and cites RFC 8414, but in the text I read it does not restate the issuer rule or the binding rule. Both are explicit in the 28 July 2026 revision, the same day the SDK's 2.0.0 was released. The SDK's own history fits that: binding of stored credentials (pull request 2933) was merged to the main branch on 20 June 2026, and the 2.2.0 notes say the protected-resource-metadata path has done the issuer check since 2.0. The 1.x line never had either check. The maintainers' pull request also says the TypeScript client applies the issuer check on every path; I have not verified that independently.
The trust boundary the client crossed
An MCP client that speaks HTTP and OAuth has to decide, on every new connection, where its secrets go. The party telling it is the MCP server: the one component in the exchange the client's operator may not control. The advisory describes two ways that instruction could be wrong and go unchallenged. The server could name its own authorisation server in its protected resource metadata. Or it could publish none and serve metadata that presents the user's real authorisation server as the issuer, so the document looks right while the client is still talking to the wrong place. Either way, the client would send the client secret, the authorisation code and the PKCE code verifier meant for the real server, or a signed client assertion when the private key provider is in use, to a token endpoint the server chose.
The second route is the more instructive. The metadata is fetched from the server's own origin but claims someone else's identity, and the check that catches it is a comparison the client did not make.
Two conditions must hold, and the advisory states both. The application must use the SDK as an HTTP client with one of the four listed providers, and it must be able to connect to a server it does not fully trust while holding credentials for a legitimate authorisation server: a pre-provisioned secret or signing key, or a stored registration. The advisory says a malicious or compromised server, so a server you run yourself is not automatically safe. My reading, which is inference and not the advisory's wording, is that a deployment whose users and agents can add servers on their own is more exposed than one where the operator fixes the list.
The same release closed a second advisory with the same shape. GHSA-rwrf-2pqf-9j8j, published earlier on 28 September and rated Medium, says the client fetched addresses that a connected server chose inside a tool's output schema, and because the fetch had no timeout it could freeze every session in the process. It is a different flaw with the same boundary problem: input from the server treated as an instruction.
The release also changes redirect handling, so the HTTP client follows a redirect only within the endpoint's origin. The pull request explains that the old behaviour re-sent headers, authentication and request body to whichever host a redirect named. Neither the notes nor any advisory I found calls that a vulnerability. It matters to you for a practical reason: upgrading brings all of these changes together, and the redirect rule can break a deployment that relied on a cross-origin redirect.
What a stolen credential can reach
The advisory scores this as a confidentiality loss only: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N, 7.5, rated for the unattended providers. For the interactive provider, where a person has to start the sign-in, the same vector with user interaction required scores 6.5. The vector says what leaks. It does not say what an attacker can then do with it, and the advisory does not either.
Cycode fills that gap with a demonstration. It says it took the three stolen values, sent them to a real authorisation server enforcing PKCE, received a genuine access token, and used it to confirm account takeover. That is a vendor's lab result and I have not tried to reproduce it. What follows from how OAuth works, and is my inference rather than the advisory's, is this: a token issued to a client registration carries the scopes that registration was granted at that authorisation server. One registration often serves more than one MCP server and other applications, so the reach of a stolen secret is the reach of the registration, not of the one server the victim thought they were using.
The specification's token audience rules do not change that. They make an MCP server refuse tokens issued for someone else, which protects servers. I found nothing in the sections I read that narrows what a client's own registration can do at its own authorisation server. Cycode also points out that a client secret is long-lived, so revoking a token without rotating the secret leaves the door open. That is consistent with the advisory's instruction after upgrading: rotate the secret and revoke the tokens, not only upgrade.
Claims a reader might reasonably take from the coverage, tested against the sources named in this briefing
| Claim | Established? | Why |
|---|---|---|
| Attackers have used this flaw | No | The advisory is silent. Cycode reports a proof of concept it built. The Hacker News says none has been reported |
| Every MCP client is affected | No | The advisory covers the Python SDK's HTTP client with four named providers. Servers, stdio clients and clients with their own tokens are outside it. Other SDKs are not addressed |
| Upgrading fixes it | Not for two providers | ClientCredentialsOAuthProvider and PrivateKeyJWTOAuthProvider follow whatever server is advertised until issuer= is passed |
| A CVE-driven scanner will flag it | Not when I checked | No CVE. OSV and GitHub's global advisory endpoint had no record at about 09:50 UTC on 29 September |
| 21 days is the time to fix | No | It runs from release to advisory. The report date is not stated |
| The user would see something wrong | Unresolved | Cycode says the sign-in page shown is the real one. The advisory does not describe what a user sees |
| The protocol is at fault | Not on this record | The issuer rule is in RFC 8414 and is now in the MCP specification. The SDK's 1.x client did not apply it |
The scanner row needs its evidence. OSV's query for the PyPI package mcp returned twelve records at that time: six earlier GitHub advisories and their six PYSEC mirrors. Neither advisory from 28 September was among them, and GitHub's global advisory endpoint answered not found for this one. For the six earlier advisories, OSV's own published time trailed the repository advisory by between about three hours and 41 days (derived), so an absence today is a state that may change, not a verdict. It is still the state your tooling was in on the morning after.
Official is a provenance, not a control
The repository describes itself as "The official Python SDK for Model Context Protocol servers and clients". That is true, and it is a statement about who published the code, not about which checks the code performs. Three labels sit on this story and none of them is a control.
Three labels on this story against the record. Counts are derived from the PyPI release list and the repository advisory list
| Label | What it suggests | What the record shows |
|---|---|---|
| Official SDK | The reference client applies the protocol's checks | The 1.x client had no issuer check and no credential binding on any path across 40 stable releases, from 22 May 2025 to 7 September 2026 (473 days). The repository lists eight advisories since July 2025, seven of them High |
| Behaviour change | A default moved, read it at leisure | By the maintainers' later advisory, it closed a credential exposure. The release notes never say so |
| No CVE | Nothing to track | Two of the repository's eight advisories have none. Tooling keyed to CVE identifiers sees nothing |
This is not an accusation. The maintainers took the fix from first pull request to release in 12 days and had backported it to the maintenance line within nine. The point is narrower: a team that chose the official SDK because it is official inherited the SDK's gaps along with its convenience, and nothing in the label warned them.
Two practical details follow from the repository's own documents. Its security policy says only the newest release of a supported line receives fixes, and older 1.x releases are unsupported. And its README says that pip install mcp now installs 2.x, and suggests keeping a below-2 bound if you are not ready to migrate. A lockfile that pins 1.29.1 or earlier stays exposed however sensible the bound looks.
Method and interest
Cycode sells an application security platform that now covers AI-assisted development and offers its own MCP server, so its post is both research and marketing. The research claims, the three lab configurations and the demonstration against a real authorisation server, are Cycode's and are not in the advisory. Its argument that the 6.5 for the interactive provider understates the risk rests on three scenarios: a poisoned server directory, a prompt injection that steers an agent to a server, and a hijacked DNS name. They are plausible and they are Cycode's, not the maintainers'; the advisory says only a malicious or compromised server. The post's HTML title also reads "CVE Fix Inside", while no CVE exists.
Cycode calls the package Anthropic's MCP Python SDK and says it reported the flaw to Anthropic's MCP team. The repository sits in the modelcontextprotocol GitHub organisation and the advisory does not mention Anthropic. Anthropic created MCP and, on 9 December 2025, announced it was donating the protocol to the Agentic AI Foundation under the Linux Foundation, saying the same maintainers would keep making decisions. I have treated Anthropic as involved for disclosure purposes, which is why the next line is here. This analysis was researched with Claude, made by Anthropic.
The Hacker News article is secondary. It independently observes that the fix appeared in the notes as a behaviour change, which matches the notes themselves. Every other figure in this briefing comes from the GitHub, PyPI, MCP specification and IETF records listed in the sources, or from Cycode's own post where it is named as such.
What to do, in the order worth doing it
Take this with you
Actions, in order
- Find every Python environment that installs the mcp package and record its version: lockfiles, container images, agent frameworks, notebooks and CI runners, including transitive dependencies.
- Decide who is exposed: anything that uses the SDK as an HTTP client with OAuthClientProvider, ClientCredentialsOAuthProvider, PrivateKeyJWTOAuthProvider or the deprecated 1.x RFC7523OAuthClientProvider. Servers built with the SDK and stdio clients are outside the advisory.
- Upgrade to 1.30.0 on the 1.x line or 2.2.0 or later. If you keep a below-2 bound, confirm the lockfile resolves to 1.30.0 or later. Test first: the same release changes redirect handling and expires idle sessions.
- For ClientCredentialsOAuthProvider and PrivateKeyJWTOAuthProvider, pass issuer= naming the authorisation server that issued the credentials. The advisory says upgrading changes nothing until you do. On 1.30.0 the warning is a DeprecationWarning that Python hides by default, so turn warnings on in CI to see it.
- Move off the deprecated RFC7523OAuthClientProvider. It has no issuer option.
- Clear stored OAuth client registrations once. Registrations stored by earlier versions carry no issuer and stay unbound. If you placed a pre-registered record in storage yourself, set its issuer.
- If a client may have connected to a server you do not trust, rotate its client secret and revoke its tokens at the authorisation server, as the advisory says. If it used a signed assertion, treat the signing key as exposed too. That last step is my inference: the advisory names the assertion, not the key.
- Compare token endpoint requests for your client identifiers against your own egress addresses. Cycode says nothing in the sign-in would look wrong, so this is a comparison to make, not an alarm to wait for.
- List which MCP servers each agent may connect to, remove any nobody chose, and enforce the list outside the agent. The advisory's only workaround is to connect OAuth-enabled clients only to servers you trust, and that includes servers an agent can select for itself.
- Record GHSA-qx49-fqc8-xw99 and GHSA-rwrf-2pqf-9j8j in your own exception or tracking list. Neither has a CVE, and your scanner may not list them yet.
python -m pip show mcp
python -m pip install --upgrade "mcp>=1.30.0,<2"
python -m pip install --upgrade "mcp>=2.2.0"
The question that exposes the gap
The maintainers fixed the flaw, backported it and released before they published the advisory. That order has a benefit and a cost, and I make no judgement on the trade. The cost is visible in the record: for 21 days the only signal in the release itself was a heading called Behaviour changes. Official, behaviour change and no CVE tell you who published something and how it was filed. They do not tell you where your credentials went.
Which of your agents holds a credential for a real login service while being allowed to connect to an MCP server nobody on your team has reviewed, and would your token logs show a second caller?
Key facts
Sources
- PrimaryAdvisory GHSA-qx49-fqc8-xw99, published 28 September 2026. Read in full on the page and through the repository advisories API. Used for the mechanism, affected and fixed versions, both CVSS vectors, the CWE list, the credits, the null CVE field and the remediation steps.modelcontextprotocol/python-sdk maintainers (GitHub)accessed 2026-09-29
- PrimaryRelease notes for 1.30.0, published 7 September 2026. Used for the Behaviour changes heading, the issuer check wording, the issuer= keyword and the deprecation warning, and for the absence of any security wording.modelcontextprotocol/python-sdk maintainers (GitHub)accessed 2026-09-29
- PrimaryRelease notes for 2.2.0, published 7 September 2026. Used for the legacy path wording, the statement that the protected-resource-metadata path has checked the issuer since 2.0, and the redirect and idle session changes.modelcontextprotocol/python-sdk maintainers (GitHub)accessed 2026-09-29
- PrimaryPull request 3398 for the main branch, opened 26 August and merged 2 September 2026. Used for the plain description of the missing check and the binding of stored credentials, and for the statement about the TypeScript client.modelcontextprotocol/python-sdk maintainers (GitHub)accessed 2026-09-29
- PrimaryPull request 3431, the 1.x backport, opened 2 September and merged 4 September 2026. Used for the 1.x behaviour: no issuer check and no credential binding before the change.modelcontextprotocol/python-sdk maintainers (GitHub)accessed 2026-09-29
- PrimarySecond advisory published 28 September 2026 about server-chosen schema references, fixed in the same releases. Used for the shared trust boundary point.modelcontextprotocol/python-sdk maintainers (GitHub)accessed 2026-09-29
- PrimarySecurity policy of the repository. Used for the statement that only the newest release of a supported line receives fixes.modelcontextprotocol/python-sdk maintainers (GitHub)accessed 2026-09-29
- PrimaryRelease history of the mcp package. Used to count the stable releases in the affected range and for the upload dates of 1.9.1, 1.30.0 and 2.2.0.Python Package Indexaccessed 2026-09-29
- PrimaryAuthorization Server Discovery, specification revision 2026-07-28. Used for the issuer identity rule and its attacker.example example, and for the client's responsibility to select the authorisation server.Model Context Protocolaccessed 2026-09-29
- PrimaryClient Registration, revision 2026-07-28. Used for the Authorization Server Binding rule for stored credentials.Model Context Protocolaccessed 2026-09-29
- PrimaryAuthorization overview, revision 2026-07-28. Used for the statement that authorisation is optional, and for the token handling and audience requirements.Model Context Protocolaccessed 2026-09-29
- PrimaryAuthorization, revision 2025-11-25. Read to test whether the earlier revision states the issuer and binding rules.Model Context Protocolaccessed 2026-09-29
- PrimaryRFC 8414, OAuth 2.0 Authorization Server Metadata, June 2018. Used for section 3.3, the issuer validation rule.IETFaccessed 2026-09-29
- PrimaryCycode's research post of 28 September 2026, by Yuval Elbar. A vendor's own account. Used for the lab demonstration, the three risk scenarios and its remediation view, each attributed to Cycode.Cycodeaccessed 2026-09-29
- PrimaryAnnouncement of 9 December 2025 that Anthropic is donating MCP to the Agentic AI Foundation. Used for the governance and disclosure context.Model Context Protocolaccessed 2026-09-29
- PrimaryLookup of the advisory identifier in OSV on 29 September 2026, which returned not found, plus a package query for mcp on PyPI. Used for the scanner coverage check.OSVaccessed 2026-09-29
- PrimaryKeyword search of NVD on 29 September 2026 that returned no records. Used for the absence of an NVD record.NIST NVDaccessed 2026-09-29
- Reported byNews coverage of 29 September 2026. A pointer to the primary sources only. Used for its independent note that the fix was listed as a behaviour change and that no attacks have been reported.The Hacker Newsaccessed 2026-09-29


