MCP Python SDK OAuth Credential Redirection: Sentinel Hunt

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

Impacted Systems

  • Vendor/product: Official modelcontextprotocol/python-sdk, PyPI package mcp, OAuth client component mcp.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: ClientCredentialsOAuthProvider and PrivateKeyJWTOAuthProvider must set issuer= even after upgrading. Deprecated RFC7523OAuthClientProvider has 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.
  • AADServicePrincipalSignInLogs and AADNonInteractiveUserSignInLogs require the corresponding Entra diagnostic exports and sufficient retention.
  • The SDK package name mcp is 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.
  • CloudAppEvents coverage 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.