diff --git a/src/net/http/transport.go b/src/net/http/transport.go index b860eb95b04..572c16a6d83 100644 --- a/src/net/http/transport.go +++ b/src/net/http/transport.go @@ -325,6 +325,13 @@ func (t *Transport) readBufferSize() int { return 4 << 10 } +func (t *Transport) maxHeaderResponseSize() int64 { + if t.MaxResponseHeaderBytes > 0 { + return t.MaxResponseHeaderBytes + } + return 10 << 20 // conservative default; same as http2 +} + // Clone returns a deep copy of t's exported fields. func (t *Transport) Clone() *Transport { t.nextProtoOnce.Do(t.onceSetNextProtoDefaults) @@ -1871,7 +1878,7 @@ func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (pconn *pers } // Okay to use and discard buffered reader here, because // TLS server will not speak until spoken to. - br := bufio.NewReader(conn) + br := bufio.NewReader(&io.LimitedReader{R: conn, N: t.maxHeaderResponseSize()}) resp, err = ReadResponse(br, connectReq) }() select { @@ -2108,10 +2115,7 @@ type persistConn struct { } func (pc *persistConn) maxHeaderResponseSize() int64 { - if v := pc.t.MaxResponseHeaderBytes; v != 0 { - return v - } - return 10 << 20 // conservative default; same as http2 + return pc.t.maxHeaderResponseSize() } func (pc *persistConn) Read(p []byte) (n int, err error) { diff --git a/src/net/http/transport_test.go b/src/net/http/transport_test.go index 9762f058867..810f21f3a51 100644 --- a/src/net/http/transport_test.go +++ b/src/net/http/transport_test.go @@ -1550,6 +1550,72 @@ func TestTransportProxy(t *testing.T) { } } +// Issue 74633: verify that a client will not indefinitely read a response from +// a proxy server that writes an infinite byte of stream, rather than +// responding with 200 OK. +func TestProxyWithInfiniteHeader(t *testing.T) { + defer afterTest(t) + + ln := newLocalListener(t) + defer ln.Close() + cancelc := make(chan struct{}) + defer close(cancelc) + + // Simulate a malicious / misbehaving proxy that writes an unlimited number + // of bytes rather than responding with 200 OK. + go func() { + c, err := ln.Accept() + if err != nil { + t.Errorf("Accept: %v", err) + return + } + defer c.Close() + // Read the CONNECT request + br := bufio.NewReader(c) + cr, err := ReadRequest(br) + if err != nil { + t.Errorf("proxy server failed to read CONNECT request") + return + } + if cr.Method != "CONNECT" { + t.Errorf("unexpected method %q", cr.Method) + return + } + + // Keep writing bytes until the test exits. + for { + // runtime.Gosched() is needed here. Otherwise, this test might + // livelock in environments like WASM, where the one single thread + // we have could be hogged by the infinite loop of writing bytes. + runtime.Gosched() + select { + case <-cancelc: + return + default: + c.Write([]byte("infinite stream of bytes")) + } + } + }() + + c := &Client{ + Transport: &Transport{ + Proxy: func(*Request) (*url.URL, error) { + return url.Parse("http://" + ln.Addr().String()) + }, + // Limit MaxResponseHeaderBytes so the test returns quicker. + MaxResponseHeaderBytes: 1024, + }, + } + req, err := NewRequest("GET", "https://golang.fake.tld/", nil) + if err != nil { + t.Fatal(err) + } + _, err = c.Do(req) + if err == nil { + t.Errorf("unexpected Get success") + } +} + func TestOnProxyConnectResponse(t *testing.T) { var tcases = []struct {