Skip to content

Commit 7917b5f

Browse files
neildgopherbot
authored andcommitted
[release-branch.go1.19] mime/multipart: limit parsed mime message sizes
The parsed forms of MIME headers and multipart forms can consume substantially more memory than the size of the input data. A malicious input containing a very large number of headers or form parts can cause excessively large memory allocations. Set limits on the size of MIME data: Reader.NextPart and Reader.NextRawPart limit the the number of headers in a part to 10000. Reader.ReadForm limits the total number of headers in all FileHeaders to 10000. Both of these limits may be set with with GODEBUG=multipartmaxheaders=<values>. Reader.ReadForm limits the number of parts in a form to 1000. This limit may be set with GODEBUG=multipartmaxparts=<value>. Thanks for Jakob Ackermann (@das7pad) for reporting this issue. For CVE-2023-24536 For #59153 For #59269 Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1802455 Run-TryBot: Damien Neil <[email protected]> Reviewed-by: Roland Shoemaker <[email protected]> Reviewed-by: Julie Qiu <[email protected]> Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1801087 Reviewed-by: Damien Neil <[email protected]> Run-TryBot: Roland Shoemaker <[email protected]> Change-Id: If134890d75f0d95c681d67234daf191ba08e6424 Reviewed-on: https://go-review.googlesource.com/c/go/+/481985 Run-TryBot: Michael Knyszek <[email protected]> Auto-Submit: Michael Knyszek <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Matthew Dempsky <[email protected]>
1 parent 7a359a6 commit 7917b5f

File tree

5 files changed

+115
-17
lines changed

5 files changed

+115
-17
lines changed

src/mime/multipart/formdata.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"math"
1313
"net/textproto"
1414
"os"
15+
"strconv"
1516
)
1617

