Skip to content

Commit 041a477

Browse files
neildgopherbot
authored andcommitted
[release-branch.go1.22] net/textproto, mime/multipart: avoid unbounded read in MIME header
mime/multipart.Reader.ReadForm allows specifying the maximum amount of memory that will be consumed by the form. While this limit is correctly applied to the parsed form data structure, it was not being applied to individual header lines in a form. For example, when presented with a form containing a header line that never ends, ReadForm will continue to read the line until it runs out of memory. Limit the amount of data consumed when reading a header. Fixes CVE-2023-45290 Fixes #65850 For #65383 Change-Id: I7f9264d25752009e95f6b2c80e3d76aaf321d658 Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/2134435 Reviewed-by: Roland Shoemaker <[email protected]> Reviewed-by: Tatiana Bradley <[email protected]> Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/2174345 Reviewed-by: Carlos Amedee <[email protected]> Reviewed-on: https://go-review.googlesource.com/c/go/+/569237 Reviewed-by: Carlos Amedee <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Michael Knyszek <[email protected]>
1 parent 3a85520 commit 041a477

File tree

3 files changed

+87
-15
lines changed

3 files changed

+87
-15
lines changed

src/mime/multipart/formdata_test.go

+42
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,48 @@ func TestReadFormLimits(t *testing.T) {
452452
}
453453
}
454454

