diff options
| author | Nicholas S. Husin <nsh@golang.org> | 2026-03-16 21:20:11 -0400 |
|---|---|---|
| committer | Gopher Robot <gobot@golang.org> | 2026-03-23 14:10:05 -0700 |
| commit | 9d5d6af2d5bb4128f7e0759434a44839898c39a2 (patch) | |
| tree | 8b7a50c4034e00fa6e19e4e49f975f91ef85fb4d /src/net | |
| parent | f26befb380e7b0a5e8c083cacfcd141b4c2d413a (diff) | |
| download | go-9d5d6af2d5bb4128f7e0759434a44839898c39a2.tar.xz | |
net/http: make ResponseWriter.ReadFrom respect declared Content-Length
Unlike ResponseWriter.Write, ResponseWriter.ReadFrom does not currently
respect declared Content-Length header. As a result, it is possible for
a server handler to inadvertently write more bytes for their response
body than has been declared via Content-Length. These excess bytes are
written to the wire, and could potentially be misinterpreted by a client
as a separate response.
That said, this is not a concern security-wise. This edge case can only
be exercised by someone who has a relatively complete control of a
server handler—by which point, worse things can be done with less
effort.
Regardless, this is still a bug. Therefore, make sure that
ResponseWriter.ReadFrom respects declared Content-Length too for
consistency.
Fixes #78179
Change-Id: I469b064e43e49e467b907d23fc1ee879066569f0
Reviewed-on: https://go-review.googlesource.com/c/go/+/755701
Reviewed-by: Nicholas Husin <husin@google.com>
Auto-Submit: Nicholas Husin <nsh@golang.org>
Reviewed-by: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Diffstat (limited to 'src/net')
| -rw-r--r-- | src/net/http/serve_test.go | 59 | ||||
| -rw-r--r-- | src/net/http/server.go | 24 |
2 files changed, 83 insertions, 0 deletions
diff --git a/src/net/http/serve_test.go b/src/net/http/serve_test.go index 9ad3fdfd2d..b2cf43cd3f 100644 --- a/src/net/http/serve_test.go +++ b/src/net/http/serve_test.go @@ -1612,6 +1612,65 @@ func testHeadReaderFrom(t *testing.T, mode testMode) { } } +// Ensure ResponseWriter.ReadFrom respects declared Content-Length header. +// https://go.dev/issue/78179. +func TestReaderFromTooLong(t *testing.T) { run(t, testReaderFromTooLong, []testMode{http1Mode}) } +func testReaderFromTooLong(t *testing.T, mode testMode) { + contentLen := 600 // Longer than content-sniffing length. + tests := []struct { + name string + reader io.Reader + wantHandlerErr error + }{ + { + name: "reader of correct length", + reader: strings.NewReader(strings.Repeat("a", contentLen)), + }, + { + name: "wrapped reader of correct outer length", + reader: io.LimitReader(strings.NewReader(strings.Repeat("a", 2*contentLen)), int64(contentLen)), + }, + { + name: "wrapped reader of correct inner length", + reader: io.LimitReader(io.LimitReader(strings.NewReader(strings.Repeat("a", 2*contentLen)), int64(contentLen)), int64(2*contentLen)), + }, + { + name: "reader that is too long", + reader: strings.NewReader(strings.Repeat("a", 2*contentLen)), + wantHandlerErr: ErrContentLength, + }, + { + name: "wrapped reader that is too long", + reader: io.LimitReader(strings.NewReader(strings.Repeat("a", 2*contentLen)), int64(2*contentLen)), + wantHandlerErr: ErrContentLength, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cst := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) { + w.Header().Set("Content-Length", strconv.Itoa(contentLen)) + n, err := w.(io.ReaderFrom).ReadFrom(tc.reader) + if int(n) != contentLen || !errors.Is(err, tc.wantHandlerErr) { + t.Errorf("got %v, %v from w.ReadFrom; want %v, %v", n, err, contentLen, tc.wantHandlerErr) + } + })) + res, err := cst.c.Get(cst.ts.URL) + if err != nil { + t.Fatal(err) + } + defer res.Body.Close() + gotBody, err := io.ReadAll(res.Body) + if err != nil { + t.Fatal(err) + } + if len(gotBody) != contentLen { + t.Errorf("got unexpected body len=%v, want %v", len(gotBody), contentLen) + } + }) + } +} + func TestTLSHandshakeTimeout(t *testing.T) { run(t, testTLSHandshakeTimeout, []testMode{https1Mode, http2Mode}) } diff --git a/src/net/http/server.go b/src/net/http/server.go index 5502ce7ac4..90e8488959 100644 --- a/src/net/http/server.go +++ b/src/net/http/server.go @@ -616,6 +616,30 @@ func (w *response) ReadFrom(src io.Reader) (n int64, err error) { // Now that cw has been flushed, its chunking field is guaranteed initialized. if !w.cw.chunking && w.bodyAllowed() && w.req.Method != "HEAD" { + // When a content length is declared, but exceeded; any excess bytes + // from src should be ignored, and ErrContentLength should be returned. + // This mirrors the behavior of response.Write. + if w.contentLength != -1 { + defer func(originalReader io.Reader) { + if w.written != w.contentLength { + return + } + if n, _ := originalReader.Read([]byte{0}); err == nil && n != 0 { + err = ErrContentLength + } + }(src) + // src can be an io.LimitedReader already. To avoid unnecessary + // alloc and having to unnest readers repeatedly in net.sendFile, + // just adjust the existing LimitedReader N when this is the case. + if lr, ok := src.(*io.LimitedReader); ok { + if lenDiff := lr.N - (w.contentLength - w.written); lenDiff > 0 { + defer func() { lr.N += lenDiff }() + lr.N -= lenDiff + } + } else { + src = io.LimitReader(src, w.contentLength-w.written) + } + } n0, err := rf.ReadFrom(src) n += n0 w.written += n0 |
