diff options
author | Vijay Anusuri <vanusuri@mvista.com> | 2023-09-25 14:15:46 +0530 |
---|---|---|
committer | Steve Sakoman <steve@sakoman.com> | 2023-09-29 04:29:01 -1000 |
commit | ea9b55c8588ce5d7f9d8a1aa317d3c5b9f966dd8 (patch) | |
tree | 3273bfb80942612ea35d381363f158ddbe8577e1 /meta/recipes-devtools/go | |
parent | 0734868d9d9365c63cadf51ff8272fb0662e11a7 (diff) | |
download | poky-ea9b55c8588ce5d7f9d8a1aa317d3c5b9f966dd8.tar.gz |
go: Backport fix for CVE-2022-41725 and CVE-2023-24536
Upstream-commit:
https://github.com/golang/go/commit/874b3132a84cf76da6a48978826c04c380a37a50
&
https://github.com/golang/go/commit/4e5a313524da62600eb59dbf98624cfe946456f8
&
https://github.com/golang/go/commit/5246fa5e75b129a7dbd9722aa4de0cbaf7ceae43
&
https://github.com/golang/go/commit/5c55ac9bf1e5f779220294c843526536605f42ab
&
https://github.com/golang/go/commit/ef41a4e2face45e580c5836eaebd51629fc23f15
&
https://github.com/golang/go/commit/7a359a651c7ebdb29e0a1c03102fce793e9f58f0
&
https://github.com/golang/go/commit/7917b5f31204528ea72e0629f0b7d52b35b27538
(From OE-Core rev: 532eb2c57fb1817999a857fc71db4438717ccadb)
Signed-off-by: Vijay Anusuri <vanusuri@mvista.com>
Signed-off-by: Steve Sakoman <steve@sakoman.com>
Diffstat (limited to 'meta/recipes-devtools/go')
8 files changed, 1614 insertions, 0 deletions
diff --git a/meta/recipes-devtools/go/go-1.14.inc b/meta/recipes-devtools/go/go-1.14.inc index 20377e095b..784b502f46 100644 --- a/meta/recipes-devtools/go/go-1.14.inc +++ b/meta/recipes-devtools/go/go-1.14.inc | |||
@@ -70,6 +70,13 @@ SRC_URI += "\ | |||
70 | file://CVE-2023-29400.patch \ | 70 | file://CVE-2023-29400.patch \ |
71 | file://CVE-2023-29406.patch \ | 71 | file://CVE-2023-29406.patch \ |
72 | file://CVE-2023-29409.patch \ | 72 | file://CVE-2023-29409.patch \ |
73 | file://CVE-2022-41725-pre1.patch \ | ||
74 | file://CVE-2022-41725-pre2.patch \ | ||
75 | file://CVE-2022-41725-pre3.patch \ | ||
76 | file://CVE-2022-41725.patch \ | ||
77 | file://CVE-2023-24536_1.patch \ | ||
78 | file://CVE-2023-24536_2.patch \ | ||
79 | file://CVE-2023-24536_3.patch \ | ||
73 | " | 80 | " |
74 | 81 | ||
75 | SRC_URI_append_libc-musl = " file://0009-ld-replace-glibc-dynamic-linker-with-musl.patch" | 82 | SRC_URI_append_libc-musl = " file://0009-ld-replace-glibc-dynamic-linker-with-musl.patch" |
diff --git a/meta/recipes-devtools/go/go-1.14/CVE-2022-41725-pre1.patch b/meta/recipes-devtools/go/go-1.14/CVE-2022-41725-pre1.patch new file mode 100644 index 0000000000..37ebc41947 --- /dev/null +++ b/meta/recipes-devtools/go/go-1.14/CVE-2022-41725-pre1.patch | |||
@@ -0,0 +1,85 @@ | |||
1 | From 874b3132a84cf76da6a48978826c04c380a37a50 Mon Sep 17 00:00:00 2001 | ||
2 | From: avivklas <avivklas@gmail.com> | ||
3 | Date: Fri, 7 Aug 2020 21:50:12 +0300 | ||
4 | Subject: [PATCH] mime/multipart: return overflow errors in Reader.ReadForm | ||
5 | |||
6 | Updates Reader.ReadForm to check for overflow errors that may | ||
7 | result from a leeway addition of 10MiB to the input argument | ||
8 | maxMemory. | ||
9 | |||
10 | Fixes #40430 | ||
11 | |||
12 | Change-Id: I510b8966c95c51d04695ba9d08fcfe005fd11a5d | ||
13 | Reviewed-on: https://go-review.googlesource.com/c/go/+/247477 | ||
14 | Run-TryBot: Emmanuel Odeke <emm.odeke@gmail.com> | ||
15 | Trust: Cuong Manh Le <cuong.manhle.vn@gmail.com> | ||
16 | Trust: Emmanuel Odeke <emm.odeke@gmail.com> | ||
17 | TryBot-Result: Go Bot <gobot@golang.org> | ||
18 | Reviewed-by: Emmanuel Odeke <emm.odeke@gmail.com> | ||
19 | |||
20 | Upstream-Status: Backport [https://github.com/golang/go/commit/874b3132a84cf76da6a48978826c04c380a37a50] | ||
21 | CVE: CVE-2022-41725 #Dependency Patch1 | ||
22 | Signed-off-by: Vijay Anusuri <vanusuri@mvista.com> | ||
23 | --- | ||
24 | src/mime/multipart/formdata.go | 4 ++++ | ||
25 | src/mime/multipart/formdata_test.go | 18 ++++++++++++++++++ | ||
26 | 2 files changed, 22 insertions(+) | ||
27 | |||
28 | diff --git a/src/mime/multipart/formdata.go b/src/mime/multipart/formdata.go | ||
29 | index 832d0ad693666..4eb31012941ac 100644 | ||
30 | --- a/src/mime/multipart/formdata.go | ||
31 | +++ b/src/mime/multipart/formdata.go | ||
32 | @@ -7,6 +7,7 @@ package multipart | ||
33 | import ( | ||
34 | "bytes" | ||
35 | "errors" | ||
36 | + "fmt" | ||
37 | "io" | ||
38 | "io/ioutil" | ||
39 | "net/textproto" | ||
40 | @@ -41,6 +42,9 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) { | ||
41 | |||
42 | // Reserve an additional 10 MB for non-file parts. | ||
43 | maxValueBytes := maxMemory + int64(10<<20) | ||
44 | + if maxValueBytes <= 0 { | ||
45 | + return nil, fmt.Errorf("multipart: integer overflow from maxMemory(%d) + 10MiB for non-file parts", maxMemory) | ||
46 | + } | ||
47 | for { | ||
48 | p, err := r.NextPart() | ||
49 | if err == io.EOF { | ||
50 | diff --git a/src/mime/multipart/formdata_test.go b/src/mime/multipart/formdata_test.go | ||
51 | index 7d756c8c244a0..7112e0d3727fe 100644 | ||
52 | --- a/src/mime/multipart/formdata_test.go | ||
53 | +++ b/src/mime/multipart/formdata_test.go | ||
54 | @@ -7,6 +7,7 @@ package multipart | ||
55 | import ( | ||
56 | "bytes" | ||
57 | "io" | ||
58 | + "math" | ||
59 | "os" | ||
60 | "strings" | ||
61 | "testing" | ||
62 | @@ -52,6 +53,23 @@ func TestReadFormWithNamelessFile(t *testing.T) { | ||
63 | } | ||
64 | } | ||
65 | |||
66 | +// Issue 40430: Ensure that we report integer overflows in additions of maxMemory, | ||
67 | +// instead of silently and subtly failing without indication. | ||
68 | +func TestReadFormMaxMemoryOverflow(t *testing.T) { | ||
69 | + b := strings.NewReader(strings.ReplaceAll(messageWithTextContentType, "\n", "\r\n")) | ||
70 | + r := NewReader(b, boundary) | ||
71 | + f, err := r.ReadForm(math.MaxInt64) | ||
72 | + if err == nil { | ||
73 | + t.Fatal("Unexpected a non-nil error") | ||
74 | + } | ||
75 | + if f != nil { | ||
76 | + t.Fatalf("Unexpected returned a non-nil form: %v\n", f) | ||
77 | + } | ||
78 | + if g, w := err.Error(), "integer overflow from maxMemory"; !strings.Contains(g, w) { | ||
79 | + t.Errorf(`Error mismatch\n%q\ndid not contain\n%q`, g, w) | ||
80 | + } | ||
81 | +} | ||
82 | + | ||
83 | func TestReadFormWithTextContentType(t *testing.T) { | ||
84 | // From https://github.com/golang/go/issues/24041 | ||
85 | b := strings.NewReader(strings.ReplaceAll(messageWithTextContentType, "\n", "\r\n")) | ||
diff --git a/meta/recipes-devtools/go/go-1.14/CVE-2022-41725-pre2.patch b/meta/recipes-devtools/go/go-1.14/CVE-2022-41725-pre2.patch new file mode 100644 index 0000000000..b951ee893e --- /dev/null +++ b/meta/recipes-devtools/go/go-1.14/CVE-2022-41725-pre2.patch | |||
@@ -0,0 +1,97 @@ | |||
1 | From 4e5a313524da62600eb59dbf98624cfe946456f8 Mon Sep 17 00:00:00 2001 | ||
2 | From: Emmanuel T Odeke <emmanuel@orijtech.com> | ||
3 | Date: Tue, 20 Oct 2020 04:11:12 -0700 | ||
4 | Subject: [PATCH] net/http: test that ParseMultipartForm catches overflows | ||
5 | |||
6 | Tests that if the combination of: | ||
7 | * HTTP multipart file payload size | ||
8 | * ParseMultipartForm's maxMemory parameter | ||
9 | * the internal leeway buffer size of 10MiB | ||
10 | |||
11 | overflows, then we'll report an overflow instead of silently | ||
12 | passing. | ||
13 | |||
14 | Reapplies and fixes CL 254977, which was reverted in CL 263658. | ||
15 | |||
16 | The prior test lacked a res.Body.Close(), so fixed that and | ||
17 | added a leaked Transport check to verify correctness. | ||
18 | |||
19 | Updates 40430. | ||
20 | |||
21 | Change-Id: I3c0f7ef43d621f6eb00f07755f04f9f36c51f98f | ||
22 | Reviewed-on: https://go-review.googlesource.com/c/go/+/263817 | ||
23 | Run-TryBot: Emmanuel Odeke <emm.odeke@gmail.com> | ||
24 | TryBot-Result: Go Bot <gobot@golang.org> | ||
25 | Reviewed-by: Bryan C. Mills <bcmills@google.com> | ||
26 | Trust: Damien Neil <dneil@google.com> | ||
27 | |||
28 | Upstream-Status: Backport [https://github.com/golang/go/commit/4e5a313524da62600eb59dbf98624cfe946456f8] | ||
29 | CVE: CVE-2022-41725 #Dependency Patch2 | ||
30 | Signed-off-by: Vijay Anusuri <vanusuri@mvista.com> | ||
31 | --- | ||
32 | src/net/http/request_test.go | 45 ++++++++++++++++++++++++++++++++++++ | ||
33 | 1 file changed, 45 insertions(+) | ||
34 | |||
35 | diff --git a/src/net/http/request_test.go b/src/net/http/request_test.go | ||
36 | index b4ef472e71229..19526b9ad791a 100644 | ||
37 | --- a/src/net/http/request_test.go | ||
38 | +++ b/src/net/http/request_test.go | ||
39 | @@ -13,6 +13,7 @@ import ( | ||
40 | "fmt" | ||
41 | "io" | ||
42 | "io/ioutil" | ||
43 | + "math" | ||
44 | "mime/multipart" | ||
45 | . "net/http" | ||
46 | "net/http/httptest" | ||
47 | @@ -245,6 +246,50 @@ func TestParseMultipartForm(t *testing.T) { | ||
48 | } | ||
49 | } | ||
50 | |||
51 | +// Issue #40430: Test that if maxMemory for ParseMultipartForm when combined with | ||
52 | +// the payload size and the internal leeway buffer size of 10MiB overflows, that we | ||
53 | +// correctly return an error. | ||
54 | +func TestMaxInt64ForMultipartFormMaxMemoryOverflow(t *testing.T) { | ||
55 | + defer afterTest(t) | ||
56 | + | ||
57 | + payloadSize := 1 << 10 | ||
58 | + cst := httptest.NewServer(HandlerFunc(func(rw ResponseWriter, req *Request) { | ||
59 | + // The combination of: | ||
60 | + // MaxInt64 + payloadSize + (internal spare of 10MiB) | ||
61 | + // triggers the overflow. See issue https://golang.org/issue/40430/ | ||
62 | + if err := req.ParseMultipartForm(math.MaxInt64); err != nil { | ||
63 | + Error(rw, err.Error(), StatusBadRequest) | ||
64 | + return | ||
65 | + } | ||
66 | + })) | ||
67 | + defer cst.Close() | ||
68 | + fBuf := new(bytes.Buffer) | ||
69 | + mw := multipart.NewWriter(fBuf) | ||
70 | + mf, err := mw.CreateFormFile("file", "myfile.txt") | ||
71 | + if err != nil { | ||
72 | + t.Fatal(err) | ||
73 | + } | ||
74 | + if _, err := mf.Write(bytes.Repeat([]byte("abc"), payloadSize)); err != nil { | ||
75 | + t.Fatal(err) | ||
76 | + } | ||
77 | + if err := mw.Close(); err != nil { | ||
78 | + t.Fatal(err) | ||
79 | + } | ||
80 | + req, err := NewRequest("POST", cst.URL, fBuf) | ||
81 | + if err != nil { | ||
82 | + t.Fatal(err) | ||
83 | + } | ||
84 | + req.Header.Set("Content-Type", mw.FormDataContentType()) | ||
85 | + res, err := cst.Client().Do(req) | ||
86 | + if err != nil { | ||
87 | + t.Fatal(err) | ||
88 | + } | ||
89 | + res.Body.Close() | ||
90 | + if g, w := res.StatusCode, StatusBadRequest; g != w { | ||
91 | + t.Fatalf("Status code mismatch: got %d, want %d", g, w) | ||
92 | + } | ||
93 | +} | ||
94 | + | ||
95 | func TestRedirect_h1(t *testing.T) { testRedirect(t, h1Mode) } | ||
96 | func TestRedirect_h2(t *testing.T) { testRedirect(t, h2Mode) } | ||
97 | func testRedirect(t *testing.T, h2 bool) { | ||
diff --git a/meta/recipes-devtools/go/go-1.14/CVE-2022-41725-pre3.patch b/meta/recipes-devtools/go/go-1.14/CVE-2022-41725-pre3.patch new file mode 100644 index 0000000000..767225b888 --- /dev/null +++ b/meta/recipes-devtools/go/go-1.14/CVE-2022-41725-pre3.patch | |||
@@ -0,0 +1,98 @@ | |||
1 | From 5246fa5e75b129a7dbd9722aa4de0cbaf7ceae43 Mon Sep 17 00:00:00 2001 | ||
2 | From: Russ Cox <rsc@golang.org> | ||
3 | Date: Thu, 3 Dec 2020 09:45:07 -0500 | ||
4 | Subject: [PATCH] mime/multipart: handle ReadForm(math.MaxInt64) better | ||
5 | |||
6 | Returning an error about integer overflow is needlessly pedantic. | ||
7 | The meaning of ReadForm(MaxInt64) is easily understood | ||
8 | (accept a lot of data) and can be implemented. | ||
9 | |||
10 | Fixes #40430. | ||
11 | |||
12 | Change-Id: I8a522033dd9a2f9ad31dd2ad82cf08d553736ab9 | ||
13 | Reviewed-on: https://go-review.googlesource.com/c/go/+/275112 | ||
14 | Trust: Russ Cox <rsc@golang.org> | ||
15 | Run-TryBot: Russ Cox <rsc@golang.org> | ||
16 | TryBot-Result: Go Bot <gobot@golang.org> | ||
17 | Reviewed-by: Ian Lance Taylor <iant@golang.org> | ||
18 | |||
19 | Upstream-Status: Backport [https://github.com/golang/go/commit/5246fa5e75b129a7dbd9722aa4de0cbaf7ceae43] | ||
20 | CVE: CVE-2022-41725 #Dependency Patch3 | ||
21 | Signed-off-by: Vijay Anusuri <vanusuri@mvista.com> | ||
22 | --- | ||
23 | src/mime/multipart/formdata.go | 8 ++++++-- | ||
24 | src/mime/multipart/formdata_test.go | 14 +++++--------- | ||
25 | src/net/http/request_test.go | 2 +- | ||
26 | 3 files changed, 12 insertions(+), 12 deletions(-) | ||
27 | |||
28 | diff --git a/src/mime/multipart/formdata.go b/src/mime/multipart/formdata.go | ||
29 | index 4eb31012941ac..9c42ea8c023b5 100644 | ||
30 | --- a/src/mime/multipart/formdata.go | ||
31 | +++ b/src/mime/multipart/formdata.go | ||
32 | @@ -7,9 +7,9 @@ package multipart | ||
33 | import ( | ||
34 | "bytes" | ||
35 | "errors" | ||
36 | - "fmt" | ||
37 | "io" | ||
38 | "io/ioutil" | ||
39 | + "math" | ||
40 | "net/textproto" | ||
41 | "os" | ||
42 | ) | ||
43 | @@ -43,7 +43,11 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) { | ||
44 | // Reserve an additional 10 MB for non-file parts. | ||
45 | maxValueBytes := maxMemory + int64(10<<20) | ||
46 | if maxValueBytes <= 0 { | ||
47 | - return nil, fmt.Errorf("multipart: integer overflow from maxMemory(%d) + 10MiB for non-file parts", maxMemory) | ||
48 | + if maxMemory < 0 { | ||
49 | + maxValueBytes = 0 | ||
50 | + } else { | ||
51 | + maxValueBytes = math.MaxInt64 | ||
52 | + } | ||
53 | } | ||
54 | for { | ||
55 | p, err := r.NextPart() | ||
56 | diff --git a/src/mime/multipart/formdata_test.go b/src/mime/multipart/formdata_test.go | ||
57 | index 7112e0d3727fe..e3a3a3eae8e15 100644 | ||
58 | --- a/src/mime/multipart/formdata_test.go | ||
59 | +++ b/src/mime/multipart/formdata_test.go | ||
60 | @@ -53,20 +53,16 @@ func TestReadFormWithNamelessFile(t *testing.T) { | ||
61 | } | ||
62 | } | ||
63 | |||
64 | -// Issue 40430: Ensure that we report integer overflows in additions of maxMemory, | ||
65 | -// instead of silently and subtly failing without indication. | ||
66 | +// Issue 40430: Handle ReadForm(math.MaxInt64) | ||
67 | func TestReadFormMaxMemoryOverflow(t *testing.T) { | ||
68 | b := strings.NewReader(strings.ReplaceAll(messageWithTextContentType, "\n", "\r\n")) | ||
69 | r := NewReader(b, boundary) | ||
70 | f, err := r.ReadForm(math.MaxInt64) | ||
71 | - if err == nil { | ||
72 | - t.Fatal("Unexpected a non-nil error") | ||
73 | - } | ||
74 | - if f != nil { | ||
75 | - t.Fatalf("Unexpected returned a non-nil form: %v\n", f) | ||
76 | + if err != nil { | ||
77 | + t.Fatalf("ReadForm(MaxInt64): %v", err) | ||
78 | } | ||
79 | - if g, w := err.Error(), "integer overflow from maxMemory"; !strings.Contains(g, w) { | ||
80 | - t.Errorf(`Error mismatch\n%q\ndid not contain\n%q`, g, w) | ||
81 | + if f == nil { | ||
82 | + t.Fatal("ReadForm(MaxInt64): missing form") | ||
83 | } | ||
84 | } | ||
85 | |||
86 | diff --git a/src/net/http/request_test.go b/src/net/http/request_test.go | ||
87 | index 19526b9ad791a..689498e19d5dd 100644 | ||
88 | --- a/src/net/http/request_test.go | ||
89 | +++ b/src/net/http/request_test.go | ||
90 | @@ -285,7 +285,7 @@ func TestMaxInt64ForMultipartFormMaxMemoryOverflow(t *testing.T) { | ||
91 | t.Fatal(err) | ||
92 | } | ||
93 | res.Body.Close() | ||
94 | - if g, w := res.StatusCode, StatusBadRequest; g != w { | ||
95 | + if g, w := res.StatusCode, StatusOK; g != w { | ||
96 | t.Fatalf("Status code mismatch: got %d, want %d", g, w) | ||
97 | } | ||
98 | } | ||
diff --git a/meta/recipes-devtools/go/go-1.14/CVE-2022-41725.patch b/meta/recipes-devtools/go/go-1.14/CVE-2022-41725.patch new file mode 100644 index 0000000000..5f80c62b0b --- /dev/null +++ b/meta/recipes-devtools/go/go-1.14/CVE-2022-41725.patch | |||
@@ -0,0 +1,660 @@ | |||
1 | From 5c55ac9bf1e5f779220294c843526536605f42ab Mon Sep 17 00:00:00 2001 | ||
2 | From: Damien Neil <dneil@google.com> | ||
3 | Date: Wed, 25 Jan 2023 09:27:01 -0800 | ||
4 | Subject: [PATCH] [release-branch.go1.19] mime/multipart: limit memory/inode consumption of ReadForm | ||
5 | |||
6 | Reader.ReadForm is documented as storing "up to maxMemory bytes + 10MB" | ||
7 | in memory. Parsed forms can consume substantially more memory than | ||
8 | this limit, since ReadForm does not account for map entry overhead | ||
9 | and MIME headers. | ||
10 | |||
11 | In addition, while the amount of disk memory consumed by ReadForm can | ||
12 | be constrained by limiting the size of the parsed input, ReadForm will | ||
13 | create one temporary file per form part stored on disk, potentially | ||
14 | consuming a large number of inodes. | ||
15 | |||
16 | Update ReadForm's memory accounting to include part names, | ||
17 | MIME headers, and map entry overhead. | ||
18 | |||
19 | Update ReadForm to store all on-disk file parts in a single | ||
20 | temporary file. | ||
21 | |||
22 | Files returned by FileHeader.Open are documented as having a concrete | ||
23 | type of *os.File when a file is stored on disk. The change to use a | ||
24 | single temporary file for all parts means that this is no longer the | ||
25 | case when a form contains more than a single file part stored on disk. | ||
26 | |||
27 | The previous behavior of storing each file part in a separate disk | ||
28 | file may be reenabled with GODEBUG=multipartfiles=distinct. | ||
29 | |||
30 | Update Reader.NextPart and Reader.NextRawPart to set a 10MiB cap | ||
31 | on the size of MIME headers. | ||
32 | |||
33 | Thanks to Jakob Ackermann (@das7pad) for reporting this issue. | ||
34 | |||
35 | Updates #58006 | ||
36 | Fixes #58362 | ||
37 | Fixes CVE-2022-41725 | ||
38 | |||
39 | Change-Id: Ibd780a6c4c83ac8bcfd3cbe344f042e9940f2eab | ||
40 | Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1714276 | ||
41 | Reviewed-by: Julie Qiu <julieqiu@google.com> | ||
42 | TryBot-Result: Security TryBots <security-trybots@go-security-trybots.iam.gserviceaccount.com> | ||
43 | Reviewed-by: Roland Shoemaker <bracewell@google.com> | ||
44 | Run-TryBot: Damien Neil <dneil@google.com> | ||
45 | (cherry picked from commit ed4664330edcd91b24914c9371c377c132dbce8c) | ||
46 | Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1728949 | ||
47 | Reviewed-by: Tatiana Bradley <tatianabradley@google.com> | ||
48 | Run-TryBot: Roland Shoemaker <bracewell@google.com> | ||
49 | Reviewed-by: Damien Neil <dneil@google.com> | ||
50 | Reviewed-on: https://go-review.googlesource.com/c/go/+/468116 | ||
51 | TryBot-Result: Gopher Robot <gobot@golang.org> | ||
52 | Reviewed-by: Than McIntosh <thanm@google.com> | ||
53 | Run-TryBot: Michael Pratt <mpratt@google.com> | ||
54 | Auto-Submit: Michael Pratt <mpratt@google.com> | ||
55 | |||
56 | Upstream-Status: Backport [https://github.com/golang/go/commit/5c55ac9bf1e5f779220294c843526536605f42ab] | ||
57 | CVE: CVE-2022-41725 | ||
58 | Signed-off-by: Vijay Anusuri <vanusuri@mvista.com> | ||
59 | --- | ||
60 | src/mime/multipart/formdata.go | 132 ++++++++++++++++++++----- | ||
61 | src/mime/multipart/formdata_test.go | 140 ++++++++++++++++++++++++++- | ||
62 | src/mime/multipart/multipart.go | 25 +++-- | ||
63 | src/mime/multipart/readmimeheader.go | 14 +++ | ||
64 | src/net/http/request_test.go | 2 +- | ||
65 | src/net/textproto/reader.go | 27 ++++++ | ||
66 | 6 files changed, 303 insertions(+), 37 deletions(-) | ||
67 | create mode 100644 src/mime/multipart/readmimeheader.go | ||
68 | |||
69 | diff --git a/src/mime/multipart/formdata.go b/src/mime/multipart/formdata.go | ||
70 | index 9c42ea8..1eeb340 100644 | ||
71 | --- a/src/mime/multipart/formdata.go | ||
72 | +++ b/src/mime/multipart/formdata.go | ||
73 | @@ -7,6 +7,7 @@ package multipart | ||
74 | import ( | ||
75 | "bytes" | ||
76 | "errors" | ||
77 | + "internal/godebug" | ||
78 | "io" | ||
79 | "io/ioutil" | ||
80 | "math" | ||
81 | @@ -34,23 +35,58 @@ func (r *Reader) ReadForm(maxMemory int64) (*Form, error) { | ||
82 | |||
83 | func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) { | ||
84 | form := &Form{make(map[string][]string), make(map[string][]*FileHeader)} | ||
85 | + var ( | ||
86 | + file *os.File | ||
87 | + fileOff int64 | ||
88 | + ) | ||
89 | + numDiskFiles := 0 | ||
90 | + multipartFiles := godebug.Get("multipartfiles") | ||
91 | + combineFiles := multipartFiles != "distinct" | ||
92 | defer func() { | ||
93 | + if file != nil { | ||
94 | + if cerr := file.Close(); err == nil { | ||
95 | + err = cerr | ||
96 | + } | ||
97 | + } | ||
98 | + if combineFiles && numDiskFiles > 1 { | ||
99 | + for _, fhs := range form.File { | ||
100 | + for _, fh := range fhs { | ||
101 | + fh.tmpshared = true | ||
102 | + } | ||
103 | + } | ||
104 | + } | ||
105 | if err != nil { | ||
106 | form.RemoveAll() | ||
107 | + if file != nil { | ||
108 | + os.Remove(file.Name()) | ||
109 | + } | ||
110 | } | ||
111 | }() | ||
112 | |||
113 | - // Reserve an additional 10 MB for non-file parts. | ||
114 | - maxValueBytes := maxMemory + int64(10<<20) | ||
115 | - if maxValueBytes <= 0 { | ||
116 | + // maxFileMemoryBytes is the maximum bytes of file data we will store in memory. | ||
117 | + // Data past this limit is written to disk. | ||
118 | + // This limit strictly applies to content, not metadata (filenames, MIME headers, etc.), | ||
119 | + // since metadata is always stored in memory, not disk. | ||
120 | + // | ||
121 | + // maxMemoryBytes is the maximum bytes we will store in memory, including file content, | ||
122 | + // non-file part values, metdata, and map entry overhead. | ||
123 | + // | ||
124 | + // We reserve an additional 10 MB in maxMemoryBytes for non-file data. | ||
125 | + // | ||
126 | + // The relationship between these parameters, as well as the overly-large and | ||
127 | + // unconfigurable 10 MB added on to maxMemory, is unfortunate but difficult to change | ||
128 | + // within the constraints of the API as documented. | ||
129 | + maxFileMemoryBytes := maxMemory | ||
130 | + maxMemoryBytes := maxMemory + int64(10<<20) | ||
131 | + if maxMemoryBytes <= 0 { | ||
132 | if maxMemory < 0 { | ||
133 | - maxValueBytes = 0 | ||
134 | + maxMemoryBytes = 0 | ||
135 | } else { | ||
136 | - maxValueBytes = math.MaxInt64 | ||
137 | + maxMemoryBytes = math.MaxInt64 | ||
138 | } | ||
139 | } | ||
140 | for { | ||
141 | - p, err := r.NextPart() | ||
142 | + p, err := r.nextPart(false, maxMemoryBytes) | ||
143 | if err == io.EOF { | ||
144 | break | ||
145 | } | ||
146 | @@ -64,16 +100,27 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) { | ||
147 | } | ||
148 | filename := p.FileName() | ||
149 | |||
150 | + // Multiple values for the same key (one map entry, longer slice) are cheaper | ||
151 | + // than the same number of values for different keys (many map entries), but | ||
152 | + // using a consistent per-value cost for overhead is simpler. | ||
153 | + maxMemoryBytes -= int64(len(name)) | ||
154 | + maxMemoryBytes -= 100 // map overhead | ||
155 | + if maxMemoryBytes < 0 { | ||
156 | + // We can't actually take this path, since nextPart would already have | ||
157 | + // rejected the MIME headers for being too large. Check anyway. | ||
158 | + return nil, ErrMessageTooLarge | ||
159 | + } | ||
160 | + | ||
161 | var b bytes.Buffer | ||
162 | |||
163 | if filename == "" { | ||
164 | // value, store as string in memory | ||
165 | - n, err := io.CopyN(&b, p, maxValueBytes+1) | ||
166 | + n, err := io.CopyN(&b, p, maxMemoryBytes+1) | ||
167 | if err != nil && err != io.EOF { | ||
168 | return nil, err | ||
169 | } | ||
170 | - maxValueBytes -= n | ||
171 | - if maxValueBytes < 0 { | ||
172 | + maxMemoryBytes -= n | ||
173 | + if maxMemoryBytes < 0 { | ||
174 | return nil, ErrMessageTooLarge | ||
175 | } | ||
176 | form.Value[name] = append(form.Value[name], b.String()) | ||
177 | @@ -81,35 +128,45 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) { | ||
178 | } | ||
179 | |||
180 | // file, store in memory or on disk | ||
181 | + maxMemoryBytes -= mimeHeaderSize(p.Header) | ||
182 | + if maxMemoryBytes < 0 { | ||
183 | + return nil, ErrMessageTooLarge | ||
184 | + } | ||
185 | fh := &FileHeader{ | ||
186 | Filename: filename, | ||
187 | Header: p.Header, | ||
188 | } | ||
189 | - n, err := io.CopyN(&b, p, maxMemory+1) | ||
190 | + n, err := io.CopyN(&b, p, maxFileMemoryBytes+1) | ||
191 | if err != nil && err != io.EOF { | ||
192 | return nil, err | ||
193 | } | ||
194 | - if n > maxMemory { | ||
195 | - // too big, write to disk and flush buffer | ||
196 | - file, err := ioutil.TempFile("", "multipart-") | ||
197 | - if err != nil { | ||
198 | - return nil, err | ||
199 | + if n > maxFileMemoryBytes { | ||
200 | + if file == nil { | ||
201 | + file, err = ioutil.TempFile(r.tempDir, "multipart-") | ||
202 | + if err != nil { | ||
203 | + return nil, err | ||
204 | + } | ||
205 | } | ||
206 | + numDiskFiles++ | ||
207 | size, err := io.Copy(file, io.MultiReader(&b, p)) | ||
208 | - if cerr := file.Close(); err == nil { | ||
209 | - err = cerr | ||
210 | - } | ||
211 | if err != nil { | ||
212 | - os.Remove(file.Name()) | ||
213 | return nil, err | ||
214 | } | ||
215 | fh.tmpfile = file.Name() | ||
216 | fh.Size = size | ||
217 | + fh.tmpoff = fileOff | ||
218 | + fileOff += size | ||
219 | + if !combineFiles { | ||
220 | + if err := file.Close(); err != nil { | ||
221 | + return nil, err | ||
222 | + } | ||
223 | + file = nil | ||
224 | + } | ||
225 | } else { | ||
226 | fh.content = b.Bytes() | ||
227 | fh.Size = int64(len(fh.content)) | ||
228 | - maxMemory -= n | ||
229 | - maxValueBytes -= n | ||
230 | + maxFileMemoryBytes -= n | ||
231 | + maxMemoryBytes -= n | ||
232 | } | ||
233 | form.File[name] = append(form.File[name], fh) | ||
234 | } | ||
235 | @@ -117,6 +174,17 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) { | ||
236 | return form, nil | ||
237 | } | ||
238 | |||
239 | +func mimeHeaderSize(h textproto.MIMEHeader) (size int64) { | ||
240 | + for k, vs := range h { | ||
241 | + size += int64(len(k)) | ||
242 | + size += 100 // map entry overhead | ||
243 | + for _, v := range vs { | ||
244 | + size += int64(len(v)) | ||
245 | + } | ||
246 | + } | ||
247 | + return size | ||
248 | +} | ||
249 | + | ||
250 | // Form is a parsed multipart form. | ||
251 | // Its File parts are stored either in memory or on disk, | ||
252 | // and are accessible via the *FileHeader's Open method. | ||
253 | @@ -134,7 +202,7 @@ func (f *Form) RemoveAll() error { | ||
254 | for _, fh := range fhs { | ||
255 | if fh.tmpfile != "" { | ||
256 | e := os.Remove(fh.tmpfile) | ||
257 | - if e != nil && err == nil { | ||
258 | + if e != nil && !errors.Is(e, os.ErrNotExist) && err == nil { | ||
259 | err = e | ||
260 | } | ||
261 | } | ||
262 | @@ -149,15 +217,25 @@ type FileHeader struct { | ||
263 | Header textproto.MIMEHeader | ||
264 | Size int64 | ||
265 | |||
266 | - content []byte | ||
267 | - tmpfile string | ||
268 | + content []byte | ||
269 | + tmpfile string | ||
270 | + tmpoff int64 | ||
271 | + tmpshared bool | ||
272 | } | ||
273 | |||
274 | // Open opens and returns the FileHeader's associated File. | ||
275 | func (fh *FileHeader) Open() (File, error) { | ||
276 | if b := fh.content; b != nil { | ||
277 | r := io.NewSectionReader(bytes.NewReader(b), 0, int64(len(b))) | ||
278 | - return sectionReadCloser{r}, nil | ||
279 | + return sectionReadCloser{r, nil}, nil | ||
280 | + } | ||
281 | + if fh.tmpshared { | ||
282 | + f, err := os.Open(fh.tmpfile) | ||
283 | + if err != nil { | ||
284 | + return nil, err | ||
285 | + } | ||
286 | + r := io.NewSectionReader(f, fh.tmpoff, fh.Size) | ||
287 | + return sectionReadCloser{r, f}, nil | ||
288 | } | ||
289 | return os.Open(fh.tmpfile) | ||
290 | } | ||
291 | @@ -176,8 +254,12 @@ type File interface { | ||
292 | |||
293 | type sectionReadCloser struct { | ||
294 | *io.SectionReader | ||
295 | + io.Closer | ||
296 | } | ||
297 | |||
298 | func (rc sectionReadCloser) Close() error { | ||
299 | + if rc.Closer != nil { | ||
300 | + return rc.Closer.Close() | ||
301 | + } | ||
302 | return nil | ||
303 | } | ||
304 | diff --git a/src/mime/multipart/formdata_test.go b/src/mime/multipart/formdata_test.go | ||
305 | index e3a3a3e..5cded71 100644 | ||
306 | --- a/src/mime/multipart/formdata_test.go | ||
307 | +++ b/src/mime/multipart/formdata_test.go | ||
308 | @@ -6,8 +6,10 @@ package multipart | ||
309 | |||
310 | import ( | ||
311 | "bytes" | ||
312 | + "fmt" | ||
313 | "io" | ||
314 | "math" | ||
315 | + "net/textproto" | ||
316 | "os" | ||
317 | "strings" | ||
318 | "testing" | ||
319 | @@ -208,8 +210,8 @@ Content-Disposition: form-data; name="largetext" | ||
320 | maxMemory int64 | ||
321 | err error | ||
322 | }{ | ||
323 | - {"smaller", 50, nil}, | ||
324 | - {"exact-fit", 25, nil}, | ||
325 | + {"smaller", 50 + int64(len("largetext")) + 100, nil}, | ||
326 | + {"exact-fit", 25 + int64(len("largetext")) + 100, nil}, | ||
327 | {"too-large", 0, ErrMessageTooLarge}, | ||
328 | } | ||
329 | for _, tc := range testCases { | ||
330 | @@ -224,7 +226,7 @@ Content-Disposition: form-data; name="largetext" | ||
331 | defer f.RemoveAll() | ||
332 | } | ||
333 | if tc.err != err { | ||
334 | - t.Fatalf("ReadForm error - got: %v; expected: %v", tc.err, err) | ||
335 | + t.Fatalf("ReadForm error - got: %v; expected: %v", err, tc.err) | ||
336 | } | ||
337 | if err == nil { | ||
338 | if g := f.Value["largetext"][0]; g != largeTextValue { | ||
339 | @@ -234,3 +236,135 @@ Content-Disposition: form-data; name="largetext" | ||
340 | }) | ||
341 | } | ||
342 | } | ||
343 | + | ||
344 | +// TestReadForm_MetadataTooLarge verifies that we account for the size of field names, | ||
345 | +// MIME headers, and map entry overhead while limiting the memory consumption of parsed forms. | ||
346 | +func TestReadForm_MetadataTooLarge(t *testing.T) { | ||
347 | + for _, test := range []struct { | ||
348 | + name string | ||
349 | + f func(*Writer) | ||
350 | + }{{ | ||
351 | + name: "large name", | ||
352 | + f: func(fw *Writer) { | ||
353 | + name := strings.Repeat("a", 10<<20) | ||
354 | + w, _ := fw.CreateFormField(name) | ||
355 | + w.Write([]byte("value")) | ||
356 | + }, | ||
357 | + }, { | ||
358 | + name: "large MIME header", | ||
359 | + f: func(fw *Writer) { | ||
360 | + h := make(textproto.MIMEHeader) | ||
361 | + h.Set("Content-Disposition", `form-data; name="a"`) | ||
362 | + h.Set("X-Foo", strings.Repeat("a", 10<<20)) | ||
363 | + w, _ := fw.CreatePart(h) | ||
364 | + w.Write([]byte("value")) | ||
365 | + }, | ||
366 | + }, { | ||
367 | + name: "many parts", | ||
368 | + f: func(fw *Writer) { | ||
369 | + for i := 0; i < 110000; i++ { | ||
370 | + w, _ := fw.CreateFormField("f") | ||
371 | + w.Write([]byte("v")) | ||
372 | + } | ||
373 | + }, | ||
374 | + }} { | ||
375 | + t.Run(test.name, func(t *testing.T) { | ||
376 | + var buf bytes.Buffer | ||
377 | + fw := NewWriter(&buf) | ||
378 | + test.f(fw) | ||
379 | + if err := fw.Close(); err != nil { | ||
380 | + t.Fatal(err) | ||
381 | + } | ||
382 | + fr := NewReader(&buf, fw.Boundary()) | ||
383 | + _, err := fr.ReadForm(0) | ||
384 | + if err != ErrMessageTooLarge { | ||
385 | + t.Errorf("fr.ReadForm() = %v, want ErrMessageTooLarge", err) | ||
386 | + } | ||
387 | + }) | ||
388 | + } | ||
389 | +} | ||
390 | + | ||
391 | +// TestReadForm_ManyFiles_Combined tests that a multipart form containing many files only | ||
392 | +// results in a single on-disk file. | ||
393 | +func TestReadForm_ManyFiles_Combined(t *testing.T) { | ||
394 | + const distinct = false | ||
395 | + testReadFormManyFiles(t, distinct) | ||
396 | +} | ||
397 | + | ||
398 | +// TestReadForm_ManyFiles_Distinct tests that setting GODEBUG=multipartfiles=distinct | ||
399 | +// results in every file in a multipart form being placed in a distinct on-disk file. | ||
400 | +func TestReadForm_ManyFiles_Distinct(t *testing.T) { | ||
401 | + t.Setenv("GODEBUG", "multipartfiles=distinct") | ||
402 | + const distinct = true | ||
403 | + testReadFormManyFiles(t, distinct) | ||
404 | +} | ||
405 | + | ||
406 | +func testReadFormManyFiles(t *testing.T, distinct bool) { | ||
407 | + var buf bytes.Buffer | ||
408 | + fw := NewWriter(&buf) | ||
409 | + const numFiles = 10 | ||
410 | + for i := 0; i < numFiles; i++ { | ||
411 | + name := fmt.Sprint(i) | ||
412 | + w, err := fw.CreateFormFile(name, name) | ||
413 | + if err != nil { | ||
414 | + t.Fatal(err) | ||
415 | + } | ||
416 | + w.Write([]byte(name)) | ||
417 | + } | ||
418 | + if err := fw.Close(); err != nil { | ||
419 | + t.Fatal(err) | ||
420 | + } | ||
421 | + fr := NewReader(&buf, fw.Boundary()) | ||
422 | + fr.tempDir = t.TempDir() | ||
423 | + form, err := fr.ReadForm(0) | ||
424 | + if err != nil { | ||
425 | + t.Fatal(err) | ||
426 | + } | ||
427 | + for i := 0; i < numFiles; i++ { | ||
428 | + name := fmt.Sprint(i) | ||
429 | + if got := len(form.File[name]); got != 1 { | ||
430 | + t.Fatalf("form.File[%q] has %v entries, want 1", name, got) | ||
431 | + } | ||
432 | + fh := form.File[name][0] | ||
433 | + file, err := fh.Open() | ||
434 | + if err != nil { | ||
435 | + t.Fatalf("form.File[%q].Open() = %v", name, err) | ||
436 | + } | ||
437 | + if distinct { | ||
438 | + if _, ok := file.(*os.File); !ok { | ||
439 | + t.Fatalf("form.File[%q].Open: %T, want *os.File", name, file) | ||
440 | + } | ||
441 | + } | ||
442 | + got, err := io.ReadAll(file) | ||
443 | + file.Close() | ||
444 | + if string(got) != name || err != nil { | ||
445 | + t.Fatalf("read form.File[%q]: %q, %v; want %q, nil", name, string(got), err, name) | ||
446 | + } | ||
447 | + } | ||
448 | + dir, err := os.Open(fr.tempDir) | ||
449 | + if err != nil { | ||
450 | + t.Fatal(err) | ||
451 | + } | ||
452 | + defer dir.Close() | ||
453 | + names, err := dir.Readdirnames(0) | ||
454 | + if err != nil { | ||
455 | + t.Fatal(err) | ||
456 | + } | ||
457 | + wantNames := 1 | ||
458 | + if distinct { | ||
459 | + wantNames = numFiles | ||
460 | + } | ||
461 | + if len(names) != wantNames { | ||
462 | + t.Fatalf("temp dir contains %v files; want 1", len(names)) | ||
463 | + } | ||
464 | + if err := form.RemoveAll(); err != nil { | ||
465 | + t.Fatalf("form.RemoveAll() = %v", err) | ||
466 | + } | ||
467 | + names, err = dir.Readdirnames(0) | ||
468 | + if err != nil { | ||
469 | + t.Fatal(err) | ||
470 | + } | ||
471 | + if len(names) != 0 { | ||
472 | + t.Fatalf("temp dir contains %v files; want 0", len(names)) | ||
473 | + } | ||
474 | +} | ||
475 | diff --git a/src/mime/multipart/multipart.go b/src/mime/multipart/multipart.go | ||
476 | index 1750300..958cef8 100644 | ||
477 | --- a/src/mime/multipart/multipart.go | ||
478 | +++ b/src/mime/multipart/multipart.go | ||
479 | @@ -121,12 +121,12 @@ func (r *stickyErrorReader) Read(p []byte) (n int, _ error) { | ||
480 | return n, r.err | ||
481 | } | ||
482 | |||
483 | -func newPart(mr *Reader, rawPart bool) (*Part, error) { | ||
484 | +func newPart(mr *Reader, rawPart bool, maxMIMEHeaderSize int64) (*Part, error) { | ||
485 | bp := &Part{ | ||
486 | Header: make(map[string][]string), | ||
487 | mr: mr, | ||
488 | } | ||
489 | - if err := bp.populateHeaders(); err != nil { | ||
490 | + if err := bp.populateHeaders(maxMIMEHeaderSize); err != nil { | ||
491 | return nil, err | ||
492 | } | ||
493 | bp.r = partReader{bp} | ||
494 | @@ -142,12 +142,16 @@ func newPart(mr *Reader, rawPart bool) (*Part, error) { | ||
495 | return bp, nil | ||
496 | } | ||
497 | |||
498 | -func (bp *Part) populateHeaders() error { | ||
499 | +func (bp *Part) populateHeaders(maxMIMEHeaderSize int64) error { | ||
500 | r := textproto.NewReader(bp.mr.bufReader) | ||
501 | - header, err := r.ReadMIMEHeader() | ||
502 | + header, err := readMIMEHeader(r, maxMIMEHeaderSize) | ||
503 | if err == nil { | ||
504 | bp.Header = header | ||
505 | } | ||
506 | + // TODO: Add a distinguishable error to net/textproto. | ||
507 | + if err != nil && err.Error() == "message too large" { | ||
508 | + err = ErrMessageTooLarge | ||
509 | + } | ||
510 | return err | ||
511 | } | ||
512 | |||
513 | @@ -287,6 +291,7 @@ func (p *Part) Close() error { | ||
514 | // isn't supported. | ||
515 | type Reader struct { | ||
516 | bufReader *bufio.Reader | ||
517 | + tempDir string // used in tests | ||
518 | |||
519 | currentPart *Part | ||
520 | partsRead int | ||
521 | @@ -297,6 +302,10 @@ type Reader struct { | ||
522 | dashBoundary []byte // "--boundary" | ||
523 | } | ||
524 | |||
525 | +// maxMIMEHeaderSize is the maximum size of a MIME header we will parse, | ||
526 | +// including header keys, values, and map overhead. | ||
527 | +const maxMIMEHeaderSize = 10 << 20 | ||
528 | + | ||
529 | // NextPart returns the next part in the multipart or an error. | ||
530 | // When there are no more parts, the error io.EOF is returned. | ||
531 | // | ||
532 | @@ -304,7 +313,7 @@ type Reader struct { | ||
533 | // has a value of "quoted-printable", that header is instead | ||
534 | // hidden and the body is transparently decoded during Read calls. | ||
535 | func (r *Reader) NextPart() (*Part, error) { | ||
536 | - return r.nextPart(false) | ||
537 | + return r.nextPart(false, maxMIMEHeaderSize) | ||
538 | } | ||
539 | |||
540 | // NextRawPart returns the next part in the multipart or an error. | ||
541 | @@ -313,10 +322,10 @@ func (r *Reader) NextPart() (*Part, error) { | ||
542 | // Unlike NextPart, it does not have special handling for | ||
543 | // "Content-Transfer-Encoding: quoted-printable". | ||
544 | func (r *Reader) NextRawPart() (*Part, error) { | ||
545 | - return r.nextPart(true) | ||
546 | + return r.nextPart(true, maxMIMEHeaderSize) | ||
547 | } | ||
548 | |||
549 | -func (r *Reader) nextPart(rawPart bool) (*Part, error) { | ||
550 | +func (r *Reader) nextPart(rawPart bool, maxMIMEHeaderSize int64) (*Part, error) { | ||
551 | if r.currentPart != nil { | ||
552 | r.currentPart.Close() | ||
553 | } | ||
554 | @@ -341,7 +350,7 @@ func (r *Reader) nextPart(rawPart bool) (*Part, error) { | ||
555 | |||
556 | if r.isBoundaryDelimiterLine(line) { | ||
557 | r.partsRead++ | ||
558 | - bp, err := newPart(r, rawPart) | ||
559 | + bp, err := newPart(r, rawPart, maxMIMEHeaderSize) | ||
560 | if err != nil { | ||
561 | return nil, err | ||
562 | } | ||
563 | diff --git a/src/mime/multipart/readmimeheader.go b/src/mime/multipart/readmimeheader.go | ||
564 | new file mode 100644 | ||
565 | index 0000000..6836928 | ||
566 | --- /dev/null | ||
567 | +++ b/src/mime/multipart/readmimeheader.go | ||
568 | @@ -0,0 +1,14 @@ | ||
569 | +// Copyright 2023 The Go Authors. All rights reserved. | ||
570 | +// Use of this source code is governed by a BSD-style | ||
571 | +// license that can be found in the LICENSE file. | ||
572 | +package multipart | ||
573 | + | ||
574 | +import ( | ||
575 | + "net/textproto" | ||
576 | + _ "unsafe" // for go:linkname | ||
577 | +) | ||
578 | + | ||
579 | +// readMIMEHeader is defined in package net/textproto. | ||
580 | +// | ||
581 | +//go:linkname readMIMEHeader net/textproto.readMIMEHeader | ||
582 | +func readMIMEHeader(r *textproto.Reader, lim int64) (textproto.MIMEHeader, error) | ||
583 | diff --git a/src/net/http/request_test.go b/src/net/http/request_test.go | ||
584 | index 94133ee..170d3f5 100644 | ||
585 | --- a/src/net/http/request_test.go | ||
586 | +++ b/src/net/http/request_test.go | ||
587 | @@ -962,7 +962,7 @@ func testMissingFile(t *testing.T, req *Request) { | ||
588 | t.Errorf("FormFile file = %v, want nil", f) | ||
589 | } | ||
590 | if fh != nil { | ||
591 | - t.Errorf("FormFile file header = %q, want nil", fh) | ||
592 | + t.Errorf("FormFile file header = %v, want nil", fh) | ||
593 | } | ||
594 | if err != ErrMissingFile { | ||
595 | t.Errorf("FormFile err = %q, want ErrMissingFile", err) | ||
596 | diff --git a/src/net/textproto/reader.go b/src/net/textproto/reader.go | ||
597 | index f63f5ec..96553fb 100644 | ||
598 | --- a/src/net/textproto/reader.go | ||
599 | +++ b/src/net/textproto/reader.go | ||
600 | @@ -7,9 +7,11 @@ package textproto | ||
601 | import ( | ||
602 | "bufio" | ||
603 | "bytes" | ||
604 | + "errors" | ||
605 | "fmt" | ||
606 | "io" | ||
607 | "io/ioutil" | ||
608 | + "math" | ||
609 | "strconv" | ||
610 | "strings" | ||
611 | "sync" | ||
612 | @@ -482,6 +484,12 @@ func (r *Reader) ReadDotLines() ([]string, error) { | ||
613 | // } | ||
614 | // | ||
615 | func (r *Reader) ReadMIMEHeader() (MIMEHeader, error) { | ||
616 | + return readMIMEHeader(r, math.MaxInt64) | ||
617 | +} | ||
618 | + | ||
619 | +// readMIMEHeader is a version of ReadMIMEHeader which takes a limit on the header size. | ||
620 | +// It is called by the mime/multipart package. | ||
621 | +func readMIMEHeader(r *Reader, lim int64) (MIMEHeader, error) { | ||
622 | // Avoid lots of small slice allocations later by allocating one | ||
623 | // large one ahead of time which we'll cut up into smaller | ||
624 | // slices. If this isn't big enough later, we allocate small ones. | ||
625 | @@ -525,6 +533,15 @@ func (r *Reader) ReadMIMEHeader() (MIMEHeader, error) { | ||
626 | continue | ||
627 | } | ||
628 | |||
629 | + // backport 5c55ac9bf1e5f779220294c843526536605f42ab | ||
630 | + // | ||
631 | + // value is computed as | ||
632 | + // value := string(bytes.TrimLeft(v, " \t")) | ||
633 | + // | ||
634 | + // in the original patch from 1.19. This relies on | ||
635 | + // 'v' which does not exist in 1.14. We leave the | ||
636 | + // 1.14 method unchanged. | ||
637 | + | ||
638 | // Skip initial spaces in value. | ||
639 | i++ // skip colon | ||
640 | for i < len(kv) && (kv[i] == ' ' || kv[i] == '\t') { | ||
641 | @@ -533,6 +550,16 @@ func (r *Reader) ReadMIMEHeader() (MIMEHeader, error) { | ||
642 | value := string(kv[i:]) | ||
643 | |||
644 | vv := m[key] | ||
645 | + if vv == nil { | ||
646 | + lim -= int64(len(key)) | ||
647 | + lim -= 100 // map entry overhead | ||
648 | + } | ||
649 | + lim -= int64(len(value)) | ||
650 | + if lim < 0 { | ||
651 | + // TODO: This should be a distinguishable error (ErrMessageTooLarge) | ||
652 | + // to allow mime/multipart to detect it. | ||
653 | + return m, errors.New("message too large") | ||
654 | + } | ||
655 | if vv == nil && len(strs) > 0 { | ||
656 | // More than likely this will be a single-element key. | ||
657 | // Most headers aren't multi-valued. | ||
658 | -- | ||
659 | 2.25.1 | ||
660 | |||
diff --git a/meta/recipes-devtools/go/go-1.14/CVE-2023-24536_1.patch b/meta/recipes-devtools/go/go-1.14/CVE-2023-24536_1.patch new file mode 100644 index 0000000000..39e1304fbd --- /dev/null +++ b/meta/recipes-devtools/go/go-1.14/CVE-2023-24536_1.patch | |||
@@ -0,0 +1,134 @@ | |||
1 | From ef41a4e2face45e580c5836eaebd51629fc23f15 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] [release-branch.go1.19] mime/multipart: avoid excessive copy | ||
5 | buffer allocations 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 | Upstream-Status: Backport [https://github.com/golang/go/commit/ef41a4e2face45e580c5836eaebd51629fc23f15] | ||
33 | CVE: CVE-2023-24536 | ||
34 | Signed-off-by: Vijay Anusuri <vanusuri@mvista.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 a7d4ca97f0484..975dcb6b26db4 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 5cded7170c6b8..f5b56083b2377 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 | +} | ||
diff --git a/meta/recipes-devtools/go/go-1.14/CVE-2023-24536_2.patch b/meta/recipes-devtools/go/go-1.14/CVE-2023-24536_2.patch new file mode 100644 index 0000000000..9ba5114c82 --- /dev/null +++ b/meta/recipes-devtools/go/go-1.14/CVE-2023-24536_2.patch | |||
@@ -0,0 +1,184 @@ | |||
1 | From 7a359a651c7ebdb29e0a1c03102fce793e9f58f0 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] [release-branch.go1.19] net/textproto, mime/multipart: | ||
5 | improve accounting of 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 | Upstream-Status: Backport [https://github.com/golang/go/commit/7a359a651c7ebdb29e0a1c03102fce793e9f58f0] | ||
36 | CVE: CVE-2023-24536 | ||
37 | Signed-off-by: Vijay Anusuri <vanusuri@mvista.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 975dcb6b26db4..3f6ff697ca608 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 f5b56083b2377..8ed26e0c34081 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 9a21777df8be0..c1284fde25eb7 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 | @@ -538,7 +544,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 { | ||
diff --git a/meta/recipes-devtools/go/go-1.14/CVE-2023-24536_3.patch b/meta/recipes-devtools/go/go-1.14/CVE-2023-24536_3.patch new file mode 100644 index 0000000000..58c0a484ee --- /dev/null +++ b/meta/recipes-devtools/go/go-1.14/CVE-2023-24536_3.patch | |||
@@ -0,0 +1,349 @@ | |||
1 | From 7917b5f31204528ea72e0629f0b7d52b35b27538 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] [release-branch.go1.19] 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 | Upstream-Status: Backport [https://github.com/golang/go/commit/7917b5f31204528ea72e0629f0b7d52b35b27538] | ||
46 | CVE: CVE-2023-24536 | ||
47 | Signed-off-by: Vijay Anusuri <vanusuri@mvista.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 216cccb..0b508ae 100644 | ||
58 | --- a/src/mime/multipart/formdata.go | ||
59 | +++ b/src/mime/multipart/formdata.go | ||
60 | @@ -13,6 +13,7 @@ import ( | ||
61 | "math" | ||
62 | "net/textproto" | ||
63 | "os" | ||
64 | + "strconv" | ||
65 | ) | ||
66 | |||
67 | // ErrMessageTooLarge is returned by ReadForm if the message form | ||
68 | @@ -42,6 +43,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 | @@ -87,13 +97,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 | @@ -137,6 +151,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 958cef8..94464a8 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 | "io/ioutil" | ||
196 | "mime" | ||
197 | "mime/quotedprintable" | ||
198 | "net/textproto" | ||
199 | + "strconv" | ||
200 | "strings" | ||
201 | ) | ||
202 | |||
203 | @@ -121,12 +123,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 | @@ -142,9 +144,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 | @@ -306,6 +308,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 | @@ -313,7 +328,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 | @@ -322,10 +337,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 | @@ -350,7 +365,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 1c79f0a..ad2d777 100644 | ||
293 | --- a/src/net/textproto/reader.go | ||
294 | +++ b/src/net/textproto/reader.go | ||
295 | @@ -484,12 +484,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 | @@ -507,7 +507,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 | @@ -539,6 +539,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.25.1 | ||
349 | |||