net/http: persist header stripping across repeated redirects

When an HTTP redirect changes the host of a request, we drop
sensitive headers such as Authorization from the redirected request.
Fix a bug where a chain of redirects could result in sensitive
headers being sent to the wrong host:

  1. request to a.tld with Authorization header
  2. a.tld redirects to b.tld
  3. request to b.tld with no Authorization header
  4. b.tld redirects to b.tld
  3. request to b.tld with Authorization header restored

Thanks to Kyle Seely for reporting this issue.

For #70530
Fixes CVE-2024-45336

Change-Id: Ia58a2e10d33d6b0cc7220935e771450e5c34de72
Reviewed-on: https://go-internal-review.googlesource.com/c/go/+/1641
Reviewed-by: Roland Shoemaker <bracewell@google.com>
Reviewed-by: Tatiana Bradley <tatianabradley@google.com>
Commit-Queue: Roland Shoemaker <bracewell@google.com>
Reviewed-on: https://go-review.googlesource.com/c/go/+/643095
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
Auto-Submit: Michael Knyszek <mknyszek@google.com>
This commit is contained in:
Damien Neil 2024-11-22 12:34:11 -08:00 committed by Gopher Robot
parent 368a9ec998
commit 6783377295
3 changed files with 151 additions and 103 deletions

View file

