Configuration & Environment
OAuth state value is generated with a non-cryptographic RNG, OR the state cookie is set in the GET `/authorize` handler before the user has approved consent — both defeat the CSRF protection that state is supposed to provide.
The OAuth `state` parameter prevents authorization-code interception and CSRF on the callback. It must be (1) generated with a CSPRNG so the attacker can't predict it, (2) stored server-side ONLY after consent has been explicitly approved (so an attacker can't bypass the consent screen by crafting a request that the IdP auto-approves), and (3) validated exactly at the callback. Setting the state cookie before consent renders the consent screen ineffective.
MCP-proxy OAuth flows are easy to get wrong because they involve THREE state-bearing actors: the MCP client, the MCP proxy server, and the upstream IdP. The state parameter exists between the proxy and the IdP. If the proxy issues state before consent, an attacker can replay the authorize URL and the state cookie + consent cookie auto-approve.
import random |
@app.get("/authorize") |
def authorize(redirect_uri: str) -> RedirectResponse: |
state = random.randint(1, 10**9) # Predictable. |
return RedirectResponse( |
f"https://accounts.google.com/o/oauth2/v2/auth?state={state}" |
) |
import secrets |
@app.get("/authorize") |
def authorize(client_id: str, redirect_uri: str) -> HTMLResponse: |
return HTMLResponse("<form action='/consent' method='POST'>Approve?</form>") |
@app.post("/consent") |
def consent(response: Response, redirect_uri: str) -> RedirectResponse: |
state = secrets.token_urlsafe(32) |
response.set_cookie("__Host-state", state, secure=True, httponly=True, samesite="lax") |
return RedirectResponse( |
f"https://accounts.google.com/o/oauth2/v2/auth?state={state}" |
) |
Two sub-rules. (1) Weak RNG: `state` / `oauth_state` / `auth_state` / `csrf_state` (+ camelCase) assigned from `random.*` / `Math.random` / `time.time` / `uuid.uuid1` / `Date.now`. Generic `state` only fires when the file has OAuth-proxy context. (2) Cookie-before-consent: file has GET `/authorize` AND a state cookie/session sink, AND no POST `/consent` route exists in the same file.
See the full threat catalog for every documented detection.
MCPSafe runs this check — and every other rule in the catalog — on any MCP server you paste in.
Scan now