1718
// ErrMessageTooLarge is returned by ReadForm if the message form
@@ -41,6 +42,15 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) {
4142
numDiskFiles := 0
4243
multipartFiles := godebug.Get("multipartfiles")
4344
combineFiles := multipartFiles != "distinct"
45+
maxParts := 1000
46+
multipartMaxParts := godebug.Get("multipartmaxparts")
47+
if multipartMaxParts != "" {
48+
if v, err := strconv.Atoi(multipartMaxParts); err == nil && v >= 0 {
49+
maxParts = v
50+
}
51+
}
52+
maxHeaders := maxMIMEHeaders()
53+
4454
defer func() {
4555
if file != nil {
4656
if cerr := file.Close(); err == nil {
@@ -86,13 +96,17 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) {
8696
}
8797
var copyBuf []byte
8898
for {
89-
p, err := r.nextPart(false, maxMemoryBytes)
99+
p, err := r.nextPart(false, maxMemoryBytes, maxHeaders)
90100
if err == io.EOF {
91101
break
92102
}
93103
if err != nil {
94104
return nil, err
95105
}
106+
if maxParts <= 0 {
107+
return nil, ErrMessageTooLarge
108+
}
109+
maxParts--
96110

97111
name := p.FormName()
98112
if name == "" {
@@ -136,6 +150,9 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) {
136150
if maxMemoryBytes < 0 {
137151
return nil, ErrMessageTooLarge
138152
}
153+
for _, v := range p.Header {
154+
maxHeaders -= int64(len(v))
155+
}
139156
fh := &FileHeader{
140157
Filename: filename,
141158
Header: p.Header,

src/mime/multipart/formdata_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,67 @@ func testReadFormManyFiles(t *testing.T, distinct bool) {
360360
}
361361
}
362362

363+
func TestReadFormLimits(t *testing.T) {
364+
for _, test := range []struct {
365+
values int
366+
files int
367+
extraKeysPerFile int
368+
wantErr error
369+
godebug string
370+
}{
371+
{values: 1000},
372+
{values: 1001, wantErr: ErrMessageTooLarge},
373+
{values: 500, files: 500},
374+
{values: 501, files: 500, wantErr: ErrMessageTooLarge},
375+
{files: 1000},
376+
{files: 1001, wantErr: ErrMessageTooLarge},
377+
{files: 1, extraKeysPerFile: 9998}, // plus Content-Disposition and Content-Type
378+
{files: 1, extraKeysPerFile: 10000, wantErr: ErrMessageTooLarge},
379+
{godebug: "multipartmaxparts=100", values: 100},
380+
{godebug: "multipartmaxparts=100", values: 101, wantErr: ErrMessageTooLarge},
381+
{godebug: "multipartmaxheaders=100", files: 2, extraKeysPerFile: 48},
382+
{godebug: "multipartmaxheaders=100", files: 2, extraKeysPerFile: 50, wantErr: ErrMessageTooLarge},
383+
} {
384+
name := fmt.Sprintf("values=%v/files=%v/extraKeysPerFile=%v", test.values, test.files, test.extraKeysPerFile)
385+
if test.godebug != "" {
386+
name += fmt.Sprintf("/godebug=%v", test.godebug)
387+
}
388+
t.Run(name, func(t *testing.T) {
389+
if test.godebug != "" {
390+
t.Setenv("GODEBUG", test.godebug)
391+
}
392+
var buf bytes.Buffer
393+
fw := NewWriter(&buf)
394+
for i := 0; i < test.values; i++ {
395+
w, _ := fw.CreateFormField(fmt.Sprintf("field%v", i))
396+
fmt.Fprintf(w, "value %v", i)
397+
}
398+
for i := 0; i < test.files; i++ {
399+
h := make(textproto.MIMEHeader)
400+
h.Set("Content-Disposition",
401+
fmt.Sprintf(`form-data; name="file%v"; filename="file%v"`, i, i))
402+
h.Set("Content-Type", "application/octet-stream")
403+
for j := 0; j < test.extraKeysPerFile; j++ {
404+
h.Set(fmt.Sprintf("k%v", j), "v")
405+
}
406+
w, _ := fw.CreatePart(h)
407+
fmt.Fprintf(w, "value %v", i)
408+
}
409+
if err := fw.Close(); err != nil {
410+
t.Fatal(err)
411+
}
412+
fr := NewReader(bytes.NewReader(buf.Bytes()), fw.Boundary())
413+
form, err := fr.ReadForm(1 << 10)
414+
if err == nil {
415+
defer form.RemoveAll()
416+
}
417+
if err != test.wantErr {
418+
t.Errorf("ReadForm = %v, want %v", err, test.wantErr)
419+
}
420+
})
421+
}
422+
}
423+
363424
func BenchmarkReadForm(b *testing.B) {
364425
for _, test := range []struct {
365426
name string

src/mime/multipart/multipart.go

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ import (
1616
"bufio"
1717
"bytes"
1818
"fmt"
19+
"internal/godebug"
1920
"io"
2021
"mime"
2122
"mime/quotedprintable"
2223
"net/textproto"
2324
"path/filepath"
25+
"strconv"
2426
"strings"
2527
)
2628

@@ -128,12 +130,12 @@ func (r *stickyErrorReader) Read(p []byte) (n int, _ error) {
128130
return n, r.err
129131
}
130132

131-
func newPart(mr *Reader, rawPart bool, maxMIMEHeaderSize int64) (*Part, error) {
133+
func newPart(mr *Reader, rawPart bool, maxMIMEHeaderSize, maxMIMEHeaders int64) (*Part, error) {
132134
bp := &Part{
133135
Header: make(map[string][]string),
134136
mr: mr,
135137
}
136-
if err := bp.populateHeaders(maxMIMEHeaderSize); err != nil {
138+
if err := bp.populateHeaders(maxMIMEHeaderSize, maxMIMEHeaders); err != nil {
137139
return nil, err
138140
}
139141
bp.r = partReader{bp}
@@ -149,9 +151,9 @@ func newPart(mr *Reader, rawPart bool, maxMIMEHeaderSize int64) (*Part, error) {
149151
return bp, nil
150152
}
151153

152-
func (p *Part) populateHeaders(maxMIMEHeaderSize int64) error {
154+
func (p *Part) populateHeaders(maxMIMEHeaderSize, maxMIMEHeaders int64) error {
153155
r := textproto.NewReader(p.mr.bufReader)
154-
header, err := readMIMEHeader(r, maxMIMEHeaderSize)
156+
header, err := readMIMEHeader(r, maxMIMEHeaderSize, maxMIMEHeaders)
155157
if err == nil {
156158
p.Header = header
157159
}
@@ -330,14 +332,27 @@ type Reader struct {
330332
// including header keys, values, and map overhead.
331333
const maxMIMEHeaderSize = 10 << 20
332334

335+
func maxMIMEHeaders() int64 {
336+
// multipartMaxHeaders is the maximum number of header entries NextPart will return,
337+
// as well as the maximum combined total of header entries Reader.ReadForm will return
338+
// in FileHeaders.
339+
multipartMaxHeaders := godebug.Get("multipartmaxheaders")
340+
if multipartMaxHeaders != "" {
341+
if v, err := strconv.ParseInt(multipartMaxHeaders, 10, 64); err == nil && v >= 0 {
342+
return v
343+
}
344+
}
345+
return 10000
346+
}
347+
333348
// NextPart returns the next part in the multipart or an error.
334349
// When there are no more parts, the error io.EOF is returned.
335350
//
336351
// As a special case, if the "Content-Transfer-Encoding" header
337352
// has a value of "quoted-printable", that header is instead
338353
// hidden and the body is transparently decoded during Read calls.
339354
func (r *Reader) NextPart() (*Part, error) {
340-
return r.nextPart(false, maxMIMEHeaderSize)
355+
return r.nextPart(false, maxMIMEHeaderSize, maxMIMEHeaders())
341356
}
342357

343358
// NextRawPart returns the next part in the multipart or an error.
@@ -346,10 +361,10 @@ func (r *Reader) NextPart() (*Part, error) {
346361
// Unlike NextPart, it does not have special handling for
347362
// "Content-Transfer-Encoding: quoted-printable".
348363
func (r *Reader) NextRawPart() (*Part, error) {
349-
return r.nextPart(true, maxMIMEHeaderSize)
364+
return r.nextPart(true, maxMIMEHeaderSize, maxMIMEHeaders())
350365
}
351366

352-
func (r *Reader) nextPart(rawPart bool, maxMIMEHeaderSize int64) (*Part, error) {
367+
func (r *Reader) nextPart(rawPart bool, maxMIMEHeaderSize, maxMIMEHeaders int64) (*Part, error) {
353368
if r.currentPart != nil {
354369
r.currentPart.Close()
355370
}
@@ -374,7 +389,7 @@ func (r *Reader) nextPart(rawPart bool, maxMIMEHeaderSize int64) (*Part, error)
374389

375390
if r.isBoundaryDelimiterLine(line) {
376391
r.partsRead++
377-
bp, err := newPart(r, rawPart, maxMIMEHeaderSize)
392+
bp, err := newPart(r, rawPart, maxMIMEHeaderSize, maxMIMEHeaders)
378393
if err != nil {
379394
return nil, err
380395
}

src/mime/multipart/readmimeheader.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ import (
1111
// readMIMEHeader is defined in package net/textproto.
1212
//
1313
//go:linkname readMIMEHeader net/textproto.readMIMEHeader
14-
func readMIMEHeader(r *textproto.Reader, lim int64) (textproto.MIMEHeader, error)
14+
func readMIMEHeader(r *textproto.Reader, maxMemory, maxHeaders int64) (textproto.MIMEHeader, error)

src/net/textproto/reader.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -483,12 +483,12 @@ var colon = []byte(":")
483483
// "Long-Key": {"Even Longer Value"},
484484
// }
485485
func (r *Reader) ReadMIMEHeader() (MIMEHeader, error) {
486-
return readMIMEHeader(r, math.MaxInt64)
486+
return readMIMEHeader(r, math.MaxInt64, math.MaxInt64)
487487
}
488488

489489
// readMIMEHeader is a version of ReadMIMEHeader which takes a limit on the header size.
490490
// It is called by the mime/multipart package.
491-
func readMIMEHeader(r *Reader, lim int64) (MIMEHeader, error) {
491+
func readMIMEHeader(r *Reader, maxMemory, maxHeaders int64) (MIMEHeader, error) {
492492
// Avoid lots of small slice allocations later by allocating one
493493
// large one ahead of time which we'll cut up into smaller
494494
// slices. If this isn't big enough later, we allocate small ones.
@@ -506,7 +506,7 @@ func readMIMEHeader(r *Reader, lim int64) (MIMEHeader, error) {
506506
// Account for 400 bytes of overhead for the MIMEHeader, plus 200 bytes per entry.
507507
// Benchmarking map creation as of go1.20, a one-entry MIMEHeader is 416 bytes and large
508508
// MIMEHeaders average about 200 bytes per entry.
509-
lim -= 400
509+
maxMemory -= 400
510510
const mapEntryOverhead = 200
511511

512512
// The first line cannot start with a leading space.
@@ -538,16 +538,21 @@ func readMIMEHeader(r *Reader, lim int64) (MIMEHeader, error) {
538538
continue
539539
}
540540

541+
maxHeaders--
542+
if maxHeaders < 0 {
543+
return nil, errors.New("message too large")
544+
}
545+
541546
// Skip initial spaces in value.
542547
value := string(bytes.TrimLeft(v, " \t"))
543548

544549
vv := m[key]
545550
if vv == nil {
546-
lim -= int64(len(key))
547-
lim -= mapEntryOverhead
551+
maxMemory -= int64(len(key))
552+
maxMemory -= mapEntryOverhead
548553
}
549-
lim -= int64(len(value))
550-
if lim < 0 {
554+
maxMemory -= int64(len(value))
555+
if maxMemory < 0 {
551556
// TODO: This should be a distinguishable error (ErrMessageTooLarge)
552557
// to allow mime/multipart to detect it.
553558
return m, errors.New("message too large")

0 commit comments

Comments
 (0)