The MCP Python SDK OAuth flaw is a short story with a long tail. On 28 September 2026 the maintainers of the official MCP Python SDK published advisory GHSA-qx49-fqc8-xw99, rated High at CVSS 7.5: an OAuth client could be talked into sending its credentials to an authorization server chosen by the MCP server it was connecting to. Upgrading takes a minute. Working out which secrets left your estate takes longer, and that is the part worth scheduling this week.
The connecting client trusted the server to say where the login provider lived.
TL;DR
- Affected per the advisory: MCP Python SDK 1.9.1 through 1.29.1 and 2.0.0a1 through 2.1.1. Fixed in 1.30.0 and 2.2.0.
- Root cause, in the advisory's words: the authorization server metadata issuer was not validated on every discovery path, and stored or pre-provisioned client credentials were not bound to the authorization server they belong to.
- What a malicious server could collect: the client_secret, the authorization code and the PKCE code_verifier, or a signed client assertion when PrivateKeyJWTOAuthProvider is in use.
- Patching is step one. The advisory also asks for an explicit issuer= on pre-provisioned providers and for clearing registrations stored by older versions.
#What the advisory says
An MCP client that authenticates to a remote server does not have the login provider configured up front. It asks. The server publishes protected resource metadata, the client reads which authorization server to use, fetches that server's metadata and runs a normal OAuth flow against it. Three providers in the SDK do this over HTTP: OAuthClientProvider, ClientCredentialsOAuthProvider and PrivateKeyJWTOAuthProvider. All three were affected.
Per the advisory, a malicious server had two ways in. It could name its own authorization server in its protected resource metadata. Or it could publish none at all and serve metadata that presents the user's real authorization server as the issuer, which is the case that matters, because the check that would have caught the lie was tied to a discovery path the server could simply decline to use.
Cycode researcher Yuval Elbar, who wrote up the finding on the same day, describes that fallback plainly: the malicious server answers the first discovery request with a 404, the client falls back to the legacy method and asks the attacker's host for the login configuration, and the identity check never runs because there was no URL from the first request to compare against. The advisory credits eight reporters. At the time of writing no CVE identifier has been assigned.
What the user sees is a real login page at their real provider, because the attacker points the authorization endpoint at the genuine service and keeps only the token endpoint for himself. Consent looks correct. The approval is correct. The credentials go to the wrong place afterwards.
#The client secret is the real loss
The authorization code is a single-use ticket, and PKCE was designed to stop a stolen one from being redeemed by anybody else. Here the attacker receives the code_verifier in the same request, so PKCE proves nothing. That much is bad and short-lived.
The client_secret is the part that outlives the incident. It is a non-human identity: it identifies the application rather than the person, it does not expire on its own, it is usually the same value across every user of that client, and nothing about presenting it from a new IP address looks unusual to the provider. An attacker holding it can keep requesting tokens within whatever the application was granted, long after the developer who ran the vulnerable client has moved on.
That is the same shape as the Salesloft Drift breach, where stolen OAuth tokens reached hundreds of companies' data. One credential belonging to an integration, trusted by design, valid until somebody revokes it. The MCP ecosystem now has thousands of those, handed out during ordinary setup and inventoried almost nowhere.
#Anatomy of the path
# per GHSA-qx49-fqc8-xw99 and Cycode's write-up mcp-client # asks the server where to log in → server-metadata # 404, client falls back to legacy discovery → attacker-issuer # claims to be the real provider, unvalidated → client-secret # secret, code and PKCE verifier posted to attacker → saas-data # attacker mints tokens at the real provider
Not one step in that path is a memory bug or an injection. The client followed the protocol, the user approved a genuine consent screen, and the provider issued a token to a caller presenting valid credentials. The only defect is that a value supplied by the other side of the connection was believed without being checked.
This is why an inventory of vulnerabilities and an inventory of access answer different questions. A scanner tells you the SDK version. It does not tell you which authorization servers that client ever spoke to, which secret it was holding, or what that secret still reaches.
#Who has to assume exposure
Being on an affected version is not the same as being compromised. The flaw needs a malicious or compromised MCP server on the other end. So the question to answer is narrower than "were we vulnerable": which remote MCP servers did our clients authenticate to, and do we trust every one of them and everyone who could have changed them.
For a client that only ever connected to servers you run yourself, the exposure is low and the fix is routine. For a client that connected to a community server, a vendor's hosted endpoint, or anything installed from a registry by a developer trying something out, treat the secret as potentially disclosed. Rotating a client secret is cheap. Discovering nine months from now that one was never rotated is not.
There is a second group that should read the advisory carefully: teams using ClientCredentialsOAuthProvider or PrivateKeyJWTOAuthProvider. Those are the machine-to-machine setups, with pre-provisioned credentials and usually broader scopes than an interactive user would ever get. The fixed versions now require the issuer= parameter for them, which means an upgrade alone will surface the places where nobody had written down which provider a credential belonged to.
#What to check this week
- Find every installed version, not every declared version. Search lockfiles and running environments for the mcp package. Anything from 1.9.1 to 1.29.1 or 2.0.0a1 to 2.1.1 is in scope. Agent tooling gets installed per developer, so a policy statement is not an answer here.
- Upgrade to 1.30.0 or 2.2.0. Pick the line you are already on. This closes the path but does nothing about credentials that may already have moved.
- List the remote MCP servers your clients authenticated to. Client configs, environment files, developer machines. Split the list into servers you operate and servers somebody else operates.
- Rotate the client secrets for everything in the second group. Cycode recommends rotating secrets and revoking tokens where exposure is possible. Treat any third-party or community server as possible exposure, because you cannot prove the negative from your side.
- Clear client registrations saved by older versions. The advisory notes those were stored without a binding to an authorization server. Delete and re-register rather than carrying them forward.
- Set issuer= explicitly on every pre-provisioned provider. This is now required for the client credentials and private key JWT providers, and it forces someone to state which authorization server each machine credential belongs to.
- Look at the token logs at the provider, not only at your own. Client credential grants from an address range you do not recognise are the signal that would distinguish a theoretical exposure from a used one. Check whether those logs exist and how far back they go before you need them.
- Write down what each MCP client secret reaches. Scope list, tenant, and the data behind it. If the answer takes more than a day to assemble, that gap is the finding, independent of this advisory. Our MCP hardening checklist covers the surrounding controls.
On Elmoz
#FAQ
Which MCP Python SDK versions are affected?
Per advisory GHSA-qx49-fqc8-xw99, versions 1.9.1 through 1.29.1 and 2.0.0a1 through 2.1.1 are affected. The fixes are in 1.30.0 and 2.2.0.
What can an attacker steal through the MCP OAuth flaw?
Per the advisory, a malicious MCP server can receive the client_secret, the authorization code and the PKCE code_verifier meant for the real authorization server, or a signed client assertion when PrivateKeyJWTOAuthProvider is used.
Is upgrading the MCP Python SDK enough?
No. The advisory also asks teams to pass the issuer parameter explicitly for pre-provisioned credential providers and to clear client registrations stored by older versions, because those were saved without an issuer binding. Cycode additionally recommends rotating client secrets and revoking tokens that may have been exposed.
The patch landed the same day the research did, which is how this is supposed to work. What the patch cannot do is tell you which client secrets your agents were carrying, which servers they presented them to, and what those secrets still open. Elmoz maps that reach, so the answer is already written down the next time an advisory like this one arrives.