```html ```
top of page

A Malicious MCP Server Could Name Its Own Login Service, and the Official Python SDK Believed It. 219 Million Downloads a Month. Here Is What Our Judge Can See, and What It Can't.

Writer: Patrick Duggan
Patrick Duggan
1 hour ago
6 min read

When an AI agent built on the official Model Context Protocol Python SDK needed to log in somewhere, it asked the server it was connecting to where the login service was. If that server declined to answer properly, the SDK accepted whatever the server said next and never checked it. A hostile server could name its own login service, and the agent would hand over its client secret, the authorization code and the PKCE proof key.


That is GHSA-qx49-fqc8-xw99, found by Yuval Elbar at Cycode and published on September 28. It is fixed. Nobody has reported exploitation in the wild. The package it lives in, mcp on PyPI, was pulled 219 million times last month.


We run two MCP servers, and one of them exists to judge other MCP servers before you connect to them. So this is our lane, and the honest answer to "would your tooling have caught it" is more interesting than yes.





What broke


MCP authentication follows OAuth. A client that needs a token first asks the MCP server for its protected-resource metadata, which says which authorization server to use. Then it fetches that authorization server's own metadata and checks that the issuer it names matches what it expected. That match is the whole point. It is how the client knows the login service is the one the resource actually trusts.


Cycode found that the check was conditional. If step one returned a URL, the SDK validated the login provider's identity. If step one returned a 404, the SDK fell back to metadata the server itself supplied, and the expected issuer was simply empty. An empty expectation matches anything. So a malicious server only had to decline the first question, and it could steer the rest of the flow.


The user experience stays clean. Per Cycode, the person approving the login sees the real provider's page. What leaks is the client secret, which stays valid until someone rotates it by hand, plus the authorization code and the PKCE verifier. The code carries no audience restriction.


Affected versions are 1.9.1 through 1.29.1 and 2.0.0 through 2.1.1, across OAuthClientProvider, ClientCredentialsOAuthProvider, PrivateKeyJWTOAuthProvider and the deprecated RFC7523OAuthClientProvider. Cycode scores it 7.5 for the unattended machine-to-machine providers and 6.5 for the interactive one. The fixes are 1.30.0 and 2.2.0.



What to do


Upgrade to 1.30.0 or 2.2.0. For the two providers where the advisory calls for it, also pass the expected issuer explicitly, so the check has something to compare against.


Then clear your stored OAuth client registrations once. Registrations made on the old versions are not bound to a login service and stay that way after the upgrade.


If any agent of yours may have connected to an MCP server you do not control, rotate that client's secret and revoke its tokens at the login service. The secret is the part that keeps working after the session ends.


This is a client-side bug. If you only run an MCP server, you are not the vulnerable party. The question for you is whether your consumers upgraded.





Dredd judges, and here is exactly what it judged


Dredd is our pre-flight check for MCP servers. You give it a server name, and it tells you what our corpus knows about that server and its dependency graph before your agent connects. We ran it this afternoon against three names, and we are reporting the output flat.


A made-up server name got REVIEW, severity unknown, known_to_us false. That is the correct answer. Dredd's own documentation says REVIEW means we hold no record, not that the server is safe, and that a brand-new attacker-published server looks exactly like this. A malicious server built to exploit this bug would almost certainly be new and unknown. Dredd would have told you "we have never seen this," which is worth hearing before you hand anything an OAuth flow.


The official python-sdk repository name also came back REVIEW, known_to_us false. Dredd indexes servers, and the SDK is a library, so that is expected.


The third result is the one to learn from. We asked about "github." Dredd matched by substring, resolved it to a small third-party server that is not GitHub's own, and returned ALLOW, severity low, with the dependency graph marked scanned but zero dependencies indexed. A verdict is only as good as the identity it resolved, and a bare name that resolves to somebody else's server is the same class of confusion this bug exploits: the client trusting the wrong party's answer about who it is talking to. That is ours to fix. An ambiguous name should come back ambiguous, not ALLOW.


