diff options
author | Sakib Sajal <sakib.sajal@windriver.com> | 2023-03-27 18:25:54 -0400 |
---|---|---|
committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2023-04-11 11:31:52 +0100 |
commit | 7a9f4f7a29425d37501dae2c977a124bdc74335d (patch) | |
tree | 195b1c08f951ec26f7e811d43d7b0459e094c91b /meta/recipes-devtools | |
parent | ce861f9dd059ca4aed2f816b7e1b3bfa1308908e (diff) | |
download | poky-7a9f4f7a29425d37501dae2c977a124bdc74335d.tar.gz |
go: fix CVE-2022-2879 and CVE-2022-41720
Backport appropriate patches to fix CVE-2022-2879 and CVE-2022-41720.
Modified the original fix for CVE-2022-2879 to remove a testdata tarball
and any references to it since git binary diffs are not supported in
quilt.
(From OE-Core rev: a896cebe1ce2363b501723475154350acf0e0783)
Signed-off-by: Sakib Sajal <sakib.sajal@windriver.com>
Signed-off-by: Steve Sakoman <steve@sakoman.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'meta/recipes-devtools')
-rw-r--r-- | meta/recipes-devtools/go/go-1.17.13.inc | 2 | ||||
-rw-r--r-- | meta/recipes-devtools/go/go-1.18/CVE-2022-2879.patch | 177 | ||||
-rw-r--r-- | meta/recipes-devtools/go/go-1.18/CVE-2022-41720.patch | 514 |
3 files changed, 693 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 99662bd298..856c14de40 100644 --- a/meta/recipes-devtools/go/go-1.17.13.inc +++ b/meta/recipes-devtools/go/go-1.17.13.inc | |||
@@ -20,6 +20,8 @@ SRC_URI += "\ | |||
20 | file://0001-net-http-httputil-avoid-query-parameter-smuggling.patch \ | 20 | file://0001-net-http-httputil-avoid-query-parameter-smuggling.patch \ |
21 | file://CVE-2022-41715.patch \ | 21 | file://CVE-2022-41715.patch \ |
22 | file://CVE-2022-41717.patch \ | 22 | file://CVE-2022-41717.patch \ |
23 | file://CVE-2022-2879.patch \ | ||
24 | file://CVE-2022-41720.patch \ | ||
23 | " | 25 | " |
24 | SRC_URI[main.sha256sum] = "a1a48b23afb206f95e7bbaa9b898d965f90826f6f1d1fc0c1d784ada0cd300fd" | 26 | SRC_URI[main.sha256sum] = "a1a48b23afb206f95e7bbaa9b898d965f90826f6f1d1fc0c1d784ada0cd300fd" |
25 | 27 | ||
diff --git a/meta/recipes-devtools/go/go-1.18/CVE-2022-2879.patch b/meta/recipes-devtools/go/go-1.18/CVE-2022-2879.patch new file mode 100644 index 0000000000..0315e1a3ee --- /dev/null +++ b/meta/recipes-devtools/go/go-1.18/CVE-2022-2879.patch | |||
@@ -0,0 +1,177 @@ | |||
1 | From d064ed520a7cc6b480f9565e30751e695d394f4e Mon Sep 17 00:00:00 2001 | ||
2 | From: Damien Neil <dneil@google.com> | ||
3 | Date: Fri, 2 Sep 2022 20:45:18 -0700 | ||
4 | Subject: [PATCH] archive/tar: limit size of headers | ||
5 | |||
6 | Set a 1MiB limit on special file blocks (PAX headers, GNU long names, | ||
7 | GNU link names), to avoid reading arbitrarily large amounts of data | ||
8 | into memory. | ||
9 | |||
10 | Thanks to Adam Korczynski (ADA Logics) and OSS-Fuzz for reporting | ||
11 | this issue. | ||
12 | |||
13 | Fixes CVE-2022-2879 | ||
14 | Updates #54853 | ||
15 | Fixes #55925 | ||
16 | |||
17 | Change-Id: I85136d6ff1e0af101a112190e027987ab4335680 | ||
18 | Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1565555 | ||
19 | Reviewed-by: Tatiana Bradley <tatianabradley@google.com> | ||
20 | Run-TryBot: Roland Shoemaker <bracewell@google.com> | ||
21 | Reviewed-by: Roland Shoemaker <bracewell@google.com> | ||
22 | (cherry picked from commit 6ee768cef6b82adf7a90dcf367a1699ef694f3b2) | ||
23 | Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1590622 | ||
24 | Reviewed-by: Damien Neil <dneil@google.com> | ||
25 | Reviewed-by: Julie Qiu <julieqiu@google.com> | ||
26 | Reviewed-on: https://go-review.googlesource.com/c/go/+/438500 | ||
27 | Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org> | ||
28 | Reviewed-by: Carlos Amedee <carlos@golang.org> | ||
29 | Reviewed-by: Dmitri Shuralyov <dmitshur@google.com> | ||
30 | Run-TryBot: Carlos Amedee <carlos@golang.org> | ||
31 | TryBot-Result: Gopher Robot <gobot@golang.org> | ||
32 | |||
33 | CVE: CVE-2022-2879 | ||
34 | Upstream-Status: Backport [0a723816cd205576945fa57fbdde7e6532d59d08] | ||
35 | Signed-off-by: Sakib Sajal <sakib.sajal@windriver.com> | ||
36 | --- | ||
37 | src/archive/tar/format.go | 4 ++++ | ||
38 | src/archive/tar/reader.go | 14 ++++++++++++-- | ||
39 | src/archive/tar/reader_test.go | 8 +++++++- | ||
40 | src/archive/tar/writer.go | 3 +++ | ||
41 | src/archive/tar/writer_test.go | 27 +++++++++++++++++++++++++++ | ||
42 | 5 files changed, 53 insertions(+), 3 deletions(-) | ||
43 | |||
44 | diff --git a/src/archive/tar/format.go b/src/archive/tar/format.go | ||
45 | index cfe24a5..6642364 100644 | ||
46 | --- a/src/archive/tar/format.go | ||
47 | +++ b/src/archive/tar/format.go | ||
48 | @@ -143,6 +143,10 @@ const ( | ||
49 | blockSize = 512 // Size of each block in a tar stream | ||
50 | nameSize = 100 // Max length of the name field in USTAR format | ||
51 | prefixSize = 155 // Max length of the prefix field in USTAR format | ||
52 | + | ||
53 | + // Max length of a special file (PAX header, GNU long name or link). | ||
54 | + // This matches the limit used by libarchive. | ||
55 | + maxSpecialFileSize = 1 << 20 | ||
56 | ) | ||
57 | |||
58 | // blockPadding computes the number of bytes needed to pad offset up to the | ||
59 | diff --git a/src/archive/tar/reader.go b/src/archive/tar/reader.go | ||
60 | index 1b1d5b4..f645af8 100644 | ||
61 | --- a/src/archive/tar/reader.go | ||
62 | +++ b/src/archive/tar/reader.go | ||
63 | @@ -103,7 +103,7 @@ func (tr *Reader) next() (*Header, error) { | ||
64 | continue // This is a meta header affecting the next header | ||
65 | case TypeGNULongName, TypeGNULongLink: | ||
66 | format.mayOnlyBe(FormatGNU) | ||
67 | - realname, err := io.ReadAll(tr) | ||
68 | + realname, err := readSpecialFile(tr) | ||
69 | if err != nil { | ||
70 | return nil, err | ||
71 | } | ||
72 | @@ -293,7 +293,7 @@ func mergePAX(hdr *Header, paxHdrs map[string]string) (err error) { | ||
73 | // parsePAX parses PAX headers. | ||
74 | // If an extended header (type 'x') is invalid, ErrHeader is returned | ||
75 | func parsePAX(r io.Reader) (map[string]string, error) { | ||
76 | - buf, err := io.ReadAll(r) | ||
77 | + buf, err := readSpecialFile(r) | ||
78 | if err != nil { | ||
79 | return nil, err | ||
80 | } | ||
81 | @@ -826,6 +826,16 @@ func tryReadFull(r io.Reader, b []byte) (n int, err error) { | ||
82 | return n, err | ||
83 | } | ||
84 | |||
85 | +// readSpecialFile is like io.ReadAll except it returns | ||
86 | +// ErrFieldTooLong if more than maxSpecialFileSize is read. | ||
87 | +func readSpecialFile(r io.Reader) ([]byte, error) { | ||
88 | + buf, err := io.ReadAll(io.LimitReader(r, maxSpecialFileSize+1)) | ||
89 | + if len(buf) > maxSpecialFileSize { | ||
90 | + return nil, ErrFieldTooLong | ||
91 | + } | ||
92 | + return buf, err | ||
93 | +} | ||
94 | + | ||
95 | // discard skips n bytes in r, reporting an error if unable to do so. | ||
96 | func discard(r io.Reader, n int64) error { | ||
97 | // If possible, Seek to the last byte before the end of the data section. | ||
98 | diff --git a/src/archive/tar/reader_test.go b/src/archive/tar/reader_test.go | ||
99 | index 789ddc1..926dc3d 100644 | ||
100 | --- a/src/archive/tar/reader_test.go | ||
101 | +++ b/src/archive/tar/reader_test.go | ||
102 | @@ -6,6 +6,7 @@ package tar | ||
103 | |||
104 | import ( | ||
105 | "bytes" | ||
106 | + "compress/bzip2" | ||
107 | "crypto/md5" | ||
108 | "errors" | ||
109 | "fmt" | ||
110 | @@ -625,9 +626,14 @@ func TestReader(t *testing.T) { | ||
111 | } | ||
112 | defer f.Close() | ||
113 | |||
114 | + var fr io.Reader = f | ||
115 | + if strings.HasSuffix(v.file, ".bz2") { | ||
116 | + fr = bzip2.NewReader(fr) | ||
117 | + } | ||
118 | + | ||
119 | // Capture all headers and checksums. | ||
120 | var ( | ||
121 | - tr = NewReader(f) | ||
122 | + tr = NewReader(fr) | ||
123 | hdrs []*Header | ||
124 | chksums []string | ||
125 | rdbuf = make([]byte, 8) | ||
126 | diff --git a/src/archive/tar/writer.go b/src/archive/tar/writer.go | ||
127 | index e80498d..893eac0 100644 | ||
128 | --- a/src/archive/tar/writer.go | ||
129 | +++ b/src/archive/tar/writer.go | ||
130 | @@ -199,6 +199,9 @@ func (tw *Writer) writePAXHeader(hdr *Header, paxHdrs map[string]string) error { | ||
131 | flag = TypeXHeader | ||
132 | } | ||
133 | data := buf.String() | ||
134 | + if len(data) > maxSpecialFileSize { | ||
135 | + return ErrFieldTooLong | ||
136 | + } | ||
137 | if err := tw.writeRawFile(name, data, flag, FormatPAX); err != nil || isGlobal { | ||
138 | return err // Global headers return here | ||
139 | } | ||
140 | diff --git a/src/archive/tar/writer_test.go b/src/archive/tar/writer_test.go | ||
141 | index a00f02d..4e709e5 100644 | ||
142 | --- a/src/archive/tar/writer_test.go | ||
143 | +++ b/src/archive/tar/writer_test.go | ||
144 | @@ -1006,6 +1006,33 @@ func TestIssue12594(t *testing.T) { | ||
145 | } | ||
146 | } | ||
147 | |||
148 | +func TestWriteLongHeader(t *testing.T) { | ||
149 | + for _, test := range []struct { | ||
150 | + name string | ||
151 | + h *Header | ||
152 | + }{{ | ||
153 | + name: "name too long", | ||
154 | + h: &Header{Name: strings.Repeat("a", maxSpecialFileSize)}, | ||
155 | + }, { | ||
156 | + name: "linkname too long", | ||
157 | + h: &Header{Linkname: strings.Repeat("a", maxSpecialFileSize)}, | ||
158 | + }, { | ||
159 | + name: "uname too long", | ||
160 | + h: &Header{Uname: strings.Repeat("a", maxSpecialFileSize)}, | ||
161 | + }, { | ||
162 | + name: "gname too long", | ||
163 | + h: &Header{Gname: strings.Repeat("a", maxSpecialFileSize)}, | ||
164 | + }, { | ||
165 | + name: "PAX header too long", | ||
166 | + h: &Header{PAXRecords: map[string]string{"GOLANG.x": strings.Repeat("a", maxSpecialFileSize)}}, | ||
167 | + }} { | ||
168 | + w := NewWriter(io.Discard) | ||
169 | + if err := w.WriteHeader(test.h); err != ErrFieldTooLong { | ||
170 | + t.Errorf("%v: w.WriteHeader() = %v, want ErrFieldTooLong", test.name, err) | ||
171 | + } | ||
172 | + } | ||
173 | +} | ||
174 | + | ||
175 | // testNonEmptyWriter wraps an io.Writer and ensures that | ||
176 | // Write is never called with an empty buffer. | ||
177 | type testNonEmptyWriter struct{ io.Writer } | ||
diff --git a/meta/recipes-devtools/go/go-1.18/CVE-2022-41720.patch b/meta/recipes-devtools/go/go-1.18/CVE-2022-41720.patch new file mode 100644 index 0000000000..6c2e8804b3 --- /dev/null +++ b/meta/recipes-devtools/go/go-1.18/CVE-2022-41720.patch | |||
@@ -0,0 +1,514 @@ | |||
1 | From f8896a97a0630b0f2f8c488310147f7f20b3ec7d Mon Sep 17 00:00:00 2001 | ||
2 | From: Damien Neil <dneil@google.com> | ||
3 | Date: Thu, 10 Nov 2022 12:16:27 -0800 | ||
4 | Subject: [PATCH] os, net/http: avoid escapes from os.DirFS and http.Dir on | ||
5 | Windows | ||
6 | |||
7 | Do not permit access to Windows reserved device names (NUL, COM1, etc.) | ||
8 | via os.DirFS and http.Dir filesystems. | ||
9 | |||
10 | Avoid escapes from os.DirFS(`\`) on Windows. DirFS would join the | ||
11 | the root to the relative path with a path separator, making | ||
12 | os.DirFS(`\`).Open(`/foo/bar`) open the path `\\foo\bar`, which is | ||
13 | a UNC name. Not only does this not open the intended file, but permits | ||
14 | reference to any file on the system rather than only files on the | ||
15 | current drive. | ||
16 | |||
17 | Make os.DirFS("") invalid, with all file access failing. Previously, | ||
18 | a root of "" was interpreted as "/", which is surprising and probably | ||
19 | unintentional. | ||
20 | |||
21 | Fixes CVE-2022-41720. | ||
22 | Fixes #56694. | ||
23 | |||
24 | Change-Id: I275b5fa391e6ad7404309ea98ccc97405942e0f0 | ||
25 | Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1663832 | ||
26 | Reviewed-by: Julie Qiu <julieqiu@google.com> | ||
27 | Reviewed-by: Tatiana Bradley <tatianabradley@google.com> | ||
28 | Reviewed-on: https://go-review.googlesource.com/c/go/+/455360 | ||
29 | Reviewed-by: Michael Pratt <mpratt@google.com> | ||
30 | TryBot-Result: Gopher Robot <gobot@golang.org> | ||
31 | Run-TryBot: Jenny Rakoczy <jenny@golang.org> | ||
32 | |||
33 | CVE: CVE-2022-41720 | ||
34 | Upstream-Status: Backport [7013a4f5f816af62033ad63dd06b77c30d7a62a7] | ||
35 | Signed-off-by: Sakib Sajal <sakib.sajal@windriver.com> | ||
36 | --- | ||
37 | src/go/build/deps_test.go | 1 + | ||
38 | src/internal/safefilepath/path.go | 21 +++++ | ||
39 | src/internal/safefilepath/path_other.go | 23 ++++++ | ||
40 | src/internal/safefilepath/path_test.go | 88 +++++++++++++++++++++ | ||
41 | src/internal/safefilepath/path_windows.go | 95 +++++++++++++++++++++++ | ||
42 | src/net/http/fs.go | 8 +- | ||
43 | src/net/http/fs_test.go | 28 +++++++ | ||
44 | src/os/file.go | 36 +++++++-- | ||
45 | src/os/os_test.go | 38 +++++++++ | ||
46 | 9 files changed, 328 insertions(+), 10 deletions(-) | ||
47 | create mode 100644 src/internal/safefilepath/path.go | ||
48 | create mode 100644 src/internal/safefilepath/path_other.go | ||
49 | create mode 100644 src/internal/safefilepath/path_test.go | ||
50 | create mode 100644 src/internal/safefilepath/path_windows.go | ||
51 | |||
52 | diff --git a/src/go/build/deps_test.go b/src/go/build/deps_test.go | ||
53 | index 45e2f25..dc3bb8c 100644 | ||
54 | --- a/src/go/build/deps_test.go | ||
55 | +++ b/src/go/build/deps_test.go | ||
56 | @@ -165,6 +165,7 @@ var depsRules = ` | ||
57 | io/fs | ||
58 | < internal/testlog | ||
59 | < internal/poll | ||
60 | + < internal/safefilepath | ||
61 | < os | ||
62 | < os/signal; | ||
63 | |||
64 | diff --git a/src/internal/safefilepath/path.go b/src/internal/safefilepath/path.go | ||
65 | new file mode 100644 | ||
66 | index 0000000..0f0a270 | ||
67 | --- /dev/null | ||
68 | +++ b/src/internal/safefilepath/path.go | ||
69 | @@ -0,0 +1,21 @@ | ||
70 | +// Copyright 2022 The Go Authors. All rights reserved. | ||
71 | +// Use of this source code is governed by a BSD-style | ||
72 | +// license that can be found in the LICENSE file. | ||
73 | + | ||
74 | +// Package safefilepath manipulates operating-system file paths. | ||
75 | +package safefilepath | ||
76 | + | ||
77 | +import ( | ||
78 | + "errors" | ||
79 | +) | ||
80 | + | ||
81 | +var errInvalidPath = errors.New("invalid path") | ||
82 | + | ||
83 | +// FromFS converts a slash-separated path into an operating-system path. | ||
84 | +// | ||
85 | +// FromFS returns an error if the path cannot be represented by the operating | ||
86 | +// system. For example, paths containing '\' and ':' characters are rejected | ||
87 | +// on Windows. | ||
88 | +func FromFS(path string) (string, error) { | ||
89 | + return fromFS(path) | ||
90 | +} | ||
91 | diff --git a/src/internal/safefilepath/path_other.go b/src/internal/safefilepath/path_other.go | ||
92 | new file mode 100644 | ||
93 | index 0000000..f93da18 | ||
94 | --- /dev/null | ||
95 | +++ b/src/internal/safefilepath/path_other.go | ||
96 | @@ -0,0 +1,23 @@ | ||
97 | +// Copyright 2022 The Go Authors. All rights reserved. | ||
98 | +// Use of this source code is governed by a BSD-style | ||
99 | +// license that can be found in the LICENSE file. | ||
100 | + | ||
101 | +//go:build !windows | ||
102 | + | ||
103 | +package safefilepath | ||
104 | + | ||
105 | +import "runtime" | ||
106 | + | ||
107 | +func fromFS(path string) (string, error) { | ||
108 | + if runtime.GOOS == "plan9" { | ||
109 | + if len(path) > 0 && path[0] == '#' { | ||
110 | + return path, errInvalidPath | ||
111 | + } | ||
112 | + } | ||
113 | + for i := range path { | ||
114 | + if path[i] == 0 { | ||
115 | + return "", errInvalidPath | ||
116 | + } | ||
117 | + } | ||
118 | + return path, nil | ||
119 | +} | ||
120 | diff --git a/src/internal/safefilepath/path_test.go b/src/internal/safefilepath/path_test.go | ||
121 | new file mode 100644 | ||
122 | index 0000000..dc662c1 | ||
123 | --- /dev/null | ||
124 | +++ b/src/internal/safefilepath/path_test.go | ||
125 | @@ -0,0 +1,88 @@ | ||
126 | +// Copyright 2022 The Go Authors. All rights reserved. | ||
127 | +// Use of this source code is governed by a BSD-style | ||
128 | +// license that can be found in the LICENSE file. | ||
129 | + | ||
130 | +package safefilepath_test | ||
131 | + | ||
132 | +import ( | ||
133 | + "internal/safefilepath" | ||
134 | + "os" | ||
135 | + "path/filepath" | ||
136 | + "runtime" | ||
137 | + "testing" | ||
138 | +) | ||
139 | + | ||
140 | +type PathTest struct { | ||
141 | + path, result string | ||
142 | +} | ||
143 | + | ||
144 | +const invalid = "" | ||
145 | + | ||
146 | +var fspathtests = []PathTest{ | ||
147 | + {".", "."}, | ||
148 | + {"/a/b/c", "/a/b/c"}, | ||
149 | + {"a\x00b", invalid}, | ||
150 | +} | ||
151 | + | ||
152 | +var winreservedpathtests = []PathTest{ | ||
153 | + {`a\b`, `a\b`}, | ||
154 | + {`a:b`, `a:b`}, | ||
155 | + {`a/b:c`, `a/b:c`}, | ||
156 | + {`NUL`, `NUL`}, | ||
157 | + {`./com1`, `./com1`}, | ||
158 | + {`a/nul/b`, `a/nul/b`}, | ||
159 | +} | ||
160 | + | ||
161 | +// Whether a reserved name with an extension is reserved or not varies by | ||
162 | +// Windows version. | ||
163 | +var winreservedextpathtests = []PathTest{ | ||
164 | + {"nul.txt", "nul.txt"}, | ||
165 | + {"a/nul.txt/b", "a/nul.txt/b"}, | ||
166 | +} | ||
167 | + | ||
168 | +var plan9reservedpathtests = []PathTest{ | ||
169 | + {`#c`, `#c`}, | ||
170 | +} | ||
171 | + | ||
172 | +func TestFromFS(t *testing.T) { | ||
173 | + switch runtime.GOOS { | ||
174 | + case "windows": | ||
175 | + if canWriteFile(t, "NUL") { | ||
176 | + t.Errorf("can unexpectedly write a file named NUL on Windows") | ||
177 | + } | ||
178 | + if canWriteFile(t, "nul.txt") { | ||
179 | + fspathtests = append(fspathtests, winreservedextpathtests...) | ||
180 | + } else { | ||
181 | + winreservedpathtests = append(winreservedpathtests, winreservedextpathtests...) | ||
182 | + } | ||
183 | + for i := range winreservedpathtests { | ||
184 | + winreservedpathtests[i].result = invalid | ||
185 | + } | ||
186 | + for i := range fspathtests { | ||
187 | + fspathtests[i].result = filepath.FromSlash(fspathtests[i].result) | ||
188 | + } | ||
189 | + case "plan9": | ||
190 | + for i := range plan9reservedpathtests { | ||
191 | + plan9reservedpathtests[i].result = invalid | ||
192 | + } | ||
193 | + } | ||
194 | + tests := fspathtests | ||
195 | + tests = append(tests, winreservedpathtests...) | ||
196 | + tests = append(tests, plan9reservedpathtests...) | ||
197 | + for _, test := range tests { | ||
198 | + got, err := safefilepath.FromFS(test.path) | ||
199 | + if (got == "") != (err != nil) { | ||
200 | + t.Errorf(`FromFS(%q) = %q, %v; want "" only if err != nil`, test.path, got, err) | ||
201 | + } | ||
202 | + if got != test.result { | ||
203 | + t.Errorf("FromFS(%q) = %q, %v; want %q", test.path, got, err, test.result) | ||
204 | + } | ||
205 | + } | ||
206 | +} | ||
207 | + | ||
208 | +func canWriteFile(t *testing.T, name string) bool { | ||
209 | + path := filepath.Join(t.TempDir(), name) | ||
210 | + os.WriteFile(path, []byte("ok"), 0666) | ||
211 | + b, _ := os.ReadFile(path) | ||
212 | + return string(b) == "ok" | ||
213 | +} | ||
214 | diff --git a/src/internal/safefilepath/path_windows.go b/src/internal/safefilepath/path_windows.go | ||
215 | new file mode 100644 | ||
216 | index 0000000..909c150 | ||
217 | --- /dev/null | ||
218 | +++ b/src/internal/safefilepath/path_windows.go | ||
219 | @@ -0,0 +1,95 @@ | ||
220 | +// Copyright 2022 The Go Authors. All rights reserved. | ||
221 | +// Use of this source code is governed by a BSD-style | ||
222 | +// license that can be found in the LICENSE file. | ||
223 | + | ||
224 | +package safefilepath | ||
225 | + | ||
226 | +import ( | ||
227 | + "syscall" | ||
228 | + "unicode/utf8" | ||
229 | +) | ||
230 | + | ||
231 | +func fromFS(path string) (string, error) { | ||
232 | + if !utf8.ValidString(path) { | ||
233 | + return "", errInvalidPath | ||
234 | + } | ||
235 | + for len(path) > 1 && path[0] == '/' && path[1] == '/' { | ||
236 | + path = path[1:] | ||
237 | + } | ||
238 | + containsSlash := false | ||
239 | + for p := path; p != ""; { | ||
240 | + // Find the next path element. | ||
241 | + i := 0 | ||
242 | + dot := -1 | ||
243 | + for i < len(p) && p[i] != '/' { | ||
244 | + switch p[i] { | ||
245 | + case 0, '\\', ':': | ||
246 | + return "", errInvalidPath | ||
247 | + case '.': | ||
248 | + if dot < 0 { | ||
249 | + dot = i | ||
250 | + } | ||
251 | + } | ||
252 | + i++ | ||
253 | + } | ||
254 | + part := p[:i] | ||
255 | + if i < len(p) { | ||
256 | + containsSlash = true | ||
257 | + p = p[i+1:] | ||
258 | + } else { | ||
259 | + p = "" | ||
260 | + } | ||
261 | + // Trim the extension and look for a reserved name. | ||
262 | + base := part | ||
263 | + if dot >= 0 { | ||
264 | + base = part[:dot] | ||
265 | + } | ||
266 | + if isReservedName(base) { | ||
267 | + if dot < 0 { | ||
268 | + return "", errInvalidPath | ||
269 | + } | ||
270 | + // The path element is a reserved name with an extension. | ||
271 | + // Some Windows versions consider this a reserved name, | ||
272 | + // while others do not. Use FullPath to see if the name is | ||
273 | + // reserved. | ||
274 | + if p, _ := syscall.FullPath(part); len(p) >= 4 && p[:4] == `\\.\` { | ||
275 | + return "", errInvalidPath | ||
276 | + } | ||
277 | + } | ||
278 | + } | ||
279 | + if containsSlash { | ||
280 | + // We can't depend on strings, so substitute \ for / manually. | ||
281 | + buf := []byte(path) | ||
282 | + for i, b := range buf { | ||
283 | + if b == '/' { | ||
284 | + buf[i] = '\\' | ||
285 | + } | ||
286 | + } | ||
287 | + path = string(buf) | ||
288 | + } | ||
289 | + return path, nil | ||
290 | +} | ||
291 | + | ||
292 | +// isReservedName reports if name is a Windows reserved device name. | ||
293 | +// It does not detect names with an extension, which are also reserved on some Windows versions. | ||
294 | +// | ||
295 | +// For details, search for PRN in | ||
296 | +// https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file. | ||
297 | +func isReservedName(name string) bool { | ||
298 | + if 3 <= len(name) && len(name) <= 4 { | ||
299 | + switch string([]byte{toUpper(name[0]), toUpper(name[1]), toUpper(name[2])}) { | ||
300 | + case "CON", "PRN", "AUX", "NUL": | ||
301 | + return len(name) == 3 | ||
302 | + case "COM", "LPT": | ||
303 | + return len(name) == 4 && '1' <= name[3] && name[3] <= '9' | ||
304 | + } | ||
305 | + } | ||
306 | + return false | ||
307 | +} | ||
308 | + | ||
309 | +func toUpper(c byte) byte { | ||
310 | + if 'a' <= c && c <= 'z' { | ||
311 | + return c - ('a' - 'A') | ||
312 | + } | ||
313 | + return c | ||
314 | +} | ||
315 | diff --git a/src/net/http/fs.go b/src/net/http/fs.go | ||
316 | index 57e731e..43ee4b5 100644 | ||
317 | --- a/src/net/http/fs.go | ||
318 | +++ b/src/net/http/fs.go | ||
319 | @@ -9,6 +9,7 @@ package http | ||
320 | import ( | ||
321 | "errors" | ||
322 | "fmt" | ||
323 | + "internal/safefilepath" | ||
324 | "io" | ||
325 | "io/fs" | ||
326 | "mime" | ||
327 | @@ -69,14 +70,15 @@ func mapDirOpenError(originalErr error, name string) error { | ||
328 | // Open implements FileSystem using os.Open, opening files for reading rooted | ||
329 | // and relative to the directory d. | ||
330 | func (d Dir) Open(name string) (File, error) { | ||
331 | - if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) { | ||
332 | - return nil, errors.New("http: invalid character in file path") | ||
333 | + path, err := safefilepath.FromFS(path.Clean("/" + name)) | ||
334 | + if err != nil { | ||
335 | + return nil, errors.New("http: invalid or unsafe file path") | ||
336 | } | ||
337 | dir := string(d) | ||
338 | if dir == "" { | ||
339 | dir = "." | ||
340 | } | ||
341 | - fullName := filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name))) | ||
342 | + fullName := filepath.Join(dir, path) | ||
343 | f, err := os.Open(fullName) | ||
344 | if err != nil { | ||
345 | return nil, mapDirOpenError(err, fullName) | ||
346 | diff --git a/src/net/http/fs_test.go b/src/net/http/fs_test.go | ||
347 | index b42ade1..941448a 100644 | ||
348 | --- a/src/net/http/fs_test.go | ||
349 | +++ b/src/net/http/fs_test.go | ||
350 | @@ -648,6 +648,34 @@ func TestFileServerZeroByte(t *testing.T) { | ||
351 | } | ||
352 | } | ||
353 | |||
354 | +func TestFileServerNamesEscape(t *testing.T) { | ||
355 | + t.Run("h1", func(t *testing.T) { | ||
356 | + testFileServerNamesEscape(t, h1Mode) | ||
357 | + }) | ||
358 | + t.Run("h2", func(t *testing.T) { | ||
359 | + testFileServerNamesEscape(t, h2Mode) | ||
360 | + }) | ||
361 | +} | ||
362 | +func testFileServerNamesEscape(t *testing.T, h2 bool) { | ||
363 | + defer afterTest(t) | ||
364 | + ts := newClientServerTest(t, h2, FileServer(Dir("testdata"))).ts | ||
365 | + defer ts.Close() | ||
366 | + for _, path := range []string{ | ||
367 | + "/../testdata/file", | ||
368 | + "/NUL", // don't read from device files on Windows | ||
369 | + } { | ||
370 | + res, err := ts.Client().Get(ts.URL + path) | ||
371 | + if err != nil { | ||
372 | + t.Fatal(err) | ||
373 | + } | ||
374 | + res.Body.Close() | ||
375 | + if res.StatusCode < 400 || res.StatusCode > 599 { | ||
376 | + t.Errorf("Get(%q): got status %v, want 4xx or 5xx", path, res.StatusCode) | ||
377 | + } | ||
378 | + | ||
379 | + } | ||
380 | +} | ||
381 | + | ||
382 | type fakeFileInfo struct { | ||
383 | dir bool | ||
384 | basename string | ||
385 | diff --git a/src/os/file.go b/src/os/file.go | ||
386 | index e717f17..cb87158 100644 | ||
387 | --- a/src/os/file.go | ||
388 | +++ b/src/os/file.go | ||
389 | @@ -37,12 +37,12 @@ | ||
390 | // Note: The maximum number of concurrent operations on a File may be limited by | ||
391 | // the OS or the system. The number should be high, but exceeding it may degrade | ||
392 | // performance or cause other issues. | ||
393 | -// | ||
394 | package os | ||
395 | |||
396 | import ( | ||
397 | "errors" | ||
398 | "internal/poll" | ||
399 | + "internal/safefilepath" | ||
400 | "internal/testlog" | ||
401 | "internal/unsafeheader" | ||
402 | "io" | ||
403 | @@ -623,6 +623,8 @@ func isWindowsNulName(name string) bool { | ||
404 | // the /prefix tree, then using DirFS does not stop the access any more than using | ||
405 | // os.Open does. DirFS is therefore not a general substitute for a chroot-style security | ||
406 | // mechanism when the directory tree contains arbitrary content. | ||
407 | +// | ||
408 | +// The directory dir must not be "". | ||
409 | func DirFS(dir string) fs.FS { | ||
410 | return dirFS(dir) | ||
411 | } | ||
412 | @@ -641,10 +643,11 @@ func containsAny(s, chars string) bool { | ||
413 | type dirFS string | ||
414 | |||
415 | func (dir dirFS) Open(name string) (fs.File, error) { | ||
416 | - if !fs.ValidPath(name) || runtime.GOOS == "windows" && containsAny(name, `\:`) { | ||
417 | - return nil, &PathError{Op: "open", Path: name, Err: ErrInvalid} | ||
418 | + fullname, err := dir.join(name) | ||
419 | + if err != nil { | ||
420 | + return nil, &PathError{Op: "stat", Path: name, Err: err} | ||
421 | } | ||
422 | - f, err := Open(string(dir) + "/" + name) | ||
423 | + f, err := Open(fullname) | ||
424 | if err != nil { | ||
425 | return nil, err // nil fs.File | ||
426 | } | ||
427 | @@ -652,16 +655,35 @@ func (dir dirFS) Open(name string) (fs.File, error) { | ||
428 | } | ||
429 | |||
430 | func (dir dirFS) Stat(name string) (fs.FileInfo, error) { | ||
431 | - if !fs.ValidPath(name) || runtime.GOOS == "windows" && containsAny(name, `\:`) { | ||
432 | - return nil, &PathError{Op: "stat", Path: name, Err: ErrInvalid} | ||
433 | + fullname, err := dir.join(name) | ||
434 | + if err != nil { | ||
435 | + return nil, &PathError{Op: "stat", Path: name, Err: err} | ||
436 | } | ||
437 | - f, err := Stat(string(dir) + "/" + name) | ||
438 | + f, err := Stat(fullname) | ||
439 | if err != nil { | ||
440 | return nil, err | ||
441 | } | ||
442 | return f, nil | ||
443 | } | ||
444 | |||
445 | +// join returns the path for name in dir. | ||
446 | +func (dir dirFS) join(name string) (string, error) { | ||
447 | + if dir == "" { | ||
448 | + return "", errors.New("os: DirFS with empty root") | ||
449 | + } | ||
450 | + if !fs.ValidPath(name) { | ||
451 | + return "", ErrInvalid | ||
452 | + } | ||
453 | + name, err := safefilepath.FromFS(name) | ||
454 | + if err != nil { | ||
455 | + return "", ErrInvalid | ||
456 | + } | ||
457 | + if IsPathSeparator(dir[len(dir)-1]) { | ||
458 | + return string(dir) + name, nil | ||
459 | + } | ||
460 | + return string(dir) + string(PathSeparator) + name, nil | ||
461 | +} | ||
462 | + | ||
463 | // ReadFile reads the named file and returns the contents. | ||
464 | // A successful call returns err == nil, not err == EOF. | ||
465 | // Because ReadFile reads the whole file, it does not treat an EOF from Read | ||
466 | diff --git a/src/os/os_test.go b/src/os/os_test.go | ||
467 | index 506f1fb..be269bb 100644 | ||
468 | --- a/src/os/os_test.go | ||
469 | +++ b/src/os/os_test.go | ||
470 | @@ -2702,6 +2702,44 @@ func TestDirFS(t *testing.T) { | ||
471 | if err == nil { | ||
472 | t.Fatalf(`Open testdata\dirfs succeeded`) | ||
473 | } | ||
474 | + | ||
475 | + // Test that Open does not open Windows device files. | ||
476 | + _, err = d.Open(`NUL`) | ||
477 | + if err == nil { | ||
478 | + t.Errorf(`Open NUL succeeded`) | ||
479 | + } | ||
480 | +} | ||
481 | + | ||
482 | +func TestDirFSRootDir(t *testing.T) { | ||
483 | + cwd, err := os.Getwd() | ||
484 | + if err != nil { | ||
485 | + t.Fatal(err) | ||
486 | + } | ||
487 | + cwd = cwd[len(filepath.VolumeName(cwd)):] // trim volume prefix (C:) on Windows | ||
488 | + cwd = filepath.ToSlash(cwd) // convert \ to / | ||
489 | + cwd = strings.TrimPrefix(cwd, "/") // trim leading / | ||
490 | + | ||
491 | + // Test that Open can open a path starting at /. | ||
492 | + d := DirFS("/") | ||
493 | + f, err := d.Open(cwd + "/testdata/dirfs/a") | ||
494 | + if err != nil { | ||
495 | + t.Fatal(err) | ||
496 | + } | ||
497 | + f.Close() | ||
498 | +} | ||
499 | + | ||
500 | +func TestDirFSEmptyDir(t *testing.T) { | ||
501 | + d := DirFS("") | ||
502 | + cwd, _ := os.Getwd() | ||
503 | + for _, path := range []string{ | ||
504 | + "testdata/dirfs/a", // not DirFS(".") | ||
505 | + filepath.ToSlash(cwd) + "/testdata/dirfs/a", // not DirFS("/") | ||
506 | + } { | ||
507 | + _, err := d.Open(path) | ||
508 | + if err == nil { | ||
509 | + t.Fatalf(`DirFS("").Open(%q) succeeded`, path) | ||
510 | + } | ||
511 | + } | ||
512 | } | ||
513 | |||
514 | func TestDirFSPathsValid(t *testing.T) { | ||