Skip to content

Commit 6446af9

Browse files
neildcagedmantis
authored andcommitted
[release-branch.go1.20] net/http: limit chunked data overhead
The chunked transfer encoding adds some overhead to the content transferred. When writing one byte per chunk, for example, there are five bytes of overhead per byte of data transferred: "1\r\nX\r\n" to send "X". Chunks may include "chunk extensions", which we skip over and do not use. For example: "1;chunk extension here\r\nX\r\n". A malicious sender can use chunk extensions to add about 4k of overhead per byte of data. (The maximum chunk header line size we will accept.) Track the amount of overhead read in chunked data, and produce an error if it seems excessive. Updates #64433 Fixes #64434 Fixes CVE-2023-39326 Change-Id: I40f8d70eb6f9575fb43f506eb19132ccedafcf39 Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/2076135 Reviewed-by: Tatiana Bradley <tatianabradley@google.com> Reviewed-by: Roland Shoemaker <bracewell@google.com> (cherry picked from commit 3473ae72ee66c60744665a24b2fde143e8964d4f) Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/2095407 Run-TryBot: Roland Shoemaker <bracewell@google.com> TryBot-Result: Security TryBots <security-trybots@go-security-trybots.iam.gserviceaccount.com> Reviewed-by: Damien Neil <dneil@google.com> Reviewed-on: https://go-review.googlesource.com/c/go/+/547355 Reviewed-by: Dmitri Shuralyov <dmitshur@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
1 parent 77397ff commit 6446af9

File tree

2 files changed

+89
-6
lines changed

2 files changed

+89
-6
lines changed

src/net/http/internal/chunked.go

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ type chunkedReader struct {
3939
n uint64 // unread bytes in chunk
4040
err error
4141
buf [2]byte
42-
checkEnd bool // whether need to check for \r\n chunk footer
42+
checkEnd bool // whether need to check for \r\n chunk footer
43+
excess int64 // "excessive" chunk overhead, for malicious sender detection
4344
}
4445

4546
func (cr *chunkedReader) beginChunk() {
@@ -49,10 +50,38 @@ func (cr *chunkedReader) beginChunk() {
4950
if cr.err != nil {
5051
return
5152
}
53+
cr.excess += int64(len(line)) + 2 // header, plus \r\n after the chunk data
54+
line = trimTrailingWhitespace(line)
55+
line, cr.err = removeChunkExtension(line)
56+
if cr.err != nil {
57+
return
58+
}
5259
cr.n, cr.err = parseHexUint(line)
5360
if cr.err != nil {
5461
return
5562
}
63+
// A sender who sends one byte per chunk will send 5 bytes of overhead
64+
// for every byte of data. ("1\r\nX\r\n" to send "X".)
65+
// We want to allow this, since streaming a byte at a time can be legitimate.
66+
//
67+
// A sender can use chunk extensions to add arbitrary amounts of additional
68+
// data per byte read. ("1;very long extension\r\nX\r\n" to send "X".)
69+
// We don't want to disallow extensions (although we discard them),
70+
// but we also don't want to allow a sender to reduce the signal/noise ratio
71+
// arbitrarily.
72+
//
73+
// We track the amount of excess overhead read,
74+
// and produce an error if it grows too large.
75+
//
76+
// Currently, we say that we're willing to accept 16 bytes of overhead per chunk,
77+
// plus twice the amount of real data in the chunk.
78+
cr.excess -= 16 + (2 * int64(cr.n))
79+
if cr.excess < 0 {
80+
cr.excess = 0
81+
}
82+
if cr.excess > 16*1024 {
83+
cr.err = errors.New("chunked encoding contains too much non-data")
84+
}
5685
if cr.n == 0 {
5786
cr.err = io.EOF
5887
}
@@ -140,11 +169,6 @@ func readChunkLine(b *bufio.Reader) ([]byte, error) {
140169
if len(p) >= maxLineLength {
141170
return nil, ErrLineTooLong
142171
}
143-
p = trimTrailingWhitespace(p)
144-
p, err = removeChunkExtension(p)
145-
if err != nil {
146-
return nil, err
147-
}
148172
return p, nil
149173
}
150174

src/net/http/internal/chunked_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,62 @@ func TestChunkEndReadError(t *testing.T) {
239239
t.Errorf("expected %v, got %v", readErr, err)
240240
}
241241
}
242+
243+
func TestChunkReaderTooMuchOverhead(t *testing.T) {
244+
// If the sender is sending 100x as many chunk header bytes as chunk data,
245+
// we should reject the stream at some point.
246+
chunk := []byte("1;")
247+
for i := 0; i < 100; i++ {
248+
chunk = append(chunk, 'a') // chunk extension
249+
}
250+
chunk = append(chunk, "\r\nX\r\n"...)
251+
const bodylen = 1 << 20
252+
r := NewChunkedReader(&funcReader{f: func(i int) ([]byte, error) {
253+
if i < bodylen {
254+
return chunk, nil
255+
}
256+
return []byte("0\r\n"), nil
257+
}})
258+
_, err := io.ReadAll(r)
259+
if err == nil {
260+
t.Fatalf("successfully read body with excessive overhead; want error")
261+
}
262+
}
263+
264+
func TestChunkReaderByteAtATime(t *testing.T) {
265+
// Sending one byte per chunk should not trip the excess-overhead detection.
266+
const bodylen = 1 << 20
267+
r := NewChunkedReader(&funcReader{f: func(i int) ([]byte, error) {
268+
if i < bodylen {
269+
return []byte("1\r\nX\r\n"), nil
270+
}
271+
return []byte("0\r\n"), nil
272+
}})
273+
got, err := io.ReadAll(r)
274+
if err != nil {
275+
t.Errorf("unexpected error: %v", err)
276+
}
277+
if len(got) != bodylen {
278+
t.Errorf("read %v bytes, want %v", len(got), bodylen)
279+
}
280+
}
281+
282+
type funcReader struct {
283+
f func(iteration int) ([]byte, error)
284+
i int
285+
b []byte
286+
err error
287+
}
288+
289+
func (r *funcReader) Read(p []byte) (n int, err error) {
290+
if len(r.b) == 0 && r.err == nil {
291+
r.b, r.err = r.f(r.i)
292+
r.i++
293+
}
294+
n = copy(p, r.b)
295+
r.b = r.b[n:]
296+
if len(r.b) > 0 {
297+
return n, nil
298+
}
299+
return n, r.err
300+
}

0 commit comments

Comments
 (0)