The Login Page Was Real — and That Was the Attack: Account Takeover in Anthropic's MCP Python SDK

On 28 September 2026 Cycode published coordinated-disclosure research by Yuval Elbar describing full account takeover in Anthropic's MCP Python SDK — the official client library that connects AI assistants to MCP tool servers. A malicious MCP server can hijack the SDK's OAuth login flow and walk away with the victim's client secret, authorization code, and PKCE proof key, then exchange them at the real identity provider for a valid access token. The victim's only symptom is a login page that is completely genuine — because it is the real Google, Okta, or Azure AD page — followed by a server that appears to flake out. Affected versions are mcp 1.9.1 through 1.29.1 and 2.0.0 through 2.1.1; the fix shipped in 2.2.0 and 1.30.0. Cycode rates the unattended providers High at 7.5.

A safety check that only runs when the attacker allows it

The mechanism is worth studying because it is a pattern, not a typo. When the SDK connects to a server that demands login, it must discover two URLs: where to send the user for the login page, and where to exchange the result for a token. The safe path fetches the login provider's configuration and then checks that the configuration's issuer field matches the independently discovered authorization-server URL. That issuer check is the control that stops a malicious server from lying about its identity.

But there is a fallback path for servers that do not support modern discovery, and on that path the SDK never obtained a URL — internally the value is None — so the guard, written as if self.context.auth_server_url is not None: validate_metadata_issuer(...), never executes. The attacker triggers the fallback by doing nothing at all: return 404 to the discovery request. From there the SDK accepts the attacker's login configuration unverified, including attacker-controlled token endpoints.

It compounds. A second control, credential binding, is supposed to verify that stored credentials belong to the current login provider — but it checks the binding against the issuer value from that same unverified configuration. The attacker simply names the victim's real provider, and the check passes on a lie. A third control, audience restriction on the authorization code, is likewise only applied on the modern path, so the stolen code is redeemable anywhere. Cycode's write-up puts it precisely: all the controls existed, they were all fed the same unverified input, and they all said "looks good."

Cycode proved the point with a three-way test: the attack configuration (404, no issuer check, takeover succeeds), a control where discovery succeeds (issuer check runs, flow halts before credentials move), and an "escape" where the attacker provides discovery naming itself (issuer check passes, but credential binding then discards the victim's credentials and the attacker gains nothing). The attack is the only configuration in which real credentials leave the machine.

Why the user-interaction caveat dissolves in agent setups

The interactive provider (OAuthClientProvider) scores 6.5 because a human must approve the login — but the login page is genuine, at the genuine URL, with a genuine certificate, which is exactly the action users are trained never to hesitate over. The unattended machine-to-machine providers (ClientCredentialsOAuthProvider and the private-key-JWT provider) need no interaction at all and carry the 7.5 rating. And three realistic setups remove even the thin human barrier: a typosquatted or compromised entry in an MCP registry, a DNS or network compromise redirecting a trusted server name, and — most on-theme for this site — prompt injection steering an AI agent to connect to the attacker's server on the user's behalf, where the model, not the human, makes the connection decision. The stolen client secret is long-lived and often unexpiring, refresh tokens can extend access indefinitely, and the authorization server's logs show a perfectly legitimate login: correct secret, valid code, correct PKCE proof. Standard monitoring has nothing to flag.

This is the same trust surface this site has been tracking all year: the SDK is the choke point, and whoever controls the server URL controls the credential flow. See the MCP design-flaw RCE analysis, the state of MCP security in 2026, and the GitLab MCP token-exfiltration flaws.

What to do

  • Upgrade to mcp 2.2.0 (2.x line) or 1.30.0 (1.x line) or later. Both shipped 7 September ahead of this disclosure; the upstream v2.2.0 release notes confirm the OAuth client now checks the authorization server's issuer on the legacy path too (upstream #3398).
  • Pass issuer= explicitly on the machine-to-machine providers (e.g. issuer="https://auth.example.com"). Without it they still follow whichever provider the server names; omission now warns and becomes mandatory in 3.0.
  • Clear stored OAuth registrations once after upgrading. Registrations saved by older versions carry no provider label and remain unbound — re-register fresh so the label is in place.
  • If exposure is possible, rotate and revoke. Any client that may have touched an untrusted server should rotate its client secret and revoke tokens at the identity provider; revoking the session token alone leaves the long-lived secret in attacker hands. On unpatched versions there is no workaround short of connecting only to servers you fully control.

Sources: