]> Cypherpunks.ru repositories - gostls13.git/commitdiff
[release-branch.go1.20] mime/multipart: limit parsed mime message sizes
authorDamien Neil <dneil@google.com>
Mon, 20 Mar 2023 17:43:19 +0000 (10:43 -0700)
committerGopher Robot <gobot@golang.org>
Tue, 4 Apr 2023 16:58:39 +0000 (16:58 +0000)
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 #59270

Change-Id: I36ddceead7f8292c327286fd8694e6113d3b4977
Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1802455
Run-TryBot: Damien Neil <dneil@google.com>
Reviewed-by: Roland Shoemaker <bracewell@google.com>
Reviewed-by: Julie Qiu <julieqiu@google.com>
Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1802608
Run-TryBot: Roland Shoemaker <bracewell@google.com>
Reviewed-on: https://go-review.googlesource.com/c/go/+/481991
Run-TryBot: Michael Knyszek <mknyszek@google.com>
Reviewed-by: Matthew Dempsky <mdempsky@google.com>
Auto-Submit: Michael Knyszek <mknyszek@google.com>
TryBot-Bypass: Michael Knyszek <mknyszek@google.com>

src/mime/multipart/formdata.go
src/mime/multipart/formdata_test.go
src/mime/multipart/multipart.go
src/mime/multipart/readmimeheader.go
src/net/textproto/reader.go

index e757ae370b5c318e7d1098d386423e4b45e461f3..267cfe84cae696c0ca87fb4fe43b7a684d18d82d 100644 (file)
@@ -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,18 @@ 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
+       }
+       maxParts := 1000
+       if s := multipartMaxParts.Value(); s != "" {
+               if v, err := strconv.Atoi(s); err == nil && v >= 0 {
+                       maxParts = v
+               }
+       }
+       maxHeaders := maxMIMEHeaders()
+
        defer func() {
                if file != nil {
                        if cerr := file.Close(); err == nil {
@@ -87,13 +102,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 == "" {
@@ -137,6 +156,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,
index 9e2806df36ced4d57609522844c0aa2d7e125f57..a618a64beb24b6c1b8ce89e2f2f3206095285c65 100644 (file)
@@ -360,6 +360,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
index 86ea926346eb5fd952fc46797e97248969962d22..7b25db9ceca6047923d615b19d3936338da3b688 100644 (file)
@@ -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,20 @@ 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 {
+                       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 +353,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 +362,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 +390,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
                        }
index 6836928c9e8b4850311fe3c70ec36c21ee15c503..25aa6e2092861b0952b7ea17ae64c9eb3c61ba0c 100644 (file)
@@ -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)
index af82b4b9ab153a91e4d3062d279e4917ae203ae2..fc2590b1cdc244c9dccd1e1d0b8f8053d6ecfa81 100644 (file)
@@ -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")