The bigger point stands either way. Dredd judges reputation. It never sees the discovery handshake, so it could not have spotted the fallback itself. The fix for this bug is the SDK upgrade, not a reputation check. What a pre-flight judge adds is a reason to slow down before connecting to a server nobody has vouched for. That is where this attack starts.


One more honest number. Our own status check this morning found that Dredd's recent tool calls were logged with an unknown verdict: it answered, but the log did not record a judgment. We will not claim Dredd as protection people are actively getting until that denominator reads clean.



Jeevesus saved, in the boring way


Jeevesus, our threat-intel MCP server, is not built on the Python SDK. It is a hand-rolled JSON-RPC handler in our own Node service. It does not use OAuth at all. Paid tools take a bearer API key, and an unauthenticated call gets a JSON-RPC error inside an HTTP 200, not an HTTP 401 with a login challenge. We checked: both of the well-known OAuth metadata paths on our host return 404, and no MCP OAuth client ever starts a discovery flow against us.


So a Python-SDK agent connecting to Jeevesus was never exposed through Jeevesus. It could still be exposed by the next server it connects to, which is why the upgrade matters regardless of who you trust.


We also ran the package itself through our own supply-chain guardrail. Jeevesus check-package on PyPI package mcp, version 2.1.1 returns allow: not on the malicious-package deny-list, reputation risk zero, 678 days old, 219,043,071 downloads last month. That is the right answer. The deny-list is for packages that are malicious, and a vulnerable legitimate package is a different thing. A deny-list will never catch this class, and neither will your dependency scanner's malware check. You need the advisory feed, which means reading GHSA-qx49-fqc8-xw99 and upgrading.


We tried to count how many public MCP servers advertise OAuth using our registry crawl. We can't do it honestly. The registry record carries no SDK or auth-method field, and a keyword search for "oauth" hits the 50,000-result cap across daily snapshots. It counts records, not servers. We are not going to print that number.



Where we were in April


On April 21 we published "Anthropic's MCP Has a Critical RCE Vulnerability. We Don't Use MCP. Here's Why." Eleven days later we shipped Jeevesus. Two days after that we shipped Dredd and wrote "Jeevesus Saves, Dredd Judges."


That is the record, and it is on the record because we keep one. The April position was that MCP's defaults were not ready to trust. The May position was that we would ship servers built our own way, with no stdio transport, no OAuth surface and a judge in front, instead of waiting for the ecosystem. This week's bug is a fair test of that choice. Our server was not in the blast radius because it never took the OAuth path. Our judge would have flagged an unknown server and could not have seen the flaw itself. Both are true.



Credit


Cycode found it, reported it through Anthropic's security process, and published a clear write-up that tells defenders exactly what to rotate. Anthropic shipped fixes on both release lines. That is the process working. Anthropic is our partner, and we are covering this straight: the official SDK shipped an authentication check that could be skipped by returning a 404, for most of two major version lines. It is fixed now. Upgrade.


Sources: Cycode, The Hacker News. Confidence in our reading of the mechanism: high, capped at 95%, because we are describing Cycode's analysis and not our own reproduction.


Was this useful? Rate this post. The widget is at the bottom of the page, and we read every response.




Every indicator in this post is in the feed. Free.

1.58M+ IOCs, STIX 2.1 / TAXII, 88% novel vs ThreatFox, exploited-CVE leads ahead of CISA. No credit card — a free API key in 30 seconds, and you can audit every claim above against the live endpoints.



Was this useful? Thirty seconds, no cookies, no tracking, no third parties, your address hashed and never stored. If the box below does not load, the same question lives at https://analytics.dugganusa.com/nps.html?post=a-malicious-mcp-server-could-name-its-own-login-service-and-the-official-python-sdk-believed-it-21



Comments

Rated 0 out of 5 stars.
No ratings yet

Add a rating
bottom of page