Repository navigation
Conversation
664efc6 to
0c84a87
Compare
…lient auth.Transport re-resolves and re-applies the configured credential on every request it handles, below the layer where net/http strips Authorization on a cross-host redirect. httpClient installs the transport but never sets CheckRedirect, so a redirect from the configured MCP endpoint carries the credential to whatever host the Location header names — contradicting the transport requirement documented in auth/gcp/provider.go and already implemented by the ADC-backed client in auth/gcp/client.go. Pin redirects (http.ErrUseLastResponse) in httpClient, at the step that wraps every streamable/SSE transport copy, so the guard covers both Config.Auth and user-supplied credential-bearing HTTPClients; an operator-provided CheckRedirect still wins. Includes a regression test with a cross-host control proving the leak is attributable to the transport layer and not to net/http itself. Closes: google#1965
0c84a87 to
be55665
Compare
|
Thanks for the report and the regression test. The leak is real, as @indurireddy-TF confirmed. We're closing this PR in favor of picking up the fix from the MCP Go SDK, for two reasons. Refusing every redirect is stricter than needed. It also breaks same-origin redirects that work today, most commonly the go-sdk has already fixed this. modelcontextprotocol/go-sdk#1440 (for modelcontextprotocol/go-sdk#1429), merged on Oct 23, makes go-sdk leaves the decision to any We'll bump go-sdk once a release containing #1440 ships and add a regression test along the lines of yours. That work, and one open question about |

Linked issue
Describe the change
Problem:
tool/mcptoolset'shttpClientinstallsauth.TransportwhenConfig.Authis set, but neversets
CheckRedirect.auth.Transport.RoundTripresolves and applies the credential on everyrequest it handles — below the layer where
net/httpstripsAuthorizationon a cross-hostredirect — so a redirect from the configured MCP endpoint carries the freshly-applied credential
to whatever host the
Locationheader names. This contradicts the transport requirement therepo itself documents in
auth/gcp/provider.go("The transport must not follow a cross-hostredirect… Set CheckRedirect on the http.Client that carries the transport") and already
implements for the ADC-backed client in
auth/gcp/client.go(http.ErrUseLastResponse).Security-relevant when the configured server is honest but the redirect is not under its
control: edge/CDN rewrite rules, header injection that sets
Location, domain lapse afterconfiguration, and
https → httpdowngrades (net/http follows them; the bearer token transitsin cleartext). The failure is silent — no error, no log, no operator signal.
Solution:
Pin redirects in
httpClient(http.ErrUseLastResponse) at the step that wraps everystreamable/SSE transport copy. Placing the guard there — rather than only on the
Config.Authpath — also covers user-supplied credential-bearing
HTTPClients (e.g. the documentedoauth2.NewClientwiring inexamples/mcp/main.go), whoseoauth2.Transportre-signs everyhop the same way. An operator-provided
CheckRedirectstill wins (nil-guard), so the changeonly removes a footgun, never a capability someone configured.
Alternatives considered: a same-origin-allowing guard (permits in-origin redirects like
trailing-slash normalization), and an allowed-host on
auth.Transportmatching adk-python's_RefreshableAsyncCredentialsbehavior (skip injection on a foreign host instead of refusingthe redirect). The
ErrUseLastResponsevariant is the minimal, repo-consistent one; happy toswitch if maintainers prefer either alternative.
Behavior change
Yes, for affected configurations: a toolset whose MCP endpoint answers a redirect now surfaces
the redirect response (e.g.
302 Found) as an error instead of silently following it. Serversthat do not redirect behave identically. Users who intentionally rely on redirect-following can
restore it by setting their own
CheckRedirecton a customHTTPClient.Testing Plan
redirect_regression_test.godrives the public API (mcptoolset.New+Tools) with a built-in control: serverA (localhost) 302s to serverB (127.0.0.1) — agenuinely cross-host hop, both loopback. A plain
http.DefaultClientcontrol proves net/httpstrips
Authorizationon this exact hop; the toolset path is then exercised on the same hop.The test fails without the fix (serverB receives the configured bearer token, which the
control shows net/http itself never forwards) and passes with it; it also asserts serverA
was contacted so a pass cannot be vacuous.
go test ./tool/mcptoolset/— full package suite green with the fix.go vet ./tool/mcptoolset/— clean.Fail-without-fix output:
Pass-with-fix: