Skip to content

Commit 285ef16

Browse files
neildcagedmantis
authored andcommitted
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. Fixes #64433 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 <[email protected]> Reviewed-by: Roland Shoemaker <[email protected]> Reviewed-on: https://go-review.googlesource.com/c/go/+/547335 Reviewed-by: Dmitri Shuralyov <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 16d3040 commit 285ef16

File tree

2 files changed

+87
-6
lines changed

2 files changed

+87
-6
lines changed

src/net/http/internal/chunked.go

Lines changed: 28 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,36 @@ 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+
cr.excess = max(cr.excess, 0)
80+
if cr.excess > 16*1024 {
81+
cr.err = errors.New("chunked encoding contains too much non-data")
82+
}
5683
if cr.n == 0 {
5784
cr.err = io.EOF
5885
}
@@ -140,11 +167,6 @@ func readChunkLine(b *bufio.Reader) ([]byte, error) {
140167
if len(p) >= maxLineLength {
141168
return nil, ErrLineTooLong
142169
}
143-
p = trimTrailingWhitespace(p)
144-
p, err = removeChunkExtension(p)
145-
if err != nil {
146-
return nil, err
147-
}
148170
return p, nil
149171
}
150172

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)