Threat Overview
The maintainers of the official Model Context Protocol Python SDK published GHSA-qx49-fqc8-xw99 on September 28, 2026. A malicious or compromised remote MCP server can influence OAuth discovery and direct an affected SDK client to send a legitimate authorization server’s client secret, authorization code, PKCE code_verifier, or a signed client assertion to an attacker-controlled token endpoint. The weakness is rated High, CVSS 7.5 for unattended providers, and had no CVE assignment as of September 29. Affected clients use the SDK over HTTP with OAuthClientProvider, ClientCredentialsOAuthProvider, PrivateKeyJWTOAuthProvider, or deprecated 1.x RFC7523OAuthClientProvider, and may connect to servers they do not fully trust while holding credentials for a legitimate identity provider. Affected releases are 1.9.1–1.29.1 and 2.0.0–2.1.1; fixed releases are 1.30.0 and 2.2.0. For the two unattended providers, upgrading alone is insufficient until the application explicitly passes issuer=. MCP servers built with the SDK, stdio clients, and clients attaching their own tokens or headers are not affected.
References
- GitHub Security Advisory GHSA-qx49-fqc8-xw99 — Official MCP Python SDK — September 28, 2026.
- Cycode — MCP Python SDK OAuth account takeover research — updated September 29, 2026.
- The Hacker News — Official MCP Python SDK flaw can let malicious servers steal OAuth credentials — September 29, 2026.
Impacted Systems
- Vendor/product: Official
modelcontextprotocol/python-sdk, PyPI packagemcp, OAuth client componentmcp.client.auth. - Affected versions: 1.9.1 through 1.29.1; 2.0.0 through 2.1.1, including 2.0.0 prereleases covered by the advisory range.
- Fixed versions: 1.30.0 and 2.2.0 or later.
- Platform: Any operating system running an affected Python MCP client over HTTP.
- Required configuration: One of the affected OAuth providers and credentials for a legitimate authorization server; the client connects to an MCP server it does not fully trust.
- Additional requirement:
ClientCredentialsOAuthProviderandPrivateKeyJWTOAuthProvidermust setissuer=even after upgrading. DeprecatedRFC7523OAuthClientProviderhas no issuer option and should be replaced. - Unaffected: SDK-built MCP servers, stdio clients, and clients supplying their own tokens/headers.
Why this matters
MCP clients commonly bridge AI applications to cloud APIs, internal tools, data stores, and automation services. Theft of a long-lived client secret or signed assertion can produce apparently valid tokens with the application’s existing scopes. The exploit can involve a real identity-provider login page, reducing user-visible warning signs, and unattended providers require no human interaction.
Exploitation Status
The SDK advisory and Cycode confirm the vulnerable behavior and an end-to-end proof of concept. Neither source reports in-the-wild exploitation as of September 29. There are no published attacker IOCs. The hunts below therefore focus on suspicious OAuth outcomes, rare service-principal sign-in origins, credential/consent changes, and Python client connections to newly observed destinations. These are behavioral leads, not signatures for this flaw.
What this hunt looks for
MCP service identities authenticating to unexpected tenants, new or changed OAuth credentials, Python processes reaching rare identity-provider domains, browser-to-Python redirect sequences, package installation or version inspection, and unusual OAuth application activity.
Required logs
Microsoft Entra SigninLogs, AADServicePrincipalSignInLogs, and AuditLogs; Microsoft Defender for Endpoint DeviceNetworkEvents and DeviceProcessEvents on hosts running MCP services; and CloudAppEvents where Defender for Cloud Apps or Microsoft 365 audit telemetry captures OAuth application activity.
Hunt 1 — Successful service-principal sign-ins from new IP addresses
let Baseline = AADServicePrincipalSignInLogs
| where TimeGenerated between (ago(30d) .. ago(1d)) and tostring(ResultType) == "0"
| summarize by ServicePrincipalId, IPAddress;
AADServicePrincipalSignInLogs
| where TimeGenerated > ago(1d) and tostring(ResultType) == "0"
| join kind=leftanti Baseline on ServicePrincipalId, IPAddress
| project TimeGenerated, ServicePrincipalId, ServicePrincipalName, AppId, IPAddress, UserAgent, ResourceDisplayName, CorrelationId
| order by TimeGenerated desc
Hunt 2 — New service-principal IP followed by access to multiple resources
let Baseline = AADServicePrincipalSignInLogs
| where TimeGenerated between (ago(30d) .. ago(1d)) and tostring(ResultType) == "0"
| summarize by ServicePrincipalId, IPAddress;
AADServicePrincipalSignInLogs
| where TimeGenerated > ago(1d) and tostring(ResultType) == "0"
| join kind=leftanti Baseline on ServicePrincipalId, IPAddress
| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), Resources=dcount(ResourceDisplayName), ResourceNames=make_set(ResourceDisplayName,25), SignIns=count() by ServicePrincipalId, ServicePrincipalName, AppId, IPAddress, UserAgent
| where Resources >= 3 or SignIns >= 10
| order by Resources desc
Hunt 3 — Rare noninteractive application and IP combinations
let Baseline = AADNonInteractiveUserSignInLogs
| where TimeGenerated between (ago(30d) .. ago(1d)) and tostring(ResultType) == "0"
| summarize by UserPrincipalName, AppId, IPAddress;
AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(1d) and tostring(ResultType) == "0"
| join kind=leftanti Baseline on UserPrincipalName, AppId, IPAddress
| project TimeGenerated, UserPrincipalName, AppId, AppDisplayName, IPAddress, UserAgent, ResourceDisplayName, CorrelationId, ConditionalAccessStatus
| order by TimeGenerated desc
Hunt 4 — OAuth application credential, permission, or consent changes
AuditLogs
| where TimeGenerated > ago(30d)
| where OperationName has_any ("Add service principal credentials","Update application","Add app role assignment","Consent to application","Add delegated permission grant","Update service principal")
| mv-expand TargetResources
| extend TargetName=tostring(TargetResources.displayName), TargetId=tostring(TargetResources.id), ModifiedProperties=tostring(TargetResources.modifiedProperties)
| project TimeGenerated, OperationName, Result, InitiatedBy, TargetName, TargetId, ModifiedProperties, CorrelationId
| order by TimeGenerated desc
Hunt 5 — Python processes contacting rare external destinations
let Baseline = DeviceNetworkEvents
| where TimeGenerated between (ago(30d) .. ago(1d))
| where InitiatingProcessFileName in~ ("python.exe","python3.exe","python","uv.exe")
| summarize by DeviceId, RemoteUrl;
DeviceNetworkEvents
| where TimeGenerated > ago(1d) and ActionType == "ConnectionSuccess"
| where InitiatingProcessFileName in~ ("python.exe","python3.exe","python","uv.exe") and isnotempty(RemoteUrl)
| join kind=leftanti Baseline on DeviceId, RemoteUrl
| project TimeGenerated, DeviceName, DeviceId, InitiatingProcessAccountName, InitiatingProcessFileName, InitiatingProcessCommandLine, RemoteUrl, RemoteIP, RemotePort
| order by TimeGenerated desc
Hunt 6 — Python client contacting an identity provider and another new endpoint
let IdPDomains = dynamic(["login.microsoftonline.com","login.windows.net","accounts.google.com","oauth2.googleapis.com","auth0.com","okta.com"]);
let PythonNet = DeviceNetworkEvents
| where TimeGenerated > ago(7d) and ActionType == "ConnectionSuccess"
| where InitiatingProcessFileName in~ ("python.exe","python3.exe","python","uv.exe") and isnotempty(RemoteUrl)
| project TimeGenerated, DeviceId, DeviceName, ProcessId=InitiatingProcessId, RemoteUrl, RemoteIP, InitiatingProcessCommandLine;
let IdP = PythonNet | where RemoteUrl has_any (IdPDomains) | project DeviceId, ProcessId, IdPTime=TimeGenerated, IdPUrl=RemoteUrl;
let Other = PythonNet | where not(RemoteUrl has_any (IdPDomains)) | project DeviceId, DeviceName, ProcessId, OtherTime=TimeGenerated, OtherUrl=RemoteUrl, OtherIP=RemoteIP, InitiatingProcessCommandLine;
IdP
| join kind=inner Other on DeviceId, ProcessId
| where OtherTime between (IdPTime-5m .. IdPTime+5m)
| project IdPTime, OtherTime, DeviceName, IdPUrl, OtherUrl, OtherIP, InitiatingProcessCommandLine
| order by IdPTime desc
Hunt 7 — MCP Python package installation or version inspection commands
DeviceProcessEvents
| where TimeGenerated > ago(30d)
| where FileName in~ ("pip.exe","pip3.exe","python.exe","python3.exe","uv.exe","poetry.exe")
| where ProcessCommandLine has_any ("pip install mcp","pip show mcp","pip freeze","uv add mcp","poetry add mcp","modelcontextprotocol")
| project TimeGenerated, DeviceName, AccountName, FileName, ProcessCommandLine, InitiatingProcessFileName, InitiatingProcessCommandLine
| order by TimeGenerated desc
Hunt 8 — Cloud application activity from newly observed source IPs for an application
let Baseline = CloudAppEvents
| where TimeGenerated between (ago(30d) .. ago(1d))
| where isnotempty(ApplicationId) and isnotempty(IPAddress)
| summarize by ApplicationId, IPAddress;
CloudAppEvents
| where TimeGenerated > ago(1d) and isnotempty(ApplicationId) and isnotempty(IPAddress)
| join kind=leftanti Baseline on ApplicationId, IPAddress
| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), Actions=count(), ActionTypes=make_set(ActionType,25), Accounts=make_set(AccountId,25), Objects=dcount(ObjectId) by ApplicationId, Application, IPAddress
| where Actions >= 5 or Objects >= 3
| order by Actions desc
Detection Notes
- Hunts 1–4 are the highest-value identity layers. Successful attacker token use can appear fully valid because the attacker possesses legitimate client credentials and authorization material.
AADServicePrincipalSignInLogsandAADNonInteractiveUserSignInLogsrequire the corresponding Entra diagnostic exports and sufficient retention.- The SDK package name
mcpis generic and package-install commands are exposure evidence, not exploitation evidence. Confirm the installed version through asset or software-composition tooling. - Endpoint network telemetry cannot see token endpoints, client secrets, authorization codes, or PKCE values inside TLS. Hunt 6 is a heuristic for unusual discovery flows and can be noisy in development systems.
CloudAppEventscoverage depends on Defender for Cloud Apps and connected applications. Field population varies by source.- No campaign IOCs are available. Do not treat a rare sign-in or Python destination as proof of this vulnerability without confirming an affected SDK client and untrusted MCP connection.