@ -610,8 +610,9 @@ func (c *Client) do(req *Request) (retres *Response, reterr error) {
reqBodyClosed = false // have we closed the current req.Body? reqBodyClosed = false // have we closed the current req.Body?
// Redirect behavior: // Redirect behavior:
redirectMethod string redirectMethod string
includeBody = true includeBody = true
stripSensitiveHeaders = false
) )
uerr := func(err error) error { uerr := func(err error) error {
// the body may have been closed already by c.send() // the body may have been closed already by c.send()
@ -678,7 +679,12 @@ func (c *Client) do(req *Request) (retres *Response, reterr error) {
// in case the user set Referer on their first request. // in case the user set Referer on their first request.
// If they really want to override, they can do it in // If they really want to override, they can do it in
// their CheckRedirect func. // their CheckRedirect func.
copyHeaders(req) if !stripSensitiveHeaders && reqs[0].URL.Host != req.URL.Host {
if !shouldCopyHeaderOnRedirect(reqs[0].URL, req.URL) {
stripSensitiveHeaders = true
}
}
copyHeaders(req, stripSensitiveHeaders)
// Add the Referer header from the most recent // Add the Referer header from the most recent
// request URL to the new one, if it's not https->http: // request URL to the new one, if it's not https->http:
@ -746,7 +752,7 @@ func (c *Client) do(req *Request) (retres *Response, reterr error) {
// makeHeadersCopier makes a function that copies headers from the // makeHeadersCopier makes a function that copies headers from the
// initial Request, ireq. For every redirect, this function must be called // initial Request, ireq. For every redirect, this function must be called
// so that it can copy headers into the upcoming Request. // so that it can copy headers into the upcoming Request.
func (c *Client) makeHeadersCopier(ireq *Request) func(*Request) { func (c *Client) makeHeadersCopier(ireq *Request) func(req *Request, stripSensitiveHeaders bool) {
// The headers to copy are from the very initial request. // The headers to copy are from the very initial request.
// We use a closured callback to keep a reference to these original headers. // We use a closured callback to keep a reference to these original headers.
var ( var (
@ -760,8 +766,7 @@ func (c *Client) makeHeadersCopier(ireq *Request) func(*Request) {
} }
} }
preq := ireq // The previous request return func(req *Request, stripSensitiveHeaders bool) {
return func(req *Request) {
// If Jar is present and there was some initial cookies provided // If Jar is present and there was some initial cookies provided
// via the request header, then we may need to alter the initial // via the request header, then we may need to alter the initial
// cookies as we follow redirects since each redirect may end up // cookies as we follow redirects since each redirect may end up
@ -798,12 +803,15 @@ func (c *Client) makeHeadersCopier(ireq *Request) func(*Request) {
// Copy the initial request's Header values // Copy the initial request's Header values
// (at least the safe ones). // (at least the safe ones).
for k, vv := range ireqhdr { for k, vv := range ireqhdr {
if shouldCopyHeaderOnRedirect(k, preq.URL, req.URL) { sensitive := false
switch CanonicalHeaderKey(k) {
case "Authorization", "Www-Authenticate", "Cookie", "Cookie2":
sensitive = true
}
if !(sensitive && stripSensitiveHeaders) {
req.Header[k] = vv req.Header[k] = vv
} }
} }
preq = req // Update previous Request with the current request
} }
} }
@ -979,28 +987,23 @@ func (b *cancelTimerBody) Close() error {
return err return err
} }
func shouldCopyHeaderOnRedirect(headerKey string, initial, dest *url.URL) bool { func shouldCopyHeaderOnRedirect(initial, dest *url.URL) bool {
switch CanonicalHeaderKey(headerKey) { // Permit sending auth/cookie headers from "foo.com"
case "Authorization", "Www-Authenticate", "Cookie", "Cookie2": // to "sub.foo.com".
// Permit sending auth/cookie headers from "foo.com"
// to "sub.foo.com".
// Note that we don't send all cookies to subdomains // Note that we don't send all cookies to subdomains
// automatically. This function is only used for // automatically. This function is only used for
// Cookies set explicitly on the initial outgoing // Cookies set explicitly on the initial outgoing
// client request. Cookies automatically added via the // client request. Cookies automatically added via the
// CookieJar mechanism continue to follow each // CookieJar mechanism continue to follow each
// cookie's scope as set by Set-Cookie. But for // cookie's scope as set by Set-Cookie. But for
// outgoing requests with the Cookie header set // outgoing requests with the Cookie header set
// directly, we don't know their scope, so we assume // directly, we don't know their scope, so we assume
// it's for *.domain.com. // it's for *.domain.com.
ihost := idnaASCIIFromURL(initial) ihost := idnaASCIIFromURL(initial)
dhost := idnaASCIIFromURL(dest) dhost := idnaASCIIFromURL(dest)
return isDomainOrSubdomain(dhost, ihost) return isDomainOrSubdomain(dhost, ihost)
}
// All other headers are copied:
return true
} }
// isDomainOrSubdomain reports whether sub is a subdomain (or exact // isDomainOrSubdomain reports whether sub is a subdomain (or exact

View file

@ -1536,6 +1536,55 @@ func testClientCopyHeadersOnRedirect(t *testing.T, mode testMode) {
} }
} }
// Issue #70530: Once we strip a header on a redirect to a different host,
// the header should stay stripped across any further redirects.
func TestClientStripHeadersOnRepeatedRedirect(t *testing.T) {
run(t, testClientStripHeadersOnRepeatedRedirect)
}
func testClientStripHeadersOnRepeatedRedirect(t *testing.T, mode testMode) {
var proto string
ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
if r.Host+r.URL.Path != "a.example.com/" {
if h := r.Header.Get("Authorization"); h != "" {
t.Errorf("on request to %v%v, Authorization=%q, want no header", r.Host, r.URL.Path, h)
}
}
// Follow a chain of redirects from a to b and back to a.
// The Authorization header is stripped on the first redirect to b,
// and stays stripped even if we're sent back to a.
switch r.Host + r.URL.Path {
case "a.example.com/":
Redirect(w, r, proto+"://b.example.com/", StatusFound)
case "b.example.com/":
Redirect(w, r, proto+"://b.example.com/redirect", StatusFound)
case "b.example.com/redirect":
Redirect(w, r, proto+"://a.example.com/redirect", StatusFound)
case "a.example.com/redirect":
w.Header().Set("X-Done", "true")
default:
t.Errorf("unexpected request to %v", r.URL)
}
})).ts
proto, _, _ = strings.Cut(ts.URL, ":")
c := ts.Client()
c.Transport.(*Transport).Dial = func(_ string, _ string) (net.Conn, error) {
return net.Dial("tcp", ts.Listener.Addr().String())
}
req, _ := NewRequest("GET", proto+"://a.example.com/", nil)
req.Header.Add("Cookie", "foo=bar")
req.Header.Add("Authorization", "secretpassword")
res, err := c.Do(req)
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
if res.Header.Get("X-Done") != "true" {
t.Fatalf("response missing expected header: X-Done=true")
}
}
// Issue 22233: copy host when Client follows a relative redirect. // Issue 22233: copy host when Client follows a relative redirect.
func TestClientCopyHostOnRedirect(t *testing.T) { run(t, testClientCopyHostOnRedirect) } func TestClientCopyHostOnRedirect(t *testing.T) { run(t, testClientCopyHostOnRedirect) }
func testClientCopyHostOnRedirect(t *testing.T, mode testMode) { func testClientCopyHostOnRedirect(t *testing.T, mode testMode) {
@ -1702,43 +1751,39 @@ func testClientAltersCookiesOnRedirect(t *testing.T, mode testMode) {
// Part of Issue 4800 // Part of Issue 4800
func TestShouldCopyHeaderOnRedirect(t *testing.T) { func TestShouldCopyHeaderOnRedirect(t *testing.T) {
tests := []struct { tests := []struct {
header string
initialURL string initialURL string
destURL string destURL string
want bool want bool
}{ }{
{"User-Agent", "http://foo.com/", "http://bar.com/", true},
{"X-Foo", "http://foo.com/", "http://bar.com/", true},
// Sensitive headers: // Sensitive headers:
{"cookie", "http://foo.com/", "http://bar.com/", false}, {"http://foo.com/", "http://bar.com/", false},
{"cookie2", "http://foo.com/", "http://bar.com/", false}, {"http://foo.com/", "http://bar.com/", false},
{"authorization", "http://foo.com/", "http://bar.com/", false}, {"http://foo.com/", "http://bar.com/", false},
{"authorization", "http://foo.com/", "https://foo.com/", true}, {"http://foo.com/", "https://foo.com/", true},
{"authorization", "http://foo.com:1234/", "http://foo.com:4321/", true}, {"http://foo.com:1234/", "http://foo.com:4321/", true},
{"www-authenticate", "http://foo.com/", "http://bar.com/", false}, {"http://foo.com/", "http://bar.com/", false},
{"authorization", "http://foo.com/", "http://[::1%25.foo.com]/", false}, {"http://foo.com/", "http://[::1%25.foo.com]/", false},
// But subdomains should work: // But subdomains should work:
{"www-authenticate", "http://foo.com/", "http://foo.com/", true}, {"http://foo.com/", "http://foo.com/", true},
{"www-authenticate", "http://foo.com/", "http://sub.foo.com/", true}, {"http://foo.com/", "http://sub.foo.com/", true},
{"www-authenticate", "http://foo.com/", "http://notfoo.com/", false}, {"http://foo.com/", "http://notfoo.com/", false},
{"www-authenticate", "http://foo.com/", "https://foo.com/", true}, {"http://foo.com/", "https://foo.com/", true},
{"www-authenticate", "http://foo.com:80/", "http://foo.com/", true}, {"http://foo.com:80/", "http://foo.com/", true},
{"www-authenticate", "http://foo.com:80/", "http://sub.foo.com/", true}, {"http://foo.com:80/", "http://sub.foo.com/", true},
{"www-authenticate", "http://foo.com:443/", "https://foo.com/", true}, {"http://foo.com:443/", "https://foo.com/", true},
{"www-authenticate", "http://foo.com:443/", "https://sub.foo.com/", true}, {"http://foo.com:443/", "https://sub.foo.com/", true},
{"www-authenticate", "http://foo.com:1234/", "http://foo.com/", true}, {"http://foo.com:1234/", "http://foo.com/", true},
{"authorization", "http://foo.com/", "http://foo.com/", true}, {"http://foo.com/", "http://foo.com/", true},
{"authorization", "http://foo.com/", "http://sub.foo.com/", true}, {"http://foo.com/", "http://sub.foo.com/", true},
{"authorization", "http://foo.com/", "http://notfoo.com/", false}, {"http://foo.com/", "http://notfoo.com/", false},
{"authorization", "http://foo.com/", "https://foo.com/", true}, {"http://foo.com/", "https://foo.com/", true},
{"authorization", "http://foo.com:80/", "http://foo.com/", true}, {"http://foo.com:80/", "http://foo.com/", true},
{"authorization", "http://foo.com:80/", "http://sub.foo.com/", true}, {"http://foo.com:80/", "http://sub.foo.com/", true},
{"authorization", "http://foo.com:443/", "https://foo.com/", true}, {"http://foo.com:443/", "https://foo.com/", true},
{"authorization", "http://foo.com:443/", "https://sub.foo.com/", true}, {"http://foo.com:443/", "https://sub.foo.com/", true},
{"authorization", "http://foo.com:1234/", "http://foo.com/", true}, {"http://foo.com:1234/", "http://foo.com/", true},
} }
for i, tt := range tests { for i, tt := range tests {
u0, err := url.Parse(tt.initialURL) u0, err := url.Parse(tt.initialURL)
@ -1751,10 +1796,10 @@ func TestShouldCopyHeaderOnRedirect(t *testing.T) {
t.Errorf("%d. dest URL %q parse error: %v", i, tt.destURL, err) t.Errorf("%d. dest URL %q parse error: %v", i, tt.destURL, err)
continue continue
} }
got := Export_shouldCopyHeaderOnRedirect(tt.header, u0, u1) got := Export_shouldCopyHeaderOnRedirect(u0, u1)
if got != tt.want { if got != tt.want {
t.Errorf("%d. shouldCopyHeaderOnRedirect(%q, %q => %q) = %v; want %v", t.Errorf("%d. shouldCopyHeaderOnRedirect(%q => %q) = %v; want %v",
i, tt.header, tt.initialURL, tt.destURL, got, tt.want) i, tt.initialURL, tt.destURL, got, tt.want)
} }
} }
} }

View file

@ -10,56 +10,56 @@ import "strings"
// LocalhostCert is a PEM-encoded TLS cert with SAN IPs // LocalhostCert is a PEM-encoded TLS cert with SAN IPs
// "127.0.0.1" and "[::1]", expiring at Jan 29 16:00:00 2084 GMT. // "127.0.0.1" and "[::1]", expiring at Jan 29 16:00:00 2084 GMT.
// generated from src/crypto/tls: // generated from src/crypto/tls:
// go run generate_cert.go --rsa-bits 2048 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h // go run generate_cert.go --rsa-bits 2048 --host 127.0.0.1,::1,example.com,*.example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
var LocalhostCert = []byte(`-----BEGIN CERTIFICATE----- var LocalhostCert = []byte(`-----BEGIN CERTIFICATE-----
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
WkBKOclmOV2xlTVuPw== rF7r4gUnKeC7mYIH1zypY7laskopiLFAfe96Kg==
-----END CERTIFICATE-----`) -----END CERTIFICATE-----`)
// LocalhostKey is the private key for LocalhostCert. // LocalhostKey is the private key for LocalhostCert.
var LocalhostKey = []byte(testingKey(`-----BEGIN RSA TESTING KEY----- var LocalhostKey = []byte(testingKey(`-----BEGIN RSA TESTING KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDoZtrm0dXV0Aqi MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDFyXr1E4l3GM34
4Bpc7f95sNRTiu/AJSD8I1onY9PnEsPg3VVxvytsVJbYdcqr4w99V3AgpH/UNzMS wlmdsWtjHJCigAMKwnpUOS4zI1AiLH8eTXk2T+4XIFfUx775oSkaZjdEhjh9S8Qu
gAZ/8lZBNbsSDOVesJ3euVqMRfYPvd9pYl6QPRRpSDPm+2tNdn3QFAvta9EgJ3sW pP+yu8AexNfBVVK20xxjylwAWZdKqjfHgy5RMb+MJfdV+2PSvcQzkzwiZjWvMD+O
URnoU85w+W6aLI2bNSq3AaE771p3VbkGolpEjo9h+i42TBHo1rhPNKPkGupR8/QX pNMsmDTSsP4Oa4MAFypC+hfD9FXzDXJNGLkE+gcMUP8BZO39iAy+TWXZir/EjxVs
AOLMpInRdeaHyDwb2a3DE5I3dG7VAVzrVfJ6W6Q84YoFX+rpEE2SVM17SAjy6xQy xQimMGgZfFaxJ69DmLazWaT3/JnO7RiynW1OXMOo49rjKwWMGK11eLB/GPG2/mde
VjKgLvK2mk0xbtfa+h0B6VK7bmODHZqeP18NVm6HsBcXn7iclLgAC3SfWU1jucZK o4I/muF4o7SxYuTR960ynU5XklIkwAnDpzZkySVTZYyoASlGN0T+8d8i42D7IZpF
x1lqzw9tAgMBAAECggEABWzxS1Y2wckblnXY57Z+sl6YdmLV+gxj2r8Qib7g4ZIk GojTs1lFAgMBAAECggEAIYthUi1lFBDd5gG4Rzlu+BlBIn5JhcqkCqLEBiJIFfOr
lIlWR1OJNfw7kU4eryib4fc6nOh6O4AWZyYqAK6tqNQSS/eVG0LQTLTTEldHyVJL /4yuMRrvS3bNzqWt6xJ9MSAC4ZlN/VobRLnxL/QNymoiGYUKCT3Ww8nvPpPzR9OE
dvBe+MsUQOj4nTndZW+QvFzbcm2D8lY5n2nBSxU5ypVoKZ1EqQzytFcLZpTN7d89 sE68TUL9tJw/zZJcRMKwgvrGqSLimfq53MxxkE+kLdOc0v9C8YH8Re26mB5ZcWYa
EPj0qDyrV4NZlWAwL1AygCwnlwhMQjXEalVF1ylXwU3QzyZ/6MgvF6d3SSUlh+sq 7YFyZQpKsQYnsmu/05cMbpOQrQWhtmIqRoyn8mG/par2s3NzjtpSE9NINyz26uFc
XefuyigXw484cQQgbzopv6niMOmGP3of+yV4JQqUSb3IDmmT68XjGd2Dkxl4iPki k/3ovFJQIHkUmTS7KHD3BgY5vuCqP98HramYnOysJ0WoYgvSDNCWw3037s5CCwJT
6ZwXf3CCi+c+i/zVEcufgZ3SLf8D99kUGE7v7fZ6AQKBgQD1ZX3RAla9hIhxCf+O gCKuM+Ow6liFrj83RrdKBpm5QUGjfNpYP31o+QNP4QKBgQDSrUQ2XdgtAnibAV7u
3D+I1j2LMrdjAh0ZKKqwMR4JnHX3mjQI6LwqIctPWTU8wYFECSh9klEclSdCa64s 7kbxOxro0EhIKso0Y/6LbDQgcXgxLqltkmeqZgG8nC3Z793lhlSasz2snhzzooV5
uI/GNpcqPXejd0cAAdqHEEeG5sHMDt0oFSurL4lyud0GtZvwlzLuwEweuDtvT9cJ 5fTy1y8ikXqjhG0nNkInFyOhsI0auE28CFoDowaQd+5cmCatpN4Grqo5PNRXxm1w
Wfvl86uyO36IW8JdvUprYDctrQKBgQDycZ697qutBieZlGkHpnYWUAeImVA878sJ HktfPEgoP11NNCFHvvN5fEKbbQKBgQDwVlOaV20IvW3IPq7cXZyiyabouFF9eTRo
w44NuXHvMxBPz+lbJGAg8Cn8fcxNAPqHIraK+kx3po8cZGQywKHUWsxi23ozHoxo VJka1Uv+JtyvL2P0NKkjYHOdN8gRblWqxQtJoTNk020rVA4UP1heiXALy50gvj/p
+bGqeQb9U661TnfdDspIXia+xilZt3mm5BPzOUuRqlh4Y9SOBpSWRmEhyw76w4ZP hMcybPTLYSPOhAGx838KIcvGR5oskP1aUCmFbFQzGELxhJ9diVVjxUtbG2DuwPKd
OPxjWYAgwQKBgA/FehSYxeJgRjSdo+MWnK66tjHgDJE8bYpUZsP0JC4R9DL5oiaA tD9TLxT2OQKBgQCcdlHSjp+dzdgERmBa0ludjGfPv9/uuNizUBAbO6D690psPFtY
brd2fI6Y+SbyeNBallObt8LSgzdtnEAbjIH8uDJqyOmknNePRvAvR6mP4xyuR+Bv JQMYaemgSd1DngEOFVWADt4e9M5Lose+YCoqr+UxpxmNlyv5kzJOFcFAs/4XeglB
m+Lgp0DMWTw5J9CKpydZDItc49T/mJ5tPhdFVd+am0NAQnmr1MCZ6nHxAoGABS3Y PHKdgNW/NVKxMc6H54l9LPr+x05sYdGlEtqnP/3W5jhEvhJ5Vjc8YiyVgQKBgQCl
LkaC9FdFUUqSU8+Chkd/YbOkuyiENdkvl6t2e52jo5DVc1T7mLiIrRQi4SI8N9bN zwjyrGo+42GACy7cPYE5FeIfIDqoVByB9guC5bD98JXEDu/opQQjsgFRcBCJZhOY
/3oJWCT+uaSLX2ouCtNFunblzWHBrhxnZzTeqVq4SLc8aESAnbslKL4i8/+vYZlN M0UsURiB8ROaFu13rpQq9KrmmF0ZH+g8FSzQbzcbsTLg4VXCDXmR5esOKowFPypr
s8xtiNcSvL+lMsOBORSXzpj/4Ot8WwTkn1qyGgECgYBKNTypzAHeLE6yVadFp3nQ Sm667BfTAGP++D5ya7MLmCv6+RKQ5XD8uEQQAaV2kQKBgAD8qeJuWIXZT0VKkQrn
Ckq9yzvP/ib05rvgbvrne00YeOxqJ9gtTrzgh7koqJyX1L4NwdkEza4ilDWpucn0 nIhgtzGERF/6sZdQGW2LxTbUDWG74AfFkkEbeBfwEkCZXY/xmnYqYABhvlSex8jU
xiUZS4SoaJq6ZvcBYS62Yr1t8n09iG47YL8ibgtmH3L+svaotvpVxVK+d7BLevA/ supU6Eea21esIxIub2zv/Np0ojUb6rlqTPS4Ox1E27D787EJ3VOXpriSD10vyNnZ
ZboOWVe3icTy64BT3OQhmg== jel6uj2FOP9g54s+GzlSVg/T
-----END RSA TESTING KEY-----`)) -----END RSA TESTING KEY-----`))
func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") } func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") }