From f54e13b9f8d5b20030007417ffe4dcd0baa6cd1b Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Mon, 20 Mar 2023 10:43:19 -0700 Subject: [PATCH] [release-branch.go1.19] mime/multipart: limit parsed mime message sizes # AWS EKS Backported To: go-1.16.15-eks Backported On: Wed, 5 Apr 2023 Backported By: kodurub@amazon.com Backported From: release-branch.go1.19 Source Commit: https://github.com/golang/go/commit/7917b5f31204528ea72e0629f0b7d52b35b27538 This fix uses godebug to fetch the value of `multipartMaxHeaders` in `maxMIMEHeaders` method. We set this value to `default` since `GODEBUG` is not backported. In `src/mime/multipart/formdata_test.go`, `TestReadFormLimits` is also modified such that the usage of godebug is eliminated. Removed unused packages in these files accordingly. 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=. Reader.ReadForm limits the number of parts in a form to 1000. This limit may be set with GODEBUG=multipartmaxparts=. 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 Reviewed-by: Roland Shoemaker Reviewed-by: Julie Qiu Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1801087 Reviewed-by: Damien Neil Run-TryBot: Roland Shoemaker Change-Id: If134890d75f0d95c681d67234daf191ba08e6424 Reviewed-on: https://go-review.googlesource.com/c/go/+/481985 Run-TryBot: Michael Knyszek Auto-Submit: Michael Knyszek TryBot-Result: Gopher Robot Reviewed-by: Matthew Dempsky --- src/mime/multipart/formdata.go | 12 +++++- src/mime/multipart/formdata_test.go | 56 ++++++++++++++++++++++++++++ src/mime/multipart/multipart.go | 34 ++++++++++++----- src/mime/multipart/readmimeheader.go | 2 +- src/net/textproto/reader.go | 19 ++++++---- 5 files changed, 104 insertions(+), 19 deletions(-) diff --git a/src/mime/multipart/formdata.go b/src/mime/multipart/formdata.go index a7231c71ac..d7f9c8b102 100644 --- a/src/mime/multipart/formdata.go +++ b/src/mime/multipart/formdata.go @@ -40,6 +40,9 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) { numDiskFiles := 0 multipartFiles := "distinct" combineFiles := multipartFiles != "distinct" + maxParts := 1000 + maxHeaders := maxMIMEHeaders() + defer func() { if file != nil { if cerr := file.Close(); err == nil { @@ -85,13 +88,17 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) { } var copyBuf []byte for { - p, err := r.nextPart(false, maxMemoryBytes) + p, err := r.nextPart(false, maxMemoryBytes, maxHeaders) if err == io.EOF { break } if err != nil { return nil, err } + if maxParts <= 0 { + return nil, ErrMessageTooLarge + } + maxParts-- name := p.FormName() if name == "" { @@ -135,6 +142,9 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) { if maxMemoryBytes < 0 { return nil, ErrMessageTooLarge } + for _, v := range p.Header { + maxHeaders -= int64(len(v)) + } fh := &FileHeader{ Filename: filename, Header: p.Header, diff --git a/src/mime/multipart/formdata_test.go b/src/mime/multipart/formdata_test.go index 5b6638ea6f..bf37ee24be 100644 --- a/src/mime/multipart/formdata_test.go +++ b/src/mime/multipart/formdata_test.go @@ -361,6 +361,62 @@ func testReadFormManyFiles(t *testing.T, distinct bool) { } } +func TestReadFormLimits(t *testing.T) { + for _, test := range []struct { + values int + files int + extraKeysPerFile int + wantErr error + }{ + {values: 1000}, + {values: 1001, wantErr: ErrMessageTooLarge}, + {values: 500, files: 500}, + {values: 501, files: 500, wantErr: ErrMessageTooLarge}, + {files: 1000}, + {files: 1001, wantErr: ErrMessageTooLarge}, + {files: 1, extraKeysPerFile: 9998}, // plus Content-Disposition and Content-Type + {files: 1, extraKeysPerFile: 10000, wantErr: ErrMessageTooLarge}, + } { + name := fmt.Sprintf("values=%v/files=%v/extraKeysPerFile=%v", test.values, test.files, test.extraKeysPerFile) +// if test.godebug != "" { +// name += fmt.Sprintf("/godebug=%v", test.godebug) +// } + t.Run(name, func(t *testing.T) { +// if test.godebug != "" { +// t.Setenv("GODEBUG", test.godebug) +// } + var buf bytes.Buffer + fw := NewWriter(&buf) + for i := 0; i < test.values; i++ { + w, _ := fw.CreateFormField(fmt.Sprintf("field%v", i)) + fmt.Fprintf(w, "value %v", i) + } + for i := 0; i < test.files; i++ { + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", + fmt.Sprintf(`form-data; name="file%v"; filename="file%v"`, i, i)) + h.Set("Content-Type", "application/octet-stream") + for j := 0; j < test.extraKeysPerFile; j++ { + h.Set(fmt.Sprintf("k%v", j), "v") + } + w, _ := fw.CreatePart(h) + fmt.Fprintf(w, "value %v", i) + } + if err := fw.Close(); err != nil { + t.Fatal(err) + } + fr := NewReader(bytes.NewReader(buf.Bytes()), fw.Boundary()) + form, err := fr.ReadForm(1 << 10) + if err == nil { + defer form.RemoveAll() + } + if err != test.wantErr { + t.Errorf("ReadForm = %v, want %v", err, test.wantErr) + } + }) + } +} + func BenchmarkReadForm(b *testing.B) { for _, test := range []struct { name string diff --git a/src/mime/multipart/multipart.go b/src/mime/multipart/multipart.go index 16f33781ba..b345ae9dfc 100644 --- a/src/mime/multipart/multipart.go +++ b/src/mime/multipart/multipart.go @@ -20,6 +20,7 @@ import ( "mime" "mime/quotedprintable" "net/textproto" + "strconv" "strings" ) @@ -120,12 +121,12 @@ func (r *stickyErrorReader) Read(p []byte) (n int, _ error) { return n, r.err } -func newPart(mr *Reader, rawPart bool, maxMIMEHeaderSize int64) (*Part, error) { +func newPart(mr *Reader, rawPart bool, maxMIMEHeaderSize, maxMIMEHeaders int64) (*Part, error) { bp := &Part{ Header: make(map[string][]string), mr: mr, } - if err := bp.populateHeaders(maxMIMEHeaderSize); err != nil { + if err := bp.populateHeaders(maxMIMEHeaderSize, maxMIMEHeaders); err != nil { return nil, err } bp.r = partReader{bp} @@ -141,11 +142,11 @@ func newPart(mr *Reader, rawPart bool, maxMIMEHeaderSize int64) (*Part, error) { return bp, nil } -func (bp *Part) populateHeaders(maxMIMEHeaderSize int64) error { - r := textproto.NewReader(bp.mr.bufReader) - header, err := readMIMEHeader(r, maxMIMEHeaderSize) +func (p *Part) populateHeaders(maxMIMEHeaderSize, maxMIMEHeaders int64) error { + r := textproto.NewReader(p.mr.bufReader) + header, err := readMIMEHeader(r, maxMIMEHeaderSize, maxMIMEHeaders) if err == nil { - bp.Header = header + p.Header = header } // TODO: Add a distinguishable error to net/textproto. if err != nil && err.Error() == "message too large" { @@ -305,6 +306,19 @@ type Reader struct { // including header keys, values, and map overhead. const maxMIMEHeaderSize = 10 << 20 +func maxMIMEHeaders() int64 { + // multipartMaxHeaders is the maximum number of header entries NextPart will return, + // as well as the maximum combined total of header entries Reader.ReadForm will return + // in FileHeaders. + multipartMaxHeaders := "distinct" + if multipartMaxHeaders != "" { + if v, err := strconv.ParseInt(multipartMaxHeaders, 10, 64); err == nil && v >= 0 { + return v + } + } + return 10000 +} + // NextPart returns the next part in the multipart or an error. // When there are no more parts, the error io.EOF is returned. // @@ -312,7 +326,7 @@ const maxMIMEHeaderSize = 10 << 20 // has a value of "quoted-printable", that header is instead // hidden and the body is transparently decoded during Read calls. func (r *Reader) NextPart() (*Part, error) { - return r.nextPart(false, maxMIMEHeaderSize) + return r.nextPart(false, maxMIMEHeaderSize, maxMIMEHeaders()) } // NextRawPart returns the next part in the multipart or an error. @@ -321,10 +335,10 @@ func (r *Reader) NextPart() (*Part, error) { // Unlike NextPart, it does not have special handling for // "Content-Transfer-Encoding: quoted-printable". func (r *Reader) NextRawPart() (*Part, error) { - return r.nextPart(true, maxMIMEHeaderSize) + return r.nextPart(true, maxMIMEHeaderSize, maxMIMEHeaders()) } -func (r *Reader) nextPart(rawPart bool, maxMIMEHeaderSize int64) (*Part, error) { +func (r *Reader) nextPart(rawPart bool, maxMIMEHeaderSize, maxMIMEHeaders int64) (*Part, error) { if r.currentPart != nil { r.currentPart.Close() } @@ -349,7 +363,7 @@ func (r *Reader) nextPart(rawPart bool, maxMIMEHeaderSize int64) (*Part, error) if r.isBoundaryDelimiterLine(line) { r.partsRead++ - bp, err := newPart(r, rawPart, maxMIMEHeaderSize) + bp, err := newPart(r, rawPart, maxMIMEHeaderSize, maxMIMEHeaders) if err != nil { return nil, err } diff --git a/src/mime/multipart/readmimeheader.go b/src/mime/multipart/readmimeheader.go index 6836928c9e..25aa6e2092 100644 --- a/src/mime/multipart/readmimeheader.go +++ b/src/mime/multipart/readmimeheader.go @@ -11,4 +11,4 @@ import ( // readMIMEHeader is defined in package net/textproto. // //go:linkname readMIMEHeader net/textproto.readMIMEHeader -func readMIMEHeader(r *textproto.Reader, lim int64) (textproto.MIMEHeader, error) +func readMIMEHeader(r *textproto.Reader, maxMemory, maxHeaders int64) (textproto.MIMEHeader, error) diff --git a/src/net/textproto/reader.go b/src/net/textproto/reader.go index 0951a05c73..c5b76c29d5 100644 --- a/src/net/textproto/reader.go +++ b/src/net/textproto/reader.go @@ -483,12 +483,12 @@ func (r *Reader) ReadDotLines() ([]string, error) { // } // func (r *Reader) ReadMIMEHeader() (MIMEHeader, error) { - return readMIMEHeader(r, math.MaxInt64) + return readMIMEHeader(r, math.MaxInt64, math.MaxInt64) } // readMIMEHeader is a version of ReadMIMEHeader which takes a limit on the header size. // It is called by the mime/multipart package. -func readMIMEHeader(r *Reader, lim int64) (MIMEHeader, error) { +func readMIMEHeader(r *Reader, maxMemory, maxHeaders int64) (MIMEHeader, error) { // Avoid lots of small slice allocations later by allocating one // large one ahead of time which we'll cut up into smaller // slices. If this isn't big enough later, we allocate small ones. @@ -503,7 +503,7 @@ func readMIMEHeader(r *Reader, lim int64) (MIMEHeader, error) { // Account for 400 bytes of overhead for the MIMEHeader, plus 200 bytes per entry. // Benchmarking map creation as of go1.20, a one-entry MIMEHeader is 416 bytes and large // MIMEHeaders average about 200 bytes per entry. - lim -= 400 + maxMemory -= 400 const mapEntryOverhead = 200 // The first line cannot start with a leading space. @@ -535,6 +535,11 @@ func readMIMEHeader(r *Reader, lim int64) (MIMEHeader, error) { continue } + maxHeaders-- + if maxHeaders < 0 { + return nil, errors.New("message too large") + } + // Skip initial spaces in value. i++ // skip colon for i < len(kv) && (kv[i] == ' ' || kv[i] == '\t') { @@ -544,11 +549,11 @@ func readMIMEHeader(r *Reader, lim int64) (MIMEHeader, error) { vv := m[key] if vv == nil { - lim -= int64(len(key)) - lim -= mapEntryOverhead + maxMemory -= int64(len(key)) + maxMemory -= mapEntryOverhead } - lim -= int64(len(value)) - if lim < 0 { + maxMemory -= int64(len(value)) + if maxMemory < 0 { // TODO: This should be a distinguishable error (ErrMessageTooLarge) // to allow mime/multipart to detect it. return m, errors.New("message too large") -- 2.39.1