Skip to content

fix(rest): prevent credential and default header leaks on cross-origin redirects - #2098

Open
Revanth14 wants to merge 2 commits into
apache:mainfrom
Revanth14:fix/rest-cross-origin-credentials
Open

Revanth14 wants to merge 2 commits into
apache:mainfrom
Revanth14:fix/rest-cross-origin-credentials

Conversation

@Revanth14

Copy link
Copy Markdown
Contributor

Summary

Fixes #2089.

Prevent the REST transport from reapplying catalog credentials and custom header defaults when following redirects to unconfigured origins.

  • Restrict managed authentication to the configured catalog origin.
  • Allow custom header defaults only for the catalog and configured OAuth endpoint origins.
  • Compare request URLs against configured origins so custom transports need not populate Response.Request.

Validation

  • go test ./catalog/rest
  • Relevant redirect and OAuth tests pass with -race.
  • Regression tests cover cross-origin redirects, synthetic redirects without Response.Request, same-origin authentication, and custom headers for a separate OAuth endpoint.

@zeroshade zeroshade left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This closes #2089 as scoped. sessionTransport.RoundTrip now sends the auth-manager header only to the catalog origin, and WithHeaders/header.* defaults only to the catalog and OAuth-endpoint origins. The two new leak tests fail on d66ab48 and pass at ab8b1b3, also when merged with current main (incl. #2060).

Smaller observations

  • Follow-up, pre-existing, not blocking (probably its own issue): this gates headers, but redirects are still followed. Verified at head:

    • a 307 from the token endpoint replays the client_credentials form, client_secret included, to the other origin, and the token that origin returns is then used against the catalog;
    • a cross-origin hop's response is decoded as the catalog's;
    • a hop can 307 back to any catalog path (e.g. DELETE …/tables/x?purgeRequested=true), which then goes out with the bearer.

    A CheckRedirect that refuses cross-origin hops on both the catalog client and the OAuth token client (oauthClient) would close all three. Nothing needs composing: the package always builds its own http.Client, and custom transports sit under sessionTransport. For comparison, Java fails closed on authenticated hops: httpclient5's DefaultRedirectStrategy.isRedirectAllowed won't follow a cross-authority redirect that carries Authorization.

  • builtinHeaders is cloned before the operator overrides, so cross-origin hops revert overridden built-in keys (inline).

  • The RoundTrip comment overstates net/http's redirect stripping (inline).

  • Nit: with a signer set, signingOrigin always equals catalogOrigin (still true after #2060). The signer gate could use toCatalog and the field could go, giving one origin check for auth, headers and signing as #2089 suggested.


This review was drafted by an AI-assisted tool and
confirmed by an Apache Iceberg maintainer. The maintainer
approving this PR has read the findings and signed off. If
something feels off, please reply on the PR and a maintainer
will follow up.

More on how Apache Iceberg handles maintainer review:
CONTRIBUTING.md.

Comment thread catalog/rest/rest.go Outdated
const emptyStringHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"

func (s *sessionTransport) RoundTrip(r *http.Request) (*http.Response, error) {
// net/http strips Authorization from cross-origin redirect hops, but this

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: net/http's redirect stripping is narrower than this reads. It only drops Authorization it copied from the first request, and only for a new hostname. A port or scheme change, or a hop to a subdomain, keeps it (shouldCopyHeaderOnRedirect). That's why this gate is needed even for the port-only redirect the new test uses.

Suggested change
// net/http strips Authorization from cross-origin redirect hops, but this
// net/http strips Authorization on redirect only for a new hostname (a
// port or scheme change, or a hop to a subdomain, keeps it), but this

Comment thread catalog/rest/rest.go Outdated
session.defaultHeaders.Set("Content-Type", "application/json")
session.defaultHeaders.Set("User-Agent", "GoIceberg/"+iceberg.Version())
session.defaultHeaders.Set(headerIcebergAccessDelegation, defaultAccessDelegation)
session.builtinHeaders = session.defaultHeaders.Clone()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: this snapshot is taken before the WithHeaders / header.* loops below, so a cross-origin hop gets the built-in default for any key the operator overrode. I checked this at ab8b1b3 with WithHeaders({"User-Agent": "corp-agent/1"}) and header.X-Iceberg-Access-Delegation=remote-signing. The same-origin request sends those values, but the hop sends GoIceberg/… and vended-credentials. Before this PR the hop got the overrides. It also doesn't match the field doc ("the subset of defaultHeaders"). Building it after the override loops, from just the built-in keys, keeps the two consistent:

session.builtinHeaders = http.Header{}
for _, k := range []string{"X-Client-Version", "Content-Type", "User-Agent", headerIcebergAccessDelegation} {
	if v := session.defaultHeaders.Values(k); len(v) > 0 {
		session.builtinHeaders[k] = v
	}
}

If reverting to the defaults is intended, the field doc should say "built-in defaults" instead.

This branch has not been deployed

No deployments
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.

catalog/rest: OAuth bearer token and header.* defaults are re-sent on cross-origin redirects

2 participants