Skip to content

Commit

Permalink
[release-branch.go1.20] mime/multipart: limit parsed mime message sizes
Browse files Browse the repository at this point in the history
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 <[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/+/1802608
Run-TryBot: Roland Shoemaker <[email protected]>
Reviewed-on: https://go-review.googlesource.com/c/go/+/481991
Run-TryBot: Michael Knyszek <[email protected]>
Reviewed-by: Matthew Dempsky <[email protected]>
Auto-Submit: Michael Knyszek <[email protected]>
TryBot-Bypass: Michael Knyszek <[email protected]>
  • Loading branch information
neild authored and gopherbot committed Apr 4, 2023
1 parent ec18f62 commit bf8c7c5
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 19 deletions.
28 changes: 25 additions & 3 deletions src/mime/multipart/formdata.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"math"
"net/textproto"
"os"
"strconv"
)

// ErrMessageTooLarge is returned by ReadForm if the message form
Expand All @@ -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)}
Expand All @@ -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 {
Expand Down Expand Up @@ -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 == "" {
Expand Down Expand Up @@ -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,
Expand Down
61 changes: 61 additions & 0 deletions src/mime/multipart/formdata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 24 additions & 8 deletions src/mime/multipart/multipart.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ import (
"bufio"
"bytes"
"fmt"
"internal/godebug"
"io"
"mime"
"mime/quotedprintable"
"net/textproto"
"path/filepath"
"strconv"
"strings"
)

Expand Down Expand Up @@ -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}
Expand All @@ -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
}
Expand Down Expand Up @@ -330,14 +332,28 @@ 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.
//
// As a special case, if the "Content-Transfer-Encoding" header
// 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.
Expand All @@ -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()
}
Expand All @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion src/mime/multipart/readmimeheader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
19 changes: 12 additions & 7 deletions src/net/textproto/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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")
Expand Down

0 comments on commit bf8c7c5

Please sign in to comment.