diff options
| -rw-r--r-- | meta/recipes-devtools/go/go-1.17.13.inc | 3 | ||||
| -rw-r--r-- | meta/recipes-devtools/go/go-1.19/CVE-2023-24536_1.patch | 137 | ||||
| -rw-r--r-- | meta/recipes-devtools/go/go-1.19/CVE-2023-24536_2.patch | 187 | ||||
| -rw-r--r-- | meta/recipes-devtools/go/go-1.19/CVE-2023-24536_3.patch | 349 |
4 files changed, 676 insertions, 0 deletions
diff --git a/meta/recipes-devtools/go/go-1.17.13.inc b/meta/recipes-devtools/go/go-1.17.13.inc index 36904a92fb..53e09a545c 100644 --- a/meta/recipes-devtools/go/go-1.17.13.inc +++ b/meta/recipes-devtools/go/go-1.17.13.inc | |||
| @@ -37,6 +37,9 @@ SRC_URI += "\ | |||
| 37 | file://CVE-2023-29402.patch \ | 37 | file://CVE-2023-29402.patch \ |
| 38 | file://CVE-2023-29400.patch \ | 38 | file://CVE-2023-29400.patch \ |
| 39 | file://CVE-2023-29406.patch \ | 39 | file://CVE-2023-29406.patch \ |
| 40 | file://CVE-2023-24536_1.patch \ | ||
| 41 | file://CVE-2023-24536_2.patch \ | ||
| 42 | file://CVE-2023-24536_3.patch \ | ||
| 40 | " | 43 | " |
| 41 | SRC_URI[main.sha256sum] = "a1a48b23afb206f95e7bbaa9b898d965f90826f6f1d1fc0c1d784ada0cd300fd" | 44 | SRC_URI[main.sha256sum] = "a1a48b23afb206f95e7bbaa9b898d965f90826f6f1d1fc0c1d784ada0cd300fd" |
| 42 | 45 | ||
diff --git a/meta/recipes-devtools/go/go-1.19/CVE-2023-24536_1.patch b/meta/recipes-devtools/go/go-1.19/CVE-2023-24536_1.patch new file mode 100644 index 0000000000..ff9ba18ec5 --- /dev/null +++ b/meta/recipes-devtools/go/go-1.19/CVE-2023-24536_1.patch | |||
| @@ -0,0 +1,137 @@ | |||
| 1 | From f8d691d335c6ac14bcbae6886b5bf8ca8bf1e6a5 Mon Sep 17 00:00:00 2001 | ||
| 2 | From: Damien Neil <dneil@google.com> | ||
| 3 | Date: Thu, 16 Mar 2023 14:18:04 -0700 | ||
| 4 | Subject: [PATCH 1/3] mime/multipart: avoid excessive copy buffer allocations | ||
| 5 | in ReadForm | ||
| 6 | |||
| 7 | When copying form data to disk with io.Copy, | ||
| 8 | allocate only one copy buffer and reuse it rather than | ||
| 9 | creating two buffers per file (one from io.multiReader.WriteTo, | ||
| 10 | and a second one from os.File.ReadFrom). | ||
| 11 | |||
| 12 | Thanks to Jakob Ackermann (@das7pad) for reporting this issue. | ||
| 13 | |||
| 14 | For CVE-2023-24536 | ||
| 15 | For #59153 | ||
| 16 | For #59269 | ||
| 17 | |||
| 18 | Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1802453 | ||
| 19 | Run-TryBot: Damien Neil <dneil@google.com> | ||
| 20 | Reviewed-by: Julie Qiu <julieqiu@google.com> | ||
| 21 | Reviewed-by: Roland Shoemaker <bracewell@google.com> | ||
| 22 | Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1802395 | ||
| 23 | Run-TryBot: Roland Shoemaker <bracewell@google.com> | ||
| 24 | Reviewed-by: Damien Neil <dneil@google.com> | ||
| 25 | Change-Id: Ie405470c92abffed3356913b37d813e982c96c8b | ||
| 26 | Reviewed-on: https://go-review.googlesource.com/c/go/+/481983 | ||
| 27 | Run-TryBot: Michael Knyszek <mknyszek@google.com> | ||
| 28 | TryBot-Result: Gopher Robot <gobot@golang.org> | ||
| 29 | Auto-Submit: Michael Knyszek <mknyszek@google.com> | ||
| 30 | Reviewed-by: Matthew Dempsky <mdempsky@google.com> | ||
| 31 | |||
| 32 | CVE: CVE-2023-24536 | ||
| 33 | Upstream-Status: Backport [ef41a4e2face45e580c5836eaebd51629fc23f15] | ||
| 34 | Signed-off-by: Sakib Sajal <sakib.sajal@windriver.com> | ||
| 35 | --- | ||
| 36 | src/mime/multipart/formdata.go | 15 +++++++-- | ||
| 37 | src/mime/multipart/formdata_test.go | 49 +++++++++++++++++++++++++++++ | ||
| 38 | 2 files changed, 61 insertions(+), 3 deletions(-) | ||
| 39 | |||
| 40 | diff --git a/src/mime/multipart/formdata.go b/src/mime/multipart/formdata.go | ||
| 41 | index a7d4ca9..975dcb6 100644 | ||
| 42 | --- a/src/mime/multipart/formdata.go | ||
| 43 | +++ b/src/mime/multipart/formdata.go | ||
| 44 | @@ -84,6 +84,7 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) { | ||
| 45 | maxMemoryBytes = math.MaxInt64 | ||
| 46 | } | ||
| 47 | } | ||
| 48 | + var copyBuf []byte | ||
| 49 | for { | ||
| 50 | p, err := r.nextPart(false, maxMemoryBytes) | ||
| 51 | if err == io.EOF { | ||
| 52 | @@ -147,14 +148,22 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) { | ||
| 53 | } | ||
| 54 | } | ||
| 55 | numDiskFiles++ | ||
| 56 | - size, err := io.Copy(file, io.MultiReader(&b, p)) | ||
| 57 | + if _, err := file.Write(b.Bytes()); err != nil { | ||
| 58 | + return nil, err | ||
| 59 | + } | ||
| 60 | + if copyBuf == nil { | ||
| 61 | + copyBuf = make([]byte, 32*1024) // same buffer size as io.Copy uses | ||
| 62 | + } | ||
| 63 | + // os.File.ReadFrom will allocate its own copy buffer if we let io.Copy use it. | ||
| 64 | + type writerOnly struct{ io.Writer } | ||
| 65 | + remainingSize, err := io.CopyBuffer(writerOnly{file}, p, copyBuf) | ||
| 66 | if err != nil { | ||
| 67 | return nil, err | ||
| 68 | } | ||
| 69 | fh.tmpfile = file.Name() | ||
| 70 | - fh.Size = size | ||
| 71 | + fh.Size = int64(b.Len()) + remainingSize | ||
| 72 | fh.tmpoff = fileOff | ||
| 73 | - fileOff += size | ||
| 74 | + fileOff += fh.Size | ||
| 75 | if !combineFiles { | ||
| 76 | if err := file.Close(); err != nil { | ||
| 77 | return nil, err | ||
| 78 | diff --git a/src/mime/multipart/formdata_test.go b/src/mime/multipart/formdata_test.go | ||
| 79 | index 5cded71..f5b5608 100644 | ||
| 80 | --- a/src/mime/multipart/formdata_test.go | ||
| 81 | +++ b/src/mime/multipart/formdata_test.go | ||
| 82 | @@ -368,3 +368,52 @@ func testReadFormManyFiles(t *testing.T, distinct bool) { | ||
| 83 | t.Fatalf("temp dir contains %v files; want 0", len(names)) | ||
| 84 | } | ||
| 85 | } | ||
| 86 | + | ||
| 87 | +func BenchmarkReadForm(b *testing.B) { | ||
| 88 | + for _, test := range []struct { | ||
| 89 | + name string | ||
| 90 | + form func(fw *Writer, count int) | ||
| 91 | + }{{ | ||
| 92 | + name: "fields", | ||
| 93 | + form: func(fw *Writer, count int) { | ||
| 94 | + for i := 0; i < count; i++ { | ||
| 95 | + w, _ := fw.CreateFormField(fmt.Sprintf("field%v", i)) | ||
| 96 | + fmt.Fprintf(w, "value %v", i) | ||
| 97 | + } | ||
| 98 | + }, | ||
| 99 | + }, { | ||
| 100 | + name: "files", | ||
| 101 | + form: func(fw *Writer, count int) { | ||
| 102 | + for i := 0; i < count; i++ { | ||
| 103 | + w, _ := fw.CreateFormFile(fmt.Sprintf("field%v", i), fmt.Sprintf("file%v", i)) | ||
| 104 | + fmt.Fprintf(w, "value %v", i) | ||
| 105 | + } | ||
| 106 | + }, | ||
| 107 | + }} { | ||
| 108 | + b.Run(test.name, func(b *testing.B) { | ||
| 109 | + for _, maxMemory := range []int64{ | ||
| 110 | + 0, | ||
| 111 | + 1 << 20, | ||
| 112 | + } { | ||
| 113 | + var buf bytes.Buffer | ||
| 114 | + fw := NewWriter(&buf) | ||
| 115 | + test.form(fw, 10) | ||
| 116 | + if err := fw.Close(); err != nil { | ||
| 117 | + b.Fatal(err) | ||
| 118 | + } | ||
| 119 | + b.Run(fmt.Sprintf("maxMemory=%v", maxMemory), func(b *testing.B) { | ||
| 120 | + b.ReportAllocs() | ||
| 121 | + for i := 0; i < b.N; i++ { | ||
| 122 | + fr := NewReader(bytes.NewReader(buf.Bytes()), fw.Boundary()) | ||
| 123 | + form, err := fr.ReadForm(maxMemory) | ||
| 124 | + if err != nil { | ||
| 125 | + b.Fatal(err) | ||
| 126 | + } | ||
| 127 | + form.RemoveAll() | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + }) | ||
| 131 | + } | ||
| 132 | + }) | ||
| 133 | + } | ||
| 134 | +} | ||
| 135 | -- | ||
| 136 | 2.35.5 | ||
| 137 | |||
diff --git a/meta/recipes-devtools/go/go-1.19/CVE-2023-24536_2.patch b/meta/recipes-devtools/go/go-1.19/CVE-2023-24536_2.patch new file mode 100644 index 0000000000..704a1fb567 --- /dev/null +++ b/meta/recipes-devtools/go/go-1.19/CVE-2023-24536_2.patch | |||
| @@ -0,0 +1,187 @@ | |||
| 1 | From 4174a87b600c58e8cc00d9d18d0c507c67ca5d41 Mon Sep 17 00:00:00 2001 | ||
| 2 | From: Damien Neil <dneil@google.com> | ||
| 3 | Date: Thu, 16 Mar 2023 16:56:12 -0700 | ||
| 4 | Subject: [PATCH 2/3] net/textproto, mime/multipart: improve accounting of | ||
| 5 | non-file data | ||
| 6 | |||
| 7 | For requests containing large numbers of small parts, | ||
| 8 | memory consumption of a parsed form could be about 250% | ||
| 9 | over the estimated size. | ||
| 10 | |||
| 11 | When considering the size of parsed forms, account for the size of | ||
| 12 | FileHeader structs and increase the estimate of memory consumed by | ||
| 13 | map entries. | ||
| 14 | |||
| 15 | Thanks to Jakob Ackermann (@das7pad) for reporting this issue. | ||
| 16 | |||
| 17 | For CVE-2023-24536 | ||
| 18 | For #59153 | ||
| 19 | For #59269 | ||
| 20 | |||
| 21 | Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1802454 | ||
| 22 | Run-TryBot: Damien Neil <dneil@google.com> | ||
| 23 | Reviewed-by: Roland Shoemaker <bracewell@google.com> | ||
| 24 | Reviewed-by: Julie Qiu <julieqiu@google.com> | ||
| 25 | Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1802396 | ||
| 26 | Run-TryBot: Roland Shoemaker <bracewell@google.com> | ||
| 27 | Reviewed-by: Damien Neil <dneil@google.com> | ||
| 28 | Change-Id: I31bc50e9346b4eee6fbe51a18c3c57230cc066db | ||
| 29 | Reviewed-on: https://go-review.googlesource.com/c/go/+/481984 | ||
| 30 | Reviewed-by: Matthew Dempsky <mdempsky@google.com> | ||
| 31 | Auto-Submit: Michael Knyszek <mknyszek@google.com> | ||
| 32 | TryBot-Result: Gopher Robot <gobot@golang.org> | ||
| 33 | Run-TryBot: Michael Knyszek <mknyszek@google.com> | ||
| 34 | |||
| 35 | CVE: CVE-2023-24536 | ||
| 36 | Upstream-Status: Backport [7a359a651c7ebdb29e0a1c03102fce793e9f58f0] | ||
| 37 | Signed-off-by: Sakib Sajal <sakib.sajal@windriver.com> | ||
| 38 | --- | ||
| 39 | src/mime/multipart/formdata.go | 9 +++-- | ||
| 40 | src/mime/multipart/formdata_test.go | 55 ++++++++++++----------------- | ||
| 41 | src/net/textproto/reader.go | 8 ++++- | ||
| 42 | 3 files changed, 37 insertions(+), 35 deletions(-) | ||
| 43 | |||
| 44 | diff --git a/src/mime/multipart/formdata.go b/src/mime/multipart/formdata.go | ||
| 45 | index 975dcb6..3f6ff69 100644 | ||
| 46 | --- a/src/mime/multipart/formdata.go | ||
| 47 | +++ b/src/mime/multipart/formdata.go | ||
| 48 | @@ -103,8 +103,9 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) { | ||
| 49 | // Multiple values for the same key (one map entry, longer slice) are cheaper | ||
| 50 | // than the same number of values for different keys (many map entries), but | ||
| 51 | // using a consistent per-value cost for overhead is simpler. | ||
| 52 | + const mapEntryOverhead = 200 | ||
| 53 | maxMemoryBytes -= int64(len(name)) | ||
| 54 | - maxMemoryBytes -= 100 // map overhead | ||
| 55 | + maxMemoryBytes -= mapEntryOverhead | ||
| 56 | if maxMemoryBytes < 0 { | ||
| 57 | // We can't actually take this path, since nextPart would already have | ||
| 58 | // rejected the MIME headers for being too large. Check anyway. | ||
| 59 | @@ -128,7 +129,10 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) { | ||
| 60 | } | ||
| 61 | |||
| 62 | // file, store in memory or on disk | ||
| 63 | + const fileHeaderSize = 100 | ||
| 64 | maxMemoryBytes -= mimeHeaderSize(p.Header) | ||
| 65 | + maxMemoryBytes -= mapEntryOverhead | ||
| 66 | + maxMemoryBytes -= fileHeaderSize | ||
| 67 | if maxMemoryBytes < 0 { | ||
| 68 | return nil, ErrMessageTooLarge | ||
| 69 | } | ||
| 70 | @@ -183,9 +187,10 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) { | ||
| 71 | } | ||
| 72 | |||
| 73 | func mimeHeaderSize(h textproto.MIMEHeader) (size int64) { | ||
| 74 | + size = 400 | ||
| 75 | for k, vs := range h { | ||
| 76 | size += int64(len(k)) | ||
| 77 | - size += 100 // map entry overhead | ||
| 78 | + size += 200 // map entry overhead | ||
| 79 | for _, v := range vs { | ||
| 80 | size += int64(len(v)) | ||
| 81 | } | ||
| 82 | diff --git a/src/mime/multipart/formdata_test.go b/src/mime/multipart/formdata_test.go | ||
| 83 | index f5b5608..8ed26e0 100644 | ||
| 84 | --- a/src/mime/multipart/formdata_test.go | ||
| 85 | +++ b/src/mime/multipart/formdata_test.go | ||
| 86 | @@ -192,10 +192,10 @@ func (r *failOnReadAfterErrorReader) Read(p []byte) (n int, err error) { | ||
| 87 | // TestReadForm_NonFileMaxMemory asserts that the ReadForm maxMemory limit is applied | ||
| 88 | // while processing non-file form data as well as file form data. | ||
| 89 | func TestReadForm_NonFileMaxMemory(t *testing.T) { | ||
| 90 | - n := 10<<20 + 25 | ||
| 91 | if testing.Short() { | ||
| 92 | - n = 10<<10 + 25 | ||
| 93 | + t.Skip("skipping in -short mode") | ||
| 94 | } | ||
| 95 | + n := 10 << 20 | ||
| 96 | largeTextValue := strings.Repeat("1", n) | ||
| 97 | message := `--MyBoundary | ||
| 98 | Content-Disposition: form-data; name="largetext" | ||
| 99 | @@ -203,38 +203,29 @@ Content-Disposition: form-data; name="largetext" | ||
| 100 | ` + largeTextValue + ` | ||
| 101 | --MyBoundary-- | ||
| 102 | ` | ||
| 103 | - | ||
| 104 | testBody := strings.ReplaceAll(message, "\n", "\r\n") | ||
| 105 | - testCases := []struct { | ||
| 106 | - name string | ||
| 107 | - maxMemory int64 | ||
| 108 | - err error | ||
| 109 | - }{ | ||
| 110 | - {"smaller", 50 + int64(len("largetext")) + 100, nil}, | ||
| 111 | - {"exact-fit", 25 + int64(len("largetext")) + 100, nil}, | ||
| 112 | - {"too-large", 0, ErrMessageTooLarge}, | ||
| 113 | - } | ||
| 114 | - for _, tc := range testCases { | ||
| 115 | - t.Run(tc.name, func(t *testing.T) { | ||
| 116 | - if tc.maxMemory == 0 && testing.Short() { | ||
| 117 | - t.Skip("skipping in -short mode") | ||
| 118 | - } | ||
| 119 | - b := strings.NewReader(testBody) | ||
| 120 | - r := NewReader(b, boundary) | ||
| 121 | - f, err := r.ReadForm(tc.maxMemory) | ||
| 122 | - if err == nil { | ||
| 123 | - defer f.RemoveAll() | ||
| 124 | - } | ||
| 125 | - if tc.err != err { | ||
| 126 | - t.Fatalf("ReadForm error - got: %v; expected: %v", err, tc.err) | ||
| 127 | - } | ||
| 128 | - if err == nil { | ||
| 129 | - if g := f.Value["largetext"][0]; g != largeTextValue { | ||
| 130 | - t.Errorf("largetext mismatch: got size: %v, expected size: %v", len(g), len(largeTextValue)) | ||
| 131 | - } | ||
| 132 | - } | ||
| 133 | - }) | ||
| 134 | + // Try parsing the form with increasing maxMemory values. | ||
| 135 | + // Changes in how we account for non-file form data may cause the exact point | ||
| 136 | + // where we change from rejecting the form as too large to accepting it to vary, | ||
| 137 | + // but we should see both successes and failures. | ||
| 138 | + const failWhenMaxMemoryLessThan = 128 | ||
| 139 | + for maxMemory := int64(0); maxMemory < failWhenMaxMemoryLessThan*2; maxMemory += 16 { | ||
| 140 | + b := strings.NewReader(testBody) | ||
| 141 | + r := NewReader(b, boundary) | ||
| 142 | + f, err := r.ReadForm(maxMemory) | ||
| 143 | + if err != nil { | ||
| 144 | + continue | ||
| 145 | + } | ||
| 146 | + if g := f.Value["largetext"][0]; g != largeTextValue { | ||
| 147 | + t.Errorf("largetext mismatch: got size: %v, expected size: %v", len(g), len(largeTextValue)) | ||
| 148 | + } | ||
| 149 | + f.RemoveAll() | ||
| 150 | + if maxMemory < failWhenMaxMemoryLessThan { | ||
| 151 | + t.Errorf("ReadForm(%v): no error, expect to hit memory limit when maxMemory < %v", maxMemory, failWhenMaxMemoryLessThan) | ||
| 152 | + } | ||
| 153 | + return | ||
| 154 | } | ||
| 155 | + t.Errorf("ReadForm(x) failed for x < 1024, expect success") | ||
| 156 | } | ||
| 157 | |||
| 158 | // TestReadForm_MetadataTooLarge verifies that we account for the size of field names, | ||
| 159 | diff --git a/src/net/textproto/reader.go b/src/net/textproto/reader.go | ||
| 160 | index fcbede8..9af4c49 100644 | ||
| 161 | --- a/src/net/textproto/reader.go | ||
| 162 | +++ b/src/net/textproto/reader.go | ||
| 163 | @@ -503,6 +503,12 @@ func readMIMEHeader(r *Reader, lim int64) (MIMEHeader, error) { | ||
| 164 | |||
| 165 | m := make(MIMEHeader, hint) | ||
| 166 | |||
| 167 | + // Account for 400 bytes of overhead for the MIMEHeader, plus 200 bytes per entry. | ||
| 168 | + // Benchmarking map creation as of go1.20, a one-entry MIMEHeader is 416 bytes and large | ||
| 169 | + // MIMEHeaders average about 200 bytes per entry. | ||
| 170 | + lim -= 400 | ||
| 171 | + const mapEntryOverhead = 200 | ||
| 172 | + | ||
| 173 | // The first line cannot start with a leading space. | ||
| 174 | if buf, err := r.R.Peek(1); err == nil && (buf[0] == ' ' || buf[0] == '\t') { | ||
| 175 | line, err := r.readLineSlice() | ||
| 176 | @@ -552,7 +558,7 @@ func readMIMEHeader(r *Reader, lim int64) (MIMEHeader, error) { | ||
| 177 | vv := m[key] | ||
| 178 | if vv == nil { | ||
| 179 | lim -= int64(len(key)) | ||
| 180 | - lim -= 100 // map entry overhead | ||
| 181 | + lim -= mapEntryOverhead | ||
| 182 | } | ||
| 183 | lim -= int64(len(value)) | ||
| 184 | if lim < 0 { | ||
| 185 | -- | ||
| 186 | 2.35.5 | ||
| 187 | |||
diff --git a/meta/recipes-devtools/go/go-1.19/CVE-2023-24536_3.patch b/meta/recipes-devtools/go/go-1.19/CVE-2023-24536_3.patch new file mode 100644 index 0000000000..6de04e9a61 --- /dev/null +++ b/meta/recipes-devtools/go/go-1.19/CVE-2023-24536_3.patch | |||
| @@ -0,0 +1,349 @@ | |||
| 1 | From ec763bc936f76cec0fe71a791c6bb7d4ac5f3e46 Mon Sep 17 00:00:00 2001 | ||
| 2 | From: Damien Neil <dneil@google.com> | ||
| 3 | Date: Mon, 20 Mar 2023 10:43:19 -0700 | ||
| 4 | Subject: [PATCH 3/3] mime/multipart: limit parsed mime message sizes | ||
| 5 | |||
| 6 | The parsed forms of MIME headers and multipart forms can consume | ||
| 7 | substantially more memory than the size of the input data. | ||
| 8 | A malicious input containing a very large number of headers or | ||
| 9 | form parts can cause excessively large memory allocations. | ||
| 10 | |||
| 11 | Set limits on the size of MIME data: | ||
| 12 | |||
| 13 | Reader.NextPart and Reader.NextRawPart limit the the number | ||
| 14 | of headers in a part to 10000. | ||
| 15 | |||
| 16 | Reader.ReadForm limits the total number of headers in all | ||
| 17 | FileHeaders to 10000. | ||
| 18 | |||
| 19 | Both of these limits may be set with with | ||
| 20 | GODEBUG=multipartmaxheaders=<values>. | ||
| 21 | |||
| 22 | Reader.ReadForm limits the number of parts in a form to 1000. | ||
| 23 | This limit may be set with GODEBUG=multipartmaxparts=<value>. | ||
| 24 | |||
| 25 | Thanks for Jakob Ackermann (@das7pad) for reporting this issue. | ||
| 26 | |||
| 27 | For CVE-2023-24536 | ||
| 28 | For #59153 | ||
| 29 | For #59269 | ||
| 30 | |||
| 31 | Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1802455 | ||
| 32 | Run-TryBot: Damien Neil <dneil@google.com> | ||
| 33 | Reviewed-by: Roland Shoemaker <bracewell@google.com> | ||
| 34 | Reviewed-by: Julie Qiu <julieqiu@google.com> | ||
| 35 | Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1801087 | ||
| 36 | Reviewed-by: Damien Neil <dneil@google.com> | ||
| 37 | Run-TryBot: Roland Shoemaker <bracewell@google.com> | ||
| 38 | Change-Id: If134890d75f0d95c681d67234daf191ba08e6424 | ||
| 39 | Reviewed-on: https://go-review.googlesource.com/c/go/+/481985 | ||
| 40 | Run-TryBot: Michael Knyszek <mknyszek@google.com> | ||
| 41 | Auto-Submit: Michael Knyszek <mknyszek@google.com> | ||
| 42 | TryBot-Result: Gopher Robot <gobot@golang.org> | ||
| 43 | Reviewed-by: Matthew Dempsky <mdempsky@google.com> | ||
| 44 | |||
| 45 | CVE: CVE-2023-24536 | ||
| 46 | Upstream-Status: Backport [7917b5f31204528ea72e0629f0b7d52b35b27538] | ||
| 47 | Signed-off-by: Sakib Sajal <sakib.sajal@windriver.com> | ||
| 48 | --- | ||
| 49 | src/mime/multipart/formdata.go | 19 ++++++++- | ||
| 50 | src/mime/multipart/formdata_test.go | 61 ++++++++++++++++++++++++++++ | ||
| 51 | src/mime/multipart/multipart.go | 31 ++++++++++---- | ||
| 52 | src/mime/multipart/readmimeheader.go | 2 +- | ||
| 53 | src/net/textproto/reader.go | 19 +++++---- | ||
| 54 | 5 files changed, 115 insertions(+), 17 deletions(-) | ||
| 55 | |||
| 56 | diff --git a/src/mime/multipart/formdata.go b/src/mime/multipart/formdata.go | ||
| 57 | index 3f6ff69..4f26aab 100644 | ||
| 58 | --- a/src/mime/multipart/formdata.go | ||
| 59 | +++ b/src/mime/multipart/formdata.go | ||
| 60 | @@ -12,6 +12,7 @@ import ( | ||
| 61 | "math" | ||
| 62 | "net/textproto" | ||
| 63 | "os" | ||
| 64 | + "strconv" | ||
| 65 | ) | ||
| 66 | |||
| 67 | // ErrMessageTooLarge is returned by ReadForm if the message form | ||
| 68 | @@ -41,6 +42,15 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) { | ||
| 69 | numDiskFiles := 0 | ||
| 70 | multipartFiles := godebug.Get("multipartfiles") | ||
| 71 | combineFiles := multipartFiles != "distinct" | ||
| 72 | + maxParts := 1000 | ||
| 73 | + multipartMaxParts := godebug.Get("multipartmaxparts") | ||
| 74 | + if multipartMaxParts != "" { | ||
| 75 | + if v, err := strconv.Atoi(multipartMaxParts); err == nil && v >= 0 { | ||
| 76 | + maxParts = v | ||
| 77 | + } | ||
| 78 | + } | ||
| 79 | + maxHeaders := maxMIMEHeaders() | ||
| 80 | + | ||
| 81 | defer func() { | ||
| 82 | if file != nil { | ||
| 83 | if cerr := file.Close(); err == nil { | ||
| 84 | @@ -86,13 +96,17 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) { | ||
| 85 | } | ||
| 86 | var copyBuf []byte | ||
| 87 | for { | ||
| 88 | - p, err := r.nextPart(false, maxMemoryBytes) | ||
| 89 | + p, err := r.nextPart(false, maxMemoryBytes, maxHeaders) | ||
| 90 | if err == io.EOF { | ||
| 91 | break | ||
| 92 | } | ||
| 93 | if err != nil { | ||
| 94 | return nil, err | ||
| 95 | } | ||
| 96 | + if maxParts <= 0 { | ||
| 97 | + return nil, ErrMessageTooLarge | ||
| 98 | + } | ||
| 99 | + maxParts-- | ||
| 100 | |||
| 101 | name := p.FormName() | ||
| 102 | if name == "" { | ||
| 103 | @@ -136,6 +150,9 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) { | ||
| 104 | if maxMemoryBytes < 0 { | ||
| 105 | return nil, ErrMessageTooLarge | ||
| 106 | } | ||
| 107 | + for _, v := range p.Header { | ||
| 108 | + maxHeaders -= int64(len(v)) | ||
| 109 | + } | ||
| 110 | fh := &FileHeader{ | ||
| 111 | Filename: filename, | ||
| 112 | Header: p.Header, | ||
| 113 | diff --git a/src/mime/multipart/formdata_test.go b/src/mime/multipart/formdata_test.go | ||
| 114 | index 8ed26e0..c78eeb7 100644 | ||
| 115 | --- a/src/mime/multipart/formdata_test.go | ||
| 116 | +++ b/src/mime/multipart/formdata_test.go | ||
| 117 | @@ -360,6 +360,67 @@ func testReadFormManyFiles(t *testing.T, distinct bool) { | ||
| 118 | } | ||
| 119 | } | ||
| 120 | |||
| 121 | +func TestReadFormLimits(t *testing.T) { | ||
| 122 | + for _, test := range []struct { | ||
| 123 | + values int | ||
| 124 | + files int | ||
| 125 | + extraKeysPerFile int | ||
| 126 | + wantErr error | ||
| 127 | + godebug string | ||
| 128 | + }{ | ||
| 129 | + {values: 1000}, | ||
| 130 | + {values: 1001, wantErr: ErrMessageTooLarge}, | ||
| 131 | + {values: 500, files: 500}, | ||
| 132 | + {values: 501, files: 500, wantErr: ErrMessageTooLarge}, | ||
| 133 | + {files: 1000}, | ||
| 134 | + {files: 1001, wantErr: ErrMessageTooLarge}, | ||
| 135 | + {files: 1, extraKeysPerFile: 9998}, // plus Content-Disposition and Content-Type | ||
| 136 | + {files: 1, extraKeysPerFile: 10000, wantErr: ErrMessageTooLarge}, | ||
| 137 | + {godebug: "multipartmaxparts=100", values: 100}, | ||
| 138 | + {godebug: "multipartmaxparts=100", values: 101, wantErr: ErrMessageTooLarge}, | ||
| 139 | + {godebug: "multipartmaxheaders=100", files: 2, extraKeysPerFile: 48}, | ||
| 140 | + {godebug: "multipartmaxheaders=100", files: 2, extraKeysPerFile: 50, wantErr: ErrMessageTooLarge}, | ||
| 141 | + } { | ||
| 142 | + name := fmt.Sprintf("values=%v/files=%v/extraKeysPerFile=%v", test.values, test.files, test.extraKeysPerFile) | ||
| 143 | + if test.godebug != "" { | ||
| 144 | + name += fmt.Sprintf("/godebug=%v", test.godebug) | ||
| 145 | + } | ||
| 146 | + t.Run(name, func(t *testing.T) { | ||
| 147 | + if test.godebug != "" { | ||
| 148 | + t.Setenv("GODEBUG", test.godebug) | ||
| 149 | + } | ||
| 150 | + var buf bytes.Buffer | ||
| 151 | + fw := NewWriter(&buf) | ||
| 152 | + for i := 0; i < test.values; i++ { | ||
| 153 | + w, _ := fw.CreateFormField(fmt.Sprintf("field%v", i)) | ||
| 154 | + fmt.Fprintf(w, "value %v", i) | ||
| 155 | + } | ||
| 156 | + for i := 0; i < test.files; i++ { | ||
| 157 | + h := make(textproto.MIMEHeader) | ||
| 158 | + h.Set("Content-Disposition", | ||
| 159 | + fmt.Sprintf(`form-data; name="file%v"; filename="file%v"`, i, i)) | ||
| 160 | + h.Set("Content-Type", "application/octet-stream") | ||
| 161 | + for j := 0; j < test.extraKeysPerFile; j++ { | ||
| 162 | + h.Set(fmt.Sprintf("k%v", j), "v") | ||
| 163 | + } | ||
| 164 | + w, _ := fw.CreatePart(h) | ||
| 165 | + fmt.Fprintf(w, "value %v", i) | ||
| 166 | + } | ||
| 167 | + if err := fw.Close(); err != nil { | ||
| 168 | + t.Fatal(err) | ||
| 169 | + } | ||
| 170 | + fr := NewReader(bytes.NewReader(buf.Bytes()), fw.Boundary()) | ||
| 171 | + form, err := fr.ReadForm(1 << 10) | ||
| 172 | + if err == nil { | ||
| 173 | + defer form.RemoveAll() | ||
| 174 | + } | ||
| 175 | + if err != test.wantErr { | ||
| 176 | + t.Errorf("ReadForm = %v, want %v", err, test.wantErr) | ||
| 177 | + } | ||
| 178 | + }) | ||
| 179 | + } | ||
| 180 | +} | ||
| 181 | + | ||
| 182 | func BenchmarkReadForm(b *testing.B) { | ||
| 183 | for _, test := range []struct { | ||
| 184 | name string | ||
| 185 | diff --git a/src/mime/multipart/multipart.go b/src/mime/multipart/multipart.go | ||
| 186 | index 19fe0ea..80acabc 100644 | ||
| 187 | --- a/src/mime/multipart/multipart.go | ||
| 188 | +++ b/src/mime/multipart/multipart.go | ||
| 189 | @@ -16,11 +16,13 @@ import ( | ||
| 190 | "bufio" | ||
| 191 | "bytes" | ||
| 192 | "fmt" | ||
| 193 | + "internal/godebug" | ||
| 194 | "io" | ||
| 195 | "mime" | ||
| 196 | "mime/quotedprintable" | ||
| 197 | "net/textproto" | ||
| 198 | "path/filepath" | ||
| 199 | + "strconv" | ||
| 200 | "strings" | ||
| 201 | ) | ||
| 202 | |||
| 203 | @@ -128,12 +130,12 @@ func (r *stickyErrorReader) Read(p []byte) (n int, _ error) { | ||
| 204 | return n, r.err | ||
| 205 | } | ||
| 206 | |||
| 207 | -func newPart(mr *Reader, rawPart bool, maxMIMEHeaderSize int64) (*Part, error) { | ||
| 208 | +func newPart(mr *Reader, rawPart bool, maxMIMEHeaderSize, maxMIMEHeaders int64) (*Part, error) { | ||
| 209 | bp := &Part{ | ||
| 210 | Header: make(map[string][]string), | ||
| 211 | mr: mr, | ||
| 212 | } | ||
| 213 | - if err := bp.populateHeaders(maxMIMEHeaderSize); err != nil { | ||
| 214 | + if err := bp.populateHeaders(maxMIMEHeaderSize, maxMIMEHeaders); err != nil { | ||
| 215 | return nil, err | ||
| 216 | } | ||
| 217 | bp.r = partReader{bp} | ||
| 218 | @@ -149,9 +151,9 @@ func newPart(mr *Reader, rawPart bool, maxMIMEHeaderSize int64) (*Part, error) { | ||
| 219 | return bp, nil | ||
| 220 | } | ||
| 221 | |||
| 222 | -func (bp *Part) populateHeaders(maxMIMEHeaderSize int64) error { | ||
| 223 | +func (bp *Part) populateHeaders(maxMIMEHeaderSize, maxMIMEHeaders int64) error { | ||
| 224 | r := textproto.NewReader(bp.mr.bufReader) | ||
| 225 | - header, err := readMIMEHeader(r, maxMIMEHeaderSize) | ||
| 226 | + header, err := readMIMEHeader(r, maxMIMEHeaderSize, maxMIMEHeaders) | ||
| 227 | if err == nil { | ||
| 228 | bp.Header = header | ||
| 229 | } | ||
| 230 | @@ -313,6 +315,19 @@ type Reader struct { | ||
| 231 | // including header keys, values, and map overhead. | ||
| 232 | const maxMIMEHeaderSize = 10 << 20 | ||
| 233 | |||
| 234 | +func maxMIMEHeaders() int64 { | ||
| 235 | + // multipartMaxHeaders is the maximum number of header entries NextPart will return, | ||
| 236 | + // as well as the maximum combined total of header entries Reader.ReadForm will return | ||
| 237 | + // in FileHeaders. | ||
| 238 | + multipartMaxHeaders := godebug.Get("multipartmaxheaders") | ||
| 239 | + if multipartMaxHeaders != "" { | ||
| 240 | + if v, err := strconv.ParseInt(multipartMaxHeaders, 10, 64); err == nil && v >= 0 { | ||
| 241 | + return v | ||
| 242 | + } | ||
| 243 | + } | ||
| 244 | + return 10000 | ||
| 245 | +} | ||
| 246 | + | ||
| 247 | // NextPart returns the next part in the multipart or an error. | ||
| 248 | // When there are no more parts, the error io.EOF is returned. | ||
| 249 | // | ||
| 250 | @@ -320,7 +335,7 @@ const maxMIMEHeaderSize = 10 << 20 | ||
| 251 | // has a value of "quoted-printable", that header is instead | ||
| 252 | // hidden and the body is transparently decoded during Read calls. | ||
| 253 | func (r *Reader) NextPart() (*Part, error) { | ||
| 254 | - return r.nextPart(false, maxMIMEHeaderSize) | ||
| 255 | + return r.nextPart(false, maxMIMEHeaderSize, maxMIMEHeaders()) | ||
| 256 | } | ||
| 257 | |||
| 258 | // NextRawPart returns the next part in the multipart or an error. | ||
| 259 | @@ -329,10 +344,10 @@ func (r *Reader) NextPart() (*Part, error) { | ||
| 260 | // Unlike NextPart, it does not have special handling for | ||
| 261 | // "Content-Transfer-Encoding: quoted-printable". | ||
| 262 | func (r *Reader) NextRawPart() (*Part, error) { | ||
| 263 | - return r.nextPart(true, maxMIMEHeaderSize) | ||
| 264 | + return r.nextPart(true, maxMIMEHeaderSize, maxMIMEHeaders()) | ||
| 265 | } | ||
| 266 | |||
| 267 | -func (r *Reader) nextPart(rawPart bool, maxMIMEHeaderSize int64) (*Part, error) { | ||
| 268 | +func (r *Reader) nextPart(rawPart bool, maxMIMEHeaderSize, maxMIMEHeaders int64) (*Part, error) { | ||
| 269 | if r.currentPart != nil { | ||
| 270 | r.currentPart.Close() | ||
| 271 | } | ||
| 272 | @@ -357,7 +372,7 @@ func (r *Reader) nextPart(rawPart bool, maxMIMEHeaderSize int64) (*Part, error) | ||
| 273 | |||
| 274 | if r.isBoundaryDelimiterLine(line) { | ||
| 275 | r.partsRead++ | ||
| 276 | - bp, err := newPart(r, rawPart, maxMIMEHeaderSize) | ||
| 277 | + bp, err := newPart(r, rawPart, maxMIMEHeaderSize, maxMIMEHeaders) | ||
| 278 | if err != nil { | ||
| 279 | return nil, err | ||
| 280 | } | ||
| 281 | diff --git a/src/mime/multipart/readmimeheader.go b/src/mime/multipart/readmimeheader.go | ||
| 282 | index 6836928..25aa6e2 100644 | ||
| 283 | --- a/src/mime/multipart/readmimeheader.go | ||
| 284 | +++ b/src/mime/multipart/readmimeheader.go | ||
| 285 | @@ -11,4 +11,4 @@ import ( | ||
| 286 | // readMIMEHeader is defined in package net/textproto. | ||
| 287 | // | ||
| 288 | //go:linkname readMIMEHeader net/textproto.readMIMEHeader | ||
| 289 | -func readMIMEHeader(r *textproto.Reader, lim int64) (textproto.MIMEHeader, error) | ||
| 290 | +func readMIMEHeader(r *textproto.Reader, maxMemory, maxHeaders int64) (textproto.MIMEHeader, error) | ||
| 291 | diff --git a/src/net/textproto/reader.go b/src/net/textproto/reader.go | ||
| 292 | index 9af4c49..c6569c8 100644 | ||
| 293 | --- a/src/net/textproto/reader.go | ||
| 294 | +++ b/src/net/textproto/reader.go | ||
| 295 | @@ -483,12 +483,12 @@ func (r *Reader) ReadDotLines() ([]string, error) { | ||
| 296 | // } | ||
| 297 | // | ||
| 298 | func (r *Reader) ReadMIMEHeader() (MIMEHeader, error) { | ||
| 299 | - return readMIMEHeader(r, math.MaxInt64) | ||
| 300 | + return readMIMEHeader(r, math.MaxInt64, math.MaxInt64) | ||
| 301 | } | ||
| 302 | |||
| 303 | // readMIMEHeader is a version of ReadMIMEHeader which takes a limit on the header size. | ||
| 304 | // It is called by the mime/multipart package. | ||
| 305 | -func readMIMEHeader(r *Reader, lim int64) (MIMEHeader, error) { | ||
| 306 | +func readMIMEHeader(r *Reader, maxMemory, maxHeaders int64) (MIMEHeader, error) { | ||
| 307 | // Avoid lots of small slice allocations later by allocating one | ||
| 308 | // large one ahead of time which we'll cut up into smaller | ||
| 309 | // slices. If this isn't big enough later, we allocate small ones. | ||
| 310 | @@ -506,7 +506,7 @@ func readMIMEHeader(r *Reader, lim int64) (MIMEHeader, error) { | ||
| 311 | // Account for 400 bytes of overhead for the MIMEHeader, plus 200 bytes per entry. | ||
| 312 | // Benchmarking map creation as of go1.20, a one-entry MIMEHeader is 416 bytes and large | ||
| 313 | // MIMEHeaders average about 200 bytes per entry. | ||
| 314 | - lim -= 400 | ||
| 315 | + maxMemory -= 400 | ||
| 316 | const mapEntryOverhead = 200 | ||
| 317 | |||
| 318 | // The first line cannot start with a leading space. | ||
| 319 | @@ -538,6 +538,11 @@ func readMIMEHeader(r *Reader, lim int64) (MIMEHeader, error) { | ||
| 320 | continue | ||
| 321 | } | ||
| 322 | |||
| 323 | + maxHeaders-- | ||
| 324 | + if maxHeaders < 0 { | ||
| 325 | + return nil, errors.New("message too large") | ||
| 326 | + } | ||
| 327 | + | ||
| 328 | // backport 5c55ac9bf1e5f779220294c843526536605f42ab | ||
| 329 | // | ||
| 330 | // value is computed as | ||
| 331 | @@ -557,11 +562,11 @@ func readMIMEHeader(r *Reader, lim int64) (MIMEHeader, error) { | ||
| 332 | |||
| 333 | vv := m[key] | ||
| 334 | if vv == nil { | ||
| 335 | - lim -= int64(len(key)) | ||
| 336 | - lim -= mapEntryOverhead | ||
| 337 | + maxMemory -= int64(len(key)) | ||
| 338 | + maxMemory -= mapEntryOverhead | ||
| 339 | } | ||
| 340 | - lim -= int64(len(value)) | ||
| 341 | - if lim < 0 { | ||
| 342 | + maxMemory -= int64(len(value)) | ||
| 343 | + if maxMemory < 0 { | ||
| 344 | // TODO: This should be a distinguishable error (ErrMessageTooLarge) | ||
| 345 | // to allow mime/multipart to detect it. | ||
| 346 | return m, errors.New("message too large") | ||
| 347 | -- | ||
| 348 | 2.35.5 | ||
| 349 | |||
