oauth2: stop cross-host redirects to prevent bearer token leakage#804
oauth2: stop cross-host redirects to prevent bearer token leakage#804evilgensec wants to merge 1 commit into
Conversation
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).
|
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:
|
|
Message from Gopher Robot: Patch Set 1: (1 comment) Please don’t reply on this GitHub thread. Visit golang.org/cl/774680. |
|
Message from Gopher Robot: Patch Set 1: Congratulations on opening your first change. Thank you for your contribution! Next steps: Most changes in the Go project go through a few rounds of revision. This can be Please don’t reply on this GitHub thread. Visit golang.org/cl/774680. |
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:
returned to the caller and the victim server does not receive the token.
remain transparent for backward compatibility.
Fixes #805