harden mcp oauth discovery against private hosts and validate browser opener urls - #2172
harden mcp oauth discovery against private hosts and validate browser opener urls#2172kevinjosethomas wants to merge 1 commit into
Conversation
…and validate browser opener URLs MCP OAuth discovery now refuses loopback, private, link-local and local-network hosts for the MCP endpoint, the resource_metadata pointer, authorization_servers, metadata URLs and the authorization, token and registration endpoints unless the server sets allowPrivateNetwork. The login dialog only launches the OS opener for http(s) URLs without credentials or control characters and shows anything else as text. Linear: ENG-5345
Prime Agent performance — completedPR
Python runtime
Sandbox cost: ~$0.1074 — no inference calls. Methodology and samplesMain resolved at 2026-09-09T23:53:50.137474+00:00. Harness
|
|
Nice, this closes the literal-IP and private-suffix cases cleanly. A scope question on the discovery hardening:
Is DNS-based SSRF in scope for this PR, or intentionally deferred to a follow-up? If in scope, the usual approach is to resolve the host, reject if any returned address is non-public, and pin that address for the connection (a custom undici |
Context
ENG-5345: MCP OAuth discovery accepted private, loopback and link-local HTTPS destinations for every URL the flow contacts or opens (MCP endpoint,
resource_metadatapointer,authorization_servers, metadata URLs, authorization/token/registration endpoints), andLoginDialogComponent.showAuthhanded any string toopen/xdg-open/rundll32.Root cause:
validatedHttpsUrlinpackages/ai/src/mcp/oauth.tsonly checked scheme, credentials and fragment, so a hostile PRM or metadata document could steer registration, the browser and the token POST at internal hosts. The dialog opener had no validation at all (defence in depth: today it only receives discovery-validated or constant URLs).Changes
packages/ai: newisPublicHost(src/mcp/host-policy.ts) rejects literal IPv4/IPv6 addresses that are loopback, unspecified, private (RFC 1918, CGNAT), link-local, ULA, multicast, documentation/benchmark ranges and IPv4-mapped/compatible/NAT64/6to4 forms, pluslocalhost,*.localhost,*.local,*.internal,*.home.arpaand single-label names.validatedHttpsUrlapplies it to every URL in the flow (endpoint,resource_metadatapointer, issuers, metadata endpoints, stored issuer on refresh);McpOAuthConfig.allowPrivateNetworkis the single escape hatch.packages/coding-agent:mcpServers.<name>.allowPrivateNetwork(alsomcp add --allow-private-network, OAuth servers only) is passed to the provider; documented indocs/mcp-integrations.md.packages/coding-agent:validateBrowserUrl(src/utils/browser-url.ts) allows only http(s) URLs without credentials or control characters, capped at 8 KiB.showAuthno longer launches the opener, emits an OSC 8 hyperlink or offers copy for anything else; it shows a sanitized copy with a message instead. The TUI hyperlink opener (packages/tui/src/tui.ts) already checks scheme and control characters and is unchanged.Validation
Local (worktree, macOS):
npm run checkclean;packages/ai:npx tsx ../../node_modules/vitest/dist/cli.js --run test/mcp-oauth.test.ts test/mcp-host-policy.test.ts(82 passed);packages/coding-agent: same runner ontest/login-dialog.test.ts test/browser-url.test.ts test/mcp-command.test.ts test/mcp-manager.test.ts test/oauth-selector.test.ts(89 passed).Prime Sandbox
node:24-bookworm, usertester,git archiveoforigin/main@ 427ea4c with the ENG-5345 validation fixtures (stubbed fetch / mockedexecFileHidden):https://127.0.0.1/mcpwith PRM naminghttps://10.0.0.7producedhttps://10.0.0.7/authorize?...after requests to 127.0.0.1 and 10.0.0.7;file:///etc/passwd,javascript:alert(1)and an ESC/OSC 52 URL were all forwarded toxdg-open.git applyof this branch): the same flow fails withMCP endpoint https://127.0.0.1 points at a private, loopback or local-network host; set allowPrivateNetwork: true ...with zero requests; onlyhttps://auth.example.org/authorize?x=1reachedxdg-open; the legitimate separate-AS case still yieldshttps://auth.example.org/authorize?...withresource=https://srv.test/mcp. The suites above pass there too (82 + 89).Linear: ENG-5345 — https://linear.app/primeintellect/issue/ENG-5345
Note
Reject private-network hosts in MCP OAuth discovery and validate browser opener URLs
isPublicHost/isPublicIPv4/isPublicIPv6classifiers in host-policy.ts that reject loopback, private, link-local, local-suffix, single-label, and reserved addresses for both IPv4 and IPv6.UrlPolicythrough every OAuth discovery, registration, token-exchange, and refresh path in oauth.ts so non-public destinations are blocked by default; providers can opt in viaallowPrivateNetwork.--allow-private-networktomcp addin mcp-command.ts, stored per HTTP OAuth server and rejected for stdio/bearer-token configs.allowPrivateNetworkis set; checkMcpServerConfig.allowPrivateNetworkin settings-manager.ts andcreateMcpOAuthProviderpolicy threading in oauth.ts.Macroscope summarized 3aa2113.