From: Damien Neil Date: Mon, 20 Mar 2023 17:43:19 +0000 (-0700) Subject: mime/multipart: limit parsed mime message sizes X-Git-Tag: go1.21rc1~1049 X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=1e43cfa15b4b618812e85c00c9e92c2615b324c8;p=gostls13.git 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=. 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 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 Change-Id: I08dd297bd75724aade4b0bd6a7d19aeca5bbf99f Reviewed-on: https://go-review.googlesource.com/c/go/+/482077 Run-TryBot: Michael Knyszek Auto-Submit: Michael Knyszek TryBot-Result: Gopher Robot Reviewed-by: Matthew Dempsky --- diff --git a/src/mime/multipart/formdata.go b/src/mime/multipart/formdata.go index 73431c3a84..86a8d2bfcf 100644 --- a/src/mime/multipart/formdata.go +++ b/src/mime/multipart/formdata.go @@ -12,6 +12,7 @@ import ( "math" "net/textproto" "os" + "strconv" ) // ErrMessageTooLarge is returned by ReadForm if the message form @@ -32,7 +33,10 @@ func (r *Reader) ReadForm(maxMemory int64) (*Form, error) { return r.readForm(maxMemory) } -var multipartFiles = godebug.New("multipartfiles") +var ( + multipartFiles = godebug.New("multipartfiles") + multipartMaxParts = godebug.New("multipartmaxparts") +) func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) { form := &Form{make(map[string][]string), make(map[string][]*FileHeader)} @@ -41,7 +45,20 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) { fileOff int64 ) numDiskFiles := 0 - combineFiles := multipartFiles.Value() != "distinct" + combineFiles := true + if multipartFiles.Value() == "distinct" { + combineFiles = false + multipartFiles.IncNonDefault() + } + maxParts := 1000 + if s := multipartMaxParts.Value(); s != "" { + if v, err := strconv.Atoi(s); err == nil && v >= 0 { + maxParts = v + multipartMaxParts.IncNonDefault() + } + } + maxHeaders := maxMIMEHeaders() + defer func() { if file != nil { if cerr := file.Close(); err == nil { @@ -90,13 +107,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 == "" { @@ -140,6 +161,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 7c09cfd07a..d422729c96 100644 --- a/src/mime/multipart/formdata_test.go +++ b/src/mime/multipart/formdata_test.go @@ -391,6 +391,67 @@ func testReadFormManyFiles(t *testing.T, distinct bool) { } } +func TestReadFormLimits(t *testing.T) { + for _, test := range []struct { + values int + files int + extraKeysPerFile int + wantErr error + godebug string + }{ + {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}, + {godebug: "multipartmaxparts=100", values: 100}, + {godebug: "multipartmaxparts=100", values: 101, wantErr: ErrMessageTooLarge}, + {godebug: "multipartmaxheaders=100", files: 2, extraKeysPerFile: 48}, + {godebug: "multipartmaxheaders=100", files: 2, extraKeysPerFile: 50, 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 86ea926346..98d48e2bdd 100644 --- a/src/mime/multipart/multipart.go +++ b/src/mime/multipart/multipart.go @@ -16,11 +16,13 @@ import ( "bufio" "bytes" "fmt" + "internal/godebug" "io" "mime" "mime/quotedprintable" "net/textproto" "path/filepath" + "strconv" "strings" ) @@ -128,12 +130,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} @@ -149,9 +151,9 @@ func newPart(mr *Reader, rawPart bool, maxMIMEHeaderSize int64) (*Part, error) { return bp, nil } -func (p *Part) populateHeaders(maxMIMEHeaderSize int64) error { +func (p *Part) populateHeaders(maxMIMEHeaderSize, maxMIMEHeaders int64) error { r := textproto.NewReader(p.mr.bufReader) - header, err := readMIMEHeader(r, maxMIMEHeaderSize) + header, err := readMIMEHeader(r, maxMIMEHeaderSize, maxMIMEHeaders) if err == nil { p.Header = header } @@ -330,6 +332,21 @@ type Reader struct { // including header keys, values, and map overhead. const maxMIMEHeaderSize = 10 << 20 +// 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. +var multipartMaxHeaders = godebug.New("multipartmaxheaders") + +func maxMIMEHeaders() int64 { + if s := multipartMaxHeaders.Value(); s != "" { + if v, err := strconv.ParseInt(s, 10, 64); err == nil && v >= 0 { + multipartMaxHeaders.IncNonDefault() + 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. // @@ -337,7 +354,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. @@ -346,10 +363,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() } @@ -374,7 +391,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 af82b4b9ab..fc2590b1cd 100644 --- a/src/net/textproto/reader.go +++ b/src/net/textproto/reader.go @@ -479,12 +479,12 @@ var colon = []byte(":") // "Long-Key": {"Even Longer Value"}, // } 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. @@ -502,7 +502,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. @@ -542,16 +542,21 @@ 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. value := string(bytes.TrimLeft(v, " \t")) 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") diff --git a/src/runtime/metrics.go b/src/runtime/metrics.go index d0c0b831e5..55e3d68c2b 100644 --- a/src/runtime/metrics.go +++ b/src/runtime/metrics.go @@ -290,6 +290,9 @@ func initMetrics() { "/godebug/non-default-behavior/http2client:events": {compute: compute0}, "/godebug/non-default-behavior/http2server:events": {compute: compute0}, "/godebug/non-default-behavior/installgoroot:events": {compute: compute0}, + "/godebug/non-default-behavior/multipartfiles:events": {compute: compute0}, + "/godebug/non-default-behavior/multipartmaxheaders:events": {compute: compute0}, + "/godebug/non-default-behavior/multipartmaxparts:events": {compute: compute0}, "/godebug/non-default-behavior/panicnil:events": {compute: compute0}, "/godebug/non-default-behavior/randautoseed:events": {compute: compute0}, "/godebug/non-default-behavior/tarinsecurepath:events": {compute: compute0}, diff --git a/src/runtime/metrics/description.go b/src/runtime/metrics/description.go index 251a4c3842..65288dea61 100644 --- a/src/runtime/metrics/description.go +++ b/src/runtime/metrics/description.go @@ -305,6 +305,27 @@ var allDesc = []Description{ Kind: KindUint64, Cumulative: true, }, + { + Name: "/godebug/non-default-behavior/multipartfiles:events", + Description: "The number of non-default behaviors executed by the mime/multipart package " + + "due to a non-default GODEBUG=multipartfiles=... setting.", + Kind: KindUint64, + Cumulative: true, + }, + { + Name: "/godebug/non-default-behavior/multipartmaxheaders:events", + Description: "The number of non-default behaviors executed by the mime/multipart package " + + "due to a non-default GODEBUG=multipartmaxheaders=... setting.", + Kind: KindUint64, + Cumulative: true, + }, + { + Name: "/godebug/non-default-behavior/multipartmaxparts:events", + Description: "The number of non-default behaviors executed by the mime/multipart package " + + "due to a non-default GODEBUG=multipartmaxparts=... setting.", + Kind: KindUint64, + Cumulative: true, + }, { Name: "/godebug/non-default-behavior/panicnil:events", Description: "The number of non-default behaviors executed by the runtime package " + diff --git a/src/runtime/metrics/doc.go b/src/runtime/metrics/doc.go index 7493943474..4cf528746b 100644 --- a/src/runtime/metrics/doc.go +++ b/src/runtime/metrics/doc.go @@ -219,6 +219,21 @@ Below is the full list of supported metrics, ordered lexicographically. The number of non-default behaviors executed by the go/build package due to a non-default GODEBUG=installgoroot=... setting. + /godebug/non-default-behavior/multipartfiles:events + The number of non-default behaviors executed by + the mime/multipart package due to a non-default + GODEBUG=multipartfiles=... setting. + + /godebug/non-default-behavior/multipartmaxheaders:events + The number of non-default behaviors executed by + the mime/multipart package due to a non-default + GODEBUG=multipartmaxheaders=... setting. + + /godebug/non-default-behavior/multipartmaxparts:events + The number of non-default behaviors executed by + the mime/multipart package due to a non-default + GODEBUG=multipartmaxparts=... setting. + /godebug/non-default-behavior/panicnil:events The number of non-default behaviors executed by the runtime package due to a non-default GODEBUG=panicnil=... setting.