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:
parent
3c010f2c21
commit
1e43cfa15b
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
|
@ -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},
|
||||
|
@ -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 " +
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user