fix(mcp): pin cross-origin OAuth discovered-issuer requests per-request#6092
Merged
Conversation
bug-ops
enabled auto-merge (squash)
July 11, 2026 20:24
bug-ops
force-pushed
the
fix/6074-mcp-oauth-cross-origin-ssrf
branch
from
July 11, 2026 20:24
eaca1ae to
f4c43f8
Compare
validate_oauth_metadata_urls validated OAuth-discovered endpoints (token_endpoint, authorization_endpoint, jwks_uri, registration_endpoint) then discarded the result. #6069 pinned the MCP server's own host via a single resolve_to_addrs client, but per SEP-985 these endpoints may live on a different host, which AuthorizationManager resolved independently and unpinned — reopening the DNS-rebinding TOCTOU for cross-origin issuers. Add PinningOAuthHttpClient, an rmcp OAuthHttpClient implementation that resolves, SSRF-validates, and DNS-pins every OAuth HTTP request individually at execution time, regardless of target host. Wire it into connect_url_oauth via OAuthState::new_with_oauth_http_client. Keep validate_oauth_metadata_urls as a fail-fast pre-check only, since it is no longer the security boundary. Follow-up #6089 filed for a narrower, separate gap: Follow-policy requests (dynamic client registration) don't pin redirect-hop hosts.
bug-ops
force-pushed
the
fix/6074-mcp-oauth-cross-origin-ssrf
branch
from
July 11, 2026 20:26
f4c43f8 to
a016191
Compare
7 tasks
bug-ops
added a commit
that referenced
this pull request
Jul 11, 2026
PinningOAuthHttpClient (#6074, PR #6092) validated and DNS-pinned every OAuth HTTP request's host, but only disabled reqwest's built-in redirect-following for OAuthHttpRedirectPolicy::Stop requests. For Follow (dynamic client registration is rmcp's only current caller), a 3xx response pointing at a different host had that hop resolved by reqwest itself, independently and unpinned, reopening a redirect-scoped DNS-rebinding TOCTOU. build_client now disables redirects unconditionally, and execute() follows Follow-policy redirects manually via a bounded loop (MAX_OAUTH_REDIRECT_HOPS) that re-validates and re-pins each hop. Sensitive headers (Authorization, Cookie, Proxy-Authorization, WWW-Authenticate) are stripped when a redirect crosses origin (scheme, host, or port), matching reqwest's own cross-origin redirect behavior. Closes #6089
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
validate_oauth_metadata_urls(crates/zeph-mcp/src/oauth.rs) validated OAuth-discovered endpoints (token_endpoint,authorization_endpoint,jwks_uri,registration_endpoint) then discarded the DNS result. zeph-mcp OAuth flow: AuthorizationManager's internal HTTP client bypasses SSRF pre-check (sibling of #6057) #6069 pinned only the MCP server's own host via a singleresolve_to_addrs-hardened client; per SEP-985, discovered endpoints may legitimately live on a different host, soAuthorizationManagerfell back to independent, unpinned DNS resolution for them — reopening the DNS-rebinding TOCTOU for cross-origin OAuth issuers that zeph-mcp HTTP transport: SSRF guard has a DNS-rebinding TOCTOU gap (same defect class as #5380, fix not applied here) #6057/zeph-mcp OAuth flow: AuthorizationManager's internal HTTP client bypasses SSRF pre-check (sibling of #6057) #6069 closed for same-origin traffic.PinningOAuthHttpClient, anrmcp::transport::auth::OAuthHttpClientimplementation that resolves, SSRF-validates, and DNS-pins every outbound OAuth HTTP request individually at execution time, regardless of target host. Wired intoconnect_url_oauthviaOAuthState::new_with_oauth_http_client.validate_oauth_metadata_urlsis kept as a fail-fast pre-check only (rejects a misconfigured/malicious endpoint before opening a browser tab for the user) — it is no longer the security boundary, since every real OAuth HTTP request is now validated at request time.Follow-policy requests (dynamic client registration) don't pin redirect-hop hosts if the response itself is a further redirect to a third host. Confirmed this does not carry credentials in zeph's current call graph (token exchange/refresh/discovery all useStop).Closes #6074
Test plan
cargo +nightly fmt --check— cleancargo clippy --profile ci --workspace --all-targets --features "desktop,ide,server,chat,pdf,scheduler,testing" -- -D warnings— cleancargo nextest run --config-file .github/nextest.toml --workspace --features "desktop,ide,server,chat,pdf,scheduler" --lib --bins— 12901 passed, 0 failedRUSTFLAGS="-D warnings" RUSTDOCFLAGS="--deny rustdoc::broken_intra_doc_links" cargo doc --no-deps --workspace --features "desktop,ide,server,chat,pdf,scheduler"— cleanPinningOAuthHttpClientblocked on private/loopback cross-origin hosts, allowed on public IP literal, trusted-mode bypass verified[Unreleased]