MCPSafe.io
RegistryThreatsMethodologyDocsPricingScanSign in
MCPSafe.io

Security checks for MCP servers — public packages and private repos, fast or deep.

Legal

Privacy PolicyCookie PolicyTerms of ServiceSecurity disclosure

Resources

State of MCP SecuritySupportSystem statusMade in Germany 🇩🇪

© 2026 MCPSafe. All rights reserved.

GDPR — Privacy Policy
← Threat Catalog

Configuration & Environment

OAuth proxy without per-client consent

HIGHCWE: CWE-441Rule: MCP-260

An MCP server proxying OAuth to a third-party IdP redirects users to the upstream `/authorize` endpoint without first checking per-client consent — an attacker who registers a malicious MCP client can ride the user's existing consent cookie and exfiltrate the auth code.

What it is

When an MCP proxy server uses a static client_id with a third-party authorization server and lets MCP clients register dynamically, the third-party IdP's consent cookie is shared across all MCP clients. A malicious client registered with `redirect_uri=attacker.example` can craft an authorize request that the IdP auto-approves (because the consent cookie is still valid from a prior legitimate flow) and redirects the auth code to attacker.example. This is the canonical Confused Deputy attack from the official MCP security best practices.

Why it matters for MCP

OAuth-proxy MCP servers are a common pattern ("connect to my Google Drive", "link your GitHub"). The proxy abstracts away IdP details, but introduces this trust-boundary bug because the IdP can't tell which MCP client made the request — only that the proxy did. The defense is the proxy implementing its OWN per-client consent screen, before forwarding to the upstream.

Vulnerable example

example.py
1
from fastmcp import FastMCP
2
from fastapi import FastAPI
3
from fastapi.responses import RedirectResponse
4
5
mcp = FastMCP("oauth-proxy")
6
app = FastAPI()
7
clients = {}
8
9
@app.post("/register")
10
def register(redirect_uri: str) -> dict:
11
    cid = secrets.token_urlsafe(16)
12
    clients[cid] = {"redirect_uri": redirect_uri}
13
    return {"client_id": cid}
14
15
@app.get("/authorize")
16
def authorize(client_id: str, state: str) -> RedirectResponse:
17
    # No per-client consent check — forwards immediately.
18
    return RedirectResponse(
19
        f"https://accounts.google.com/o/oauth2/v2/auth"
20
        f"?client_id={GOOGLE_CLIENT_ID}&state={state}"
21
    )

Secure example

example.py
1
@app.get("/authorize")
2
def authorize(req, client_id: str, state: str):
3
    user_id = req.session["user_id"]
4
    if not consent_required(user_id, client_id):
5
        return RedirectResponse(
6
            f"https://accounts.google.com/o/oauth2/v2/auth?client_id={GOOGLE_CLIENT_ID}&state={state}"
7
        )
8
    # Render an MCP-owned consent page that names this specific MCP client.
9
    return HTMLResponse("<form>Allow this client to access your Google Drive?</form>")

How MCPSafe detects this

MCPSafe matches files that (1) are MCP server context (`FastMCP` / `McpServer` / `@mcp.tool`); (2) contain a redirect to a known 3P OAuth authorize endpoint (Google `/o/oauth2/auth`, GitHub `/login/oauth/authorize`, Microsoft Entra `/oauth2/v2.0/authorize`, Slack `/oauth/v2/authorize`, Atlassian `/authorize`); (3) wrap that URL in a redirect-shaped sink (`RedirectResponse`, `res.redirect`, Location header); and (4) contain NO per-client consent identifier anywhere (`consent_required`, `check_consent`, `getClientConsent`, `approved_clients`, etc.).

See the full threat catalog for every documented detection.

Further reading

  • MCP Security Best Practices — Confused Deputy
  • CWE-441: Unintended Proxy or Intermediary

Scan an MCP server for this issue

MCPSafe runs this check — and every other rule in the catalog — on any MCP server you paste in.

Scan now