Problem
sessionTransport.RoundTrip (catalog/rest/rest.go:251-338 on main @ dd935d8) adds session credentials to every request it sends: the header.* / WithHeaders defaults (rest.go:257-268), the authManager header, typically Authorization: Bearer <catalog token> (rest.go:276-293), and the SigV4 signature (rest.go:295). The catalog http.Client is built with no CheckRedirect (rest.go:1074), so the default redirect policy applies and each redirect hop goes back through RoundTrip. If the catalog responds with a 3xx to a different host, that host receives the catalog bearer token and any header.* values.
Go's stdlib protection does not apply here. Client.do snapshots the headers of the initial request before the first send (makeHeadersCopier, net/http/client.go:772-776). On each redirect it copies that snapshot and drops Authorization, Www-Authenticate, Cookie, Cookie2, Proxy-Authorization and Proxy-Authenticate when the destination hostname is neither the initial hostname nor a subdomain of it (shouldCopyHeaderOnRedirect, client.go:1017-1039, applied at client.go:700-705 and 822-840). Our credentials are not in that snapshot because the transport adds them per hop inside RoundTrip, after the client has done its stripping, so the stdlib check never sees them. Custom header.* values are never treated as sensitive by stdlib, so they would be forwarded either way.
Reproduction
I ran a throwaway internal test against main. Catalog at http://127.0.0.1:<p1>, /v1/redirect returns 307 to http://localhost:<p2>/landing (different hostname). Catalog built with WithOAuthToken("SECRET-CATALOG-TOKEN") and WithAdditionalProps({"header.X-Api-Key": "SECRET-API-KEY"}), then cat.cl.Do(GET /v1/redirect).
- Control, plain
http.Client with Authorization set on the request before Do: the redirect target received Authorization="" (stripped by stdlib).
- Catalog client: the redirect target received
Authorization="Bearer SECRET-CATALOG-TOKEN" and X-Api-Key="SECRET-API-KEY".
Expected: neither value reaches a host other than the configured catalog origin.
Other implementations
- pyiceberg uses
requests, whose Session.rebuild_auth drops Authorization when a redirect changes host.
Proposed fix
In RoundTrip, apply the auth-manager header and the header.* defaults only when the request's scheme, host and effective port match the configured catalog origin, the same check #1999 adds for SigV4 (signingOrigin / sameOrigin). Reuse those helpers once #1999 lands. Add a two-host regression test like the one above that asserts the target receives neither header. Alternatively, a CheckRedirect that refuses cross-origin redirects would close it more bluntly.
Related
Problem
sessionTransport.RoundTrip(catalog/rest/rest.go:251-338 on main @ dd935d8) adds session credentials to every request it sends: theheader.*/WithHeadersdefaults (rest.go:257-268), theauthManagerheader, typicallyAuthorization: Bearer <catalog token>(rest.go:276-293), and the SigV4 signature (rest.go:295). The cataloghttp.Clientis built with noCheckRedirect(rest.go:1074), so the default redirect policy applies and each redirect hop goes back throughRoundTrip. If the catalog responds with a 3xx to a different host, that host receives the catalog bearer token and anyheader.*values.Go's stdlib protection does not apply here.
Client.dosnapshots the headers of the initial request before the first send (makeHeadersCopier, net/http/client.go:772-776). On each redirect it copies that snapshot and dropsAuthorization,Www-Authenticate,Cookie,Cookie2,Proxy-AuthorizationandProxy-Authenticatewhen the destination hostname is neither the initial hostname nor a subdomain of it (shouldCopyHeaderOnRedirect, client.go:1017-1039, applied at client.go:700-705 and 822-840). Our credentials are not in that snapshot because the transport adds them per hop insideRoundTrip, after the client has done its stripping, so the stdlib check never sees them. Customheader.*values are never treated as sensitive by stdlib, so they would be forwarded either way.Reproduction
I ran a throwaway internal test against main. Catalog at
http://127.0.0.1:<p1>,/v1/redirectreturns 307 tohttp://localhost:<p2>/landing(different hostname). Catalog built withWithOAuthToken("SECRET-CATALOG-TOKEN")andWithAdditionalProps({"header.X-Api-Key": "SECRET-API-KEY"}), thencat.cl.Do(GET /v1/redirect).http.ClientwithAuthorizationset on the request beforeDo: the redirect target receivedAuthorization=""(stripped by stdlib).Authorization="Bearer SECRET-CATALOG-TOKEN"andX-Api-Key="SECRET-API-KEY".Expected: neither value reaches a host other than the configured catalog origin.
Other implementations
requests, whoseSession.rebuild_authdropsAuthorizationwhen a redirect changes host.Proposed fix
In
RoundTrip, apply the auth-manager header and theheader.*defaults only when the request's scheme, host and effective port match the configured catalog origin, the same check #1999 adds for SigV4 (signingOrigin/sameOrigin). Reuse those helpers once #1999 lands. Add a two-host regression test like the one above that asserts the target receives neither header. Alternatively, aCheckRedirectthat refuses cross-origin redirects would close it more bluntly.Related
RequestSigner. The origin check should stay in coreRoundTripso it covers both the signer and the auth/header path.