Skip to content

oauth2: stop cross-host redirects to prevent bearer token leakage#804

Open
evilgensec wants to merge 1 commit into
golang:masterfrom
evilgensec:fix/cross-host-redirect-token-leakage
Open

oauth2: stop cross-host redirects to prevent bearer token leakage#804
evilgensec wants to merge 1 commit into
golang:masterfrom
evilgensec:fix/cross-host-redirect-token-leakage

Conversation

@evilgensec
Copy link
Copy Markdown

@evilgensec evilgensec commented May 6, 2026

Transport.RoundTrip unconditionally injects the Authorization header on
every RoundTrip call, including the intermediate calls that http.Client
makes for each redirect hop. Go's http.Client strips the Authorization
header before building a cross-host redirect request, but the
oauth2.Transport re-adds the token immediately, forwarding it to every
host in the redirect chain.

Reproduction sketch (two test servers on different ports):

victim receives GET with Authorization: Bearer SECRET
redirector issues 302 -> victim.URL
client := oauth2.NewClient(ctx, StaticTokenSource(&Token{AccessToken: "SECRET"}))
client.Get(redirector.URL)
// token leaked to unrelated host

Any application using NewClient that follows cross-host redirects (a
webhook caller, an API proxy, or code that accepts user-provided URLs)
will forward its OAuth bearer token to the redirect destination. An
attacker who controls a redirect via an open redirect on the target API
or a user-controlled URL can steal the token.

Fix: add a CheckRedirect hook in NewClient that returns
http.ErrUseLastResponse when the redirect target host differs from the
first request host. This surfaces the 3xx response to the caller instead
of following it, preventing token injection into cross-host requests.

Same-host redirects continue to be followed as before. The hook composes
with any CheckRedirect already set on the context http.Client.

New tests:

  • TestNewClientCrossHostRedirectDoesNotLeakToken verifies the 302 is
    returned to the caller and the victim server does not receive the token.
  • TestNewClientSameHostRedirectIsFollowed verifies same-host redirects
    remain transparent for backward compatibility.

Fixes #805

oauth2.Transport.RoundTrip unconditionally injects the bearer token on
every call, including the calls made for each redirect hop that
http.Client forwards through the RoundTripper.  Go's http.Client does
strip the Authorization header before building the redirect request,
but the Transport re-adds it immediately, so the token is sent to every
host in the redirect chain regardless of whether that host is the
intended API endpoint.

Demonstration (before this fix):

  victim := httptest.NewServer(...)   // different host
  redirector := httptest.NewServer(func(w, r) {
      http.Redirect(w, r, victim.URL, http.StatusFound)
  })
  client := oauth2.NewClient(ctx, StaticTokenSource(&Token{AccessToken: "SECRET"}))
  client.Get(redirector.URL)
  // victim receives: Authorization: Bearer SECRET

Fix: add a CheckRedirect hook in NewClient that returns
http.ErrUseLastResponse when the redirect target is on a different host
than the first request.  Same-host redirects (e.g. HTTP → HTTPS on the
same domain) continue to be followed.  The hook composes with any
CheckRedirect already set on the context's *http.Client.

Tests:
- TestNewClientCrossHostRedirectDoesNotLeakToken: verifies the 302
  response is returned to the caller rather than followed, and that the
  victim server does not receive the token.
- TestNewClientSameHostRedirectIsFollowed: verifies same-host redirects
  remain transparent (backward compatibility).
@gopherbot
Copy link
Copy Markdown
Contributor

This PR (HEAD: 7a99e43) has been imported to Gerrit for code review.

Please visit Gerrit at https://go-review.googlesource.com/c/oauth2/+/774680.

Important tips:

  • Don't comment on this PR. All discussion takes place in Gerrit.
  • You need a Gmail or other Google account to log in to Gerrit.
  • To change your code in response to feedback:
    • Push a new commit to the branch used by your GitHub PR.
    • A new "patch set" will then appear in Gerrit.
    • Respond to each comment by marking as Done in Gerrit if implemented as suggested. You can alternatively write a reply.
    • Critical: you must click the blue Reply button near the top to publish your Gerrit responses.
    • Multiple commits in the PR will be squashed by GerritBot.
  • The title and description of the GitHub PR are used to construct the final commit message.
    • Edit these as needed via the GitHub web interface (not via Gerrit or git).
    • You should word wrap the PR description at ~76 characters unless you need longer lines (e.g., for tables or URLs).
  • See the Sending a change via GitHub and Reviews sections of the Contribution Guide as well as the FAQ for details.

@gopherbot
Copy link
Copy Markdown
Contributor

Message from Gopher Robot:

Patch Set 1:

(1 comment)


Please don’t reply on this GitHub thread. Visit golang.org/cl/774680.
After addressing review feedback, remember to publish your drafts!

@gopherbot
Copy link
Copy Markdown
Contributor

Message from Gopher Robot:

Patch Set 1:

Congratulations on opening your first change. Thank you for your contribution!

Next steps:
A maintainer will review your change and provide feedback. See
https://go.dev/doc/contribute#review for more info and tips to get your
patch through code review.

Most changes in the Go project go through a few rounds of revision. This can be
surprising to people new to the project. The careful, iterative review process
is our way of helping mentor contributors and ensuring that their contributions
have a lasting impact.


Please don’t reply on this GitHub thread. Visit golang.org/cl/774680.
After addressing review feedback, remember to publish your drafts!

@evilgensec evilgensec changed the title NewClient: stop cross-host redirects to prevent bearer token leakage oauth2: stop cross-host redirects to prevent bearer token leakage May 6, 2026
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.

oauth2: NewClient forwards bearer token on cross-host redirects

2 participants