Skip to content

Reject unsafe redirects in CIMD documents#470

Merged
alistair3149 merged 1 commit into
masterfrom
cimd-reject-cleartext-redirects
Jul 23, 2026
Merged

Reject unsafe redirects in CIMD documents#470
alistair3149 merged 1 commit into
masterfrom
cimd-reject-cleartext-redirects

Conversation

@alistair3149

Copy link
Copy Markdown
Member

Summary

A client that identifies by a vendor-hosted URL (a Client ID Metadata Document, CIMD) supplies its own redirect_uris in that document. MCP_OAUTH_ALLOWED_REDIRECTS does not gate CIMD clients — the curated CIMD host allowlist (MCP_OAUTH_CIMD_ALLOWED_HOSTS + shipped first-party hosts) is their trust anchor. validateCimdDocument checked only that redirect_uris was a non-empty array of strings, so an allowlisted host whose document listed a cleartext http redirect to a non-loopback host would have it honored — a target a network attacker on that path could intercept the authorization code at.

What changed

validateCimdDocument now requires each redirect_uri to be one of:

  • an https URL,
  • a loopback http URL (RFC 8252: 127.0.0.1 / localhost / [::1], any port — reusing isLoopbackHost), or
  • a custom app scheme (e.g. vscode://, com.example.app:/…).

Everything else is rejected: cleartext http to a non-loopback host, and any string that is not a parseable absolute URL (fail-closed — every legitimate redirect form parses). No shipped first-party client (vscode.dev, chatgpt.com, claude.ai, zed.dev) is affected. deployment.md documents that the CIMD host set grants redirect authority independent of the redirect allowlist.

Testing

  • Test-driven. The reject matrix pins the security-load-bearing cases: http://evil.example, uppercase scheme, userinfo, the deceptive http://127.0.0.1.evil.example (starts with a loopback literal but is not loopback), protocol-relative //evil.example, and unparseable/empty. Accept cases cover https (incl. nonstandard port), all three loopback hosts, and custom schemes (incl. the RFC 8252 private-use form).
  • An e2e test drives the real /authorize route: a bad-redirect CIMD doc yields a 400 auth-error page with no code minted.
  • Independently verified 14 adversarial URL forms against WHATWG URL; full gate (fmt:check, lint, tsc, tests) green.

Review

Reviewed by a code-review agent (verdict: merge). Folded in its recommendations: pinned the deceptive-subdomain regression test, and moved from "leave unparseable alone" (which produced a 500 for a trusted host's garbage redirect) to rejecting unparseable at validation (clean 400, fail-closed) — safe because every legitimate redirect form parses.

🤖 Generated with Claude Code

A client that identifies by a vendor-hosted URL (Client ID Metadata Document)
supplies its own redirect_uris in that document, and MCP_OAUTH_ALLOWED_REDIRECTS
does not gate CIMD clients — the curated CIMD host allowlist is their trust
anchor. validateCimdDocument checked only that redirect_uris was a non-empty
array of strings, so an allowlisted host whose document listed a cleartext http
redirect to a non-loopback host would have that redirect honoured, where a
network attacker on the path could intercept the authorization code.

Require each redirect_uri to be an https URL, a loopback http URL (RFC 8252:
127.0.0.1 / localhost / [::1], any port, reusing isLoopbackHost), or a custom app
scheme (e.g. vscode:// or com.example.app:/…). Reject everything else: cleartext
http to a non-loopback host, and any string that is not a parseable absolute URL
(fail closed — every legitimate redirect form parses). No first-party client is
affected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@alistair3149
alistair3149 force-pushed the cimd-reject-cleartext-redirects branch from 6bdc657 to 375a929 Compare July 23, 2026 05:29
@alistair3149
alistair3149 marked this pull request as ready for review July 23, 2026 05:30
@alistair3149
alistair3149 merged commit a661d3e into master Jul 23, 2026
1 check passed
@alistair3149
alistair3149 deleted the cimd-reject-cleartext-redirects branch July 23, 2026 05:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant