1
0
mirror of https://github.com/golang/go synced 2024-09-29 13:14:28 -06:00

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

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>
Change-Id: I08dd297bd75724aade4b0bd6a7d19aeca5bbf99f
Reviewed-on: https://go-review.googlesource.com/c/go/+/482077
Run-TryBot: Michael Knyszek <mknyszek@google.com>
Auto-Submit: Michael Knyszek <mknyszek@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Matthew Dempsky <mdempsky@google.com>
This commit is contained in:
Damien Neil 2023-03-20 10:43:19 -07:00 committed by Gopher Robot
parent 3c010f2c21
commit 1e43cfa15b
8 changed files with 165 additions and 19 deletions

View 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,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,

View File

@ -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

View 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,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
}

View 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)

View 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")

View File

@ -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},

View File

@ -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 " +

View File

@ -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.