How a malicious MCP server could steal your OAuth login
September 30, 2026 · mcp · oauth · cybersecurity · ai-agents · python · developer-tools · anthropic
Cycode and Anthropic disclosed a High-severity flaw in the official MCP Python SDK: a malicious server could skip issuer checks and steal OAuth credentials. Here is what happened and how to patch.
How a malicious MCP server could steal your OAuth login
If you build AI agents that talk to tools over the Model Context Protocol, pause for a minute. On September 28, 2026, Cycode and the MCP maintainers disclosed a High-severity bug in the official MCP Python SDK. A malicious MCP server could trick a vulnerable client into handing over the OAuth credentials it uses to log in to a real identity provider - client secret, authorization code, and PKCE proof key included.
No CVE has been assigned. The advisory tracks it as GHSA-qx49-fqc8-xw99. Fixes shipped in mcp 1.30.0 and 2.2.0. Neither Cycode nor the advisory reports known exploitation in the wild. That does not mean you can wait. The package is widely used, the login page looks legitimate, and upgrading alone is not enough for some auth providers.
What MCP is doing here
MCP is an open standard for connecting AI apps to external tools and data. Anthropic introduced it; it is now under the Linux Foundation's Agentic AI Foundation. The official Python SDK on PyPI is the package most teams use to build MCP clients and servers.
When an MCP client needs to authenticate over HTTP, it uses OAuth. That means asking "where is the login service?" and then sending secrets to a token endpoint. Those two steps are security-critical. If the client trusts the wrong answer, credentials leave the machine.
How the bug worked
Discovery is supposed to look like this:
- The client asks the MCP server which authorization server (Google, Okta, Azure AD, and so on) handles logins.
- The SDK fetches that authorization server's metadata.
- The SDK checks that the metadata's
issuermatches the URL from step 1.
Step 3 is the control that stops a lying server. On affected versions, a fallback path skipped it.
Cycode's researcher showed that when a malicious MCP server returns HTTP 404 for the standard discovery endpoint (/.well-known/oauth-authorization-server), the SDK falls back to configuration the MCP server itself supplies. On that path, the expected issuer was empty, so the issuer validation never ran. The attacker could then:
- Point the interactive login page at the real identity provider (so the user sees a genuine Google/Okta/Azure AD screen)
- Point the token endpoint at a server they control
- Set
issuerto the victim's real login provider so a second "credential binding" check also passed
After the user approved the real login page, the SDK sent the client secret, authorization code, and PKCE code_verifier to the attacker. The attacker exchanged them at the real authorization server and received a valid access token with whatever scopes the app had.
From the victim's side, the MCP server just looked flaky after login. From the identity provider's side, the exchange looked legitimate: correct secret, valid code, valid PKCE. Standard monitoring is unlikely to flag it.
Who is affected
You are in scope if all of these are true:
- Your app uses the official MCP Python SDK as an HTTP client
- It uses
OAuthClientProvider,ClientCredentialsOAuthProvider,PrivateKeyJWTOAuthProvider, or the deprecated 1.xRFC7523OAuthClientProvider - It can connect to an MCP server you do not fully control while holding credentials for a real login service
| Line | Affected | Fixed |
|---|---|---|
| 1.x | 1.9.1 through 1.29.1 | 1.30.0 |
| 2.x | 2.0.0 through 2.1.1 | 2.2.0 |
Severity is rated 7.5 for the two machine-to-machine providers (no human in the loop) and 6.5 for the interactive provider. Cycode notes that "user interaction" often means approving a real login page - the one action people are trained to trust.
Not affected: MCP servers built with the SDK, local stdio clients, and clients that attach their own tokens or headers.
Why this is easy to miss in patch queues
Two details make this dangerous for ops teams:
- No CVE. Many scanners and ticket automations key off CVE IDs. GHSA-only issues can sit unseen.
- Fixes looked like behavior changes. Issuer checks landed in the 1.30.0 and 2.2.0 release notes on September 7 under behavior changes, not as a security advisory. The advisory itself followed on September 28, the same day Cycode published its write-up.
If your dependency bot only screams for CVEs, check your mcp version by hand.
What to do now
1. Upgrade the SDK
pip install "mcp>=2.2.0"
# or on the 1.x line:
pip install "mcp>=1.30.0"
Pin and redeploy. Confirm the installed version in your runtime image, not only in a lockfile on a laptop.
2. Pass issuer= for machine-to-machine providers
For ClientCredentialsOAuthProvider and PrivateKeyJWTOAuthProvider, the advisory is blunt: upgrading changes nothing until you also pass issuer= to name the login service those credentials belong to. Without it, those providers still follow whichever authorization server the MCP server advertises.
Example shape:
ClientCredentialsOAuthProvider(
# ... your existing args ...
issuer="https://auth.example.com",
)
Omitting issuer= now produces a deprecation warning (easy to miss if Python hides DeprecationWarning). It becomes required in 3.0. The deprecated RFC7523OAuthClientProvider has no issuer= option - migrate off it.
3. Clear old OAuth client registrations
Registrations saved by older SDK versions are not labeled with a login provider. Clear stored OAuth client information once after upgrading so the client re-registers with the binding in place.
4. Rotate if you might already be exposed
If a vulnerable client may have connected to an untrusted MCP server:
- Rotate the OAuth client secret
- Revoke access and refresh tokens at the identity provider
- Review which scopes that client held - blast radius follows the scopes
On older versions, the only mitigation is connecting solely to MCP servers you fully trust.
Practical hardening beyond the patch
Even after you are on a fixed SDK, treat MCP server identity like you treat package registries:
- Prefer allowlists. Do not let agents auto-connect to arbitrary MCP servers from a public catalog without review.
- Watch for typosquats and poisoned listings. Cycode calls out registry/catalog poisoning as a realistic way the "user chose the server" assumption fails.
- Be careful with agent-selected tools. If a model picks MCP servers on the user's behalf, prompt injection can steer it toward a hostile server. Keep human approval for new server connections and for OAuth grants with broad scopes.
- Scope tightly. A stolen client secret inherits every scope you granted. Prefer least privilege and short-lived credentials where your IdP supports them.
- Log the MCP server URL alongside OAuth events so you can answer "which servers did this client talk to?" after an incident.
Short FAQ
Was this exploited in the wild?
Neither the advisory nor Cycode reports known attacks as of the September 28-29 disclosures.
Is the login page fake?
No. In the demonstrated attack, the user approves the genuine identity-provider page. The theft happens when the SDK posts the token request to an attacker-controlled endpoint.
Do I need to worry if I only run MCP servers, not clients?
Servers built with the SDK, and local stdio clients, are out of scope for this specific issue. HTTP clients using the affected OAuth providers are in scope.
Why does rotating only the access token not fix it?
The client secret is long-lived. An attacker who stole it can mint new tokens until you rotate the secret and revoke outstanding tokens.
Bottom line
This is not a flashy model jailbreak. It is plumbing: an issuer check that did not run on a fallback path, feeding the next checks with attacker-controlled input. The controls existed; they just were not always on.
Upgrade to mcp 1.30.0 or 2.2.0, set issuer= on machine-to-machine providers, clear old registrations, and rotate secrets if an untrusted server was ever in the path. If your team ships agents that connect to third-party MCP tools, treat that connection list like production credentials - because after this bug, that is exactly what it became.
Sources: Cycode disclosure, The Hacker News, GitHub advisory GHSA-qx49-fqc8-xw99.