455+
func TestReadFormEndlessHeaderLine(t *testing.T) {
456+
for _, test := range []struct {
457+
name string
458+
prefix string
459+
}{{
460+
name: "name",
461+
prefix: "X-",
462+
}, {
463+
name: "value",
464+
prefix: "X-Header: ",
465+
}, {
466+
name: "continuation",
467+
prefix: "X-Header: foo\r\n ",
468+
}} {
469+
t.Run(test.name, func(t *testing.T) {
470+
const eol = "\r\n"
471+
s := `--boundary` + eol
472+
s += `Content-Disposition: form-data; name="a"` + eol
473+
s += `Content-Type: text/plain` + eol
474+
s += test.prefix
475+
fr := io.MultiReader(
476+
strings.NewReader(s),
477+
neverendingReader('X'),
478+
)
479+
r := NewReader(fr, "boundary")
480+
_, err := r.ReadForm(1 << 20)
481+
if err != ErrMessageTooLarge {
482+
t.Fatalf("ReadForm(1 << 20): %v, want ErrMessageTooLarge", err)
483+
}
484+
})
485+
}
486+
}
487+
488+
type neverendingReader byte
489+
490+
func (r neverendingReader) Read(p []byte) (n int, err error) {
491+
for i := range p {
492+
p[i] = byte(r)
493+
}
494+
return len(p), nil
495+
}
496+
455497
func BenchmarkReadForm(b *testing.B) {
456498
for _, test := range []struct {
457499
name string

src/net/textproto/reader.go

+33-15
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import (
1616
"sync"
1717
)
1818

19+
// TODO: This should be a distinguishable error (ErrMessageTooLarge)
20+
// to allow mime/multipart to detect it.
21+
var errMessageTooLarge = errors.New("message too large")
22+
1923
// A Reader implements convenience methods for reading requests
2024
// or responses from a text protocol network connection.
2125
type Reader struct {
@@ -36,27 +40,33 @@ func NewReader(r *bufio.Reader) *Reader {
3640
// ReadLine reads a single line from r,
3741
// eliding the final \n or \r\n from the returned string.
3842
func (r *Reader) ReadLine() (string, error) {
39-
line, err := r.readLineSlice()
43+
line, err := r.readLineSlice(-1)
4044
return string(line), err
4145
}
4246

4347
// ReadLineBytes is like [Reader.ReadLine] but returns a []byte instead of a string.
4448
func (r *Reader) ReadLineBytes() ([]byte, error) {
45-
line, err := r.readLineSlice()
49+
line, err := r.readLineSlice(-1)
4650
if line != nil {
4751
line = bytes.Clone(line)
4852
}
4953
return line, err
5054
}
5155

52-
func (r *Reader) readLineSlice() ([]byte, error) {
56+
// readLineSlice reads a single line from r,
57+
// up to lim bytes long (or unlimited if lim is less than 0),
58+
// eliding the final \r or \r\n from the returned string.
59+
func (r *Reader) readLineSlice(lim int64) ([]byte, error) {
5360
r.closeDot()
5461
var line []byte
5562
for {
5663
l, more, err := r.R.ReadLine()
5764
if err != nil {
5865
return nil, err
5966
}
67+
if lim >= 0 && int64(len(line))+int64(len(l)) > lim {
68+
return nil, errMessageTooLarge
69+
}
6070
// Avoid the copy if the first call produced a full line.
6171
if line == nil && !more {
6272
return l, nil
@@ -88,7 +98,7 @@ func (r *Reader) readLineSlice() ([]byte, error) {
8898
//
8999
// Empty lines are never continued.
90100
func (r *Reader) ReadContinuedLine() (string, error) {
91-
line, err := r.readContinuedLineSlice(noValidation)
101+
line, err := r.readContinuedLineSlice(-1, noValidation)
92102
return string(line), err
93103
}
94104

@@ -109,7 +119,7 @@ func trim(s []byte) []byte {
109119
// ReadContinuedLineBytes is like [Reader.ReadContinuedLine] but
110120
// returns a []byte instead of a string.
111121
func (r *Reader) ReadContinuedLineBytes() ([]byte, error) {
112-
line, err := r.readContinuedLineSlice(noValidation)
122+
line, err := r.readContinuedLineSlice(-1, noValidation)
113123
if line != nil {
114124
line = bytes.Clone(line)
115125
}
@@ -120,13 +130,14 @@ func (r *Reader) ReadContinuedLineBytes() ([]byte, error) {
120130
// returning a byte slice with all lines. The validateFirstLine function
121131
// is run on the first read line, and if it returns an error then this
122132
// error is returned from readContinuedLineSlice.
123-
func (r *Reader) readContinuedLineSlice(validateFirstLine func([]byte) error) ([]byte, error) {
133+
// It reads up to lim bytes of data (or unlimited if lim is less than 0).
134+
func (r *Reader) readContinuedLineSlice(lim int64, validateFirstLine func([]byte) error) ([]byte, error) {
124135
if validateFirstLine == nil {
125136
return nil, fmt.Errorf("missing validateFirstLine func")
126137
}
127138

128139
// Read the first line.
129-
line, err := r.readLineSlice()
140+
line, err := r.readLineSlice(lim)
130141
if err != nil {
131142
return nil, err
132143
}
@@ -154,13 +165,21 @@ func (r *Reader) readContinuedLineSlice(validateFirstLine func([]byte) error) ([
154165
// copy the slice into buf.
155166
r.buf = append(r.buf[:0], trim(line)...)
156167

168+
if lim < 0 {
169+
lim = math.MaxInt64
170+
}
171+
lim -= int64(len(r.buf))
172+
157173
// Read continuation lines.
158174
for r.skipSpace() > 0 {
159-
line, err := r.readLineSlice()
175+
r.buf = append(r.buf, ' ')
176+
if int64(len(r.buf)) >= lim {
177+
return nil, errMessageTooLarge
178+
}
179+
line, err := r.readLineSlice(lim - int64(len(r.buf)))
160180
if err != nil {
161181
break
162182
}
163-
r.buf = append(r.buf, ' ')
164183
r.buf = append(r.buf, trim(line)...)
165184
}
166185
return r.buf, nil
@@ -507,15 +526,16 @@ func readMIMEHeader(r *Reader, maxMemory, maxHeaders int64) (MIMEHeader, error)
507526

508527
// The first line cannot start with a leading space.
509528
if buf, err := r.R.Peek(1); err == nil && (buf[0] == ' ' || buf[0] == '\t') {
510-
line, err := r.readLineSlice()
529+
const errorLimit = 80 // arbitrary limit on how much of the line we'll quote
530+
line, err := r.readLineSlice(errorLimit)
511531
if err != nil {
512532
return m, err
513533
}
514534
return m, ProtocolError("malformed MIME header initial line: " + string(line))
515535
}
516536

517537
for {
518-
kv, err := r.readContinuedLineSlice(mustHaveFieldNameColon)
538+
kv, err := r.readContinuedLineSlice(maxMemory, mustHaveFieldNameColon)
519539
if len(kv) == 0 {
520540
return m, err
521541
}
@@ -544,7 +564,7 @@ func readMIMEHeader(r *Reader, maxMemory, maxHeaders int64) (MIMEHeader, error)
544564

545565
maxHeaders--
546566
if maxHeaders < 0 {
547-
return nil, errors.New("message too large")
567+
return nil, errMessageTooLarge
548568
}
549569

550570
// Skip initial spaces in value.
@@ -557,9 +577,7 @@ func readMIMEHeader(r *Reader, maxMemory, maxHeaders int64) (MIMEHeader, error)
557577
}
558578
maxMemory -= int64(len(value))
559579
if maxMemory < 0 {
560-
// TODO: This should be a distinguishable error (ErrMessageTooLarge)
561-
// to allow mime/multipart to detect it.
562-
return m, errors.New("message too large")
580+
return m, errMessageTooLarge
563581
}
564582
if vv == nil && len(strs) > 0 {
565583
// More than likely this will be a single-element key.

src/net/textproto/reader_test.go

+12
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ func TestReadLine(t *testing.T) {
3636
}
3737
}
3838

39+
func TestReadLineLongLine(t *testing.T) {
40+
line := strings.Repeat("12345", 10000)
41+
r := reader(line + "\r\n")
42+
s, err := r.ReadLine()
43+
if err != nil {
44+
t.Fatalf("Line 1: %v", err)
45+
}
46+
if s != line {
47+
t.Fatalf("%v-byte line does not match expected %v-byte line", len(s), len(line))
48+
}
49+
}
50+
3951
func TestReadContinuedLine(t *testing.T) {
4052
r := reader("line1\nline\n 2\nline3\n")
4153
s, err := r.ReadContinuedLine()

0 commit comments

Comments
 (0)