xref: /OK3568_Linux_fs/yocto/poky/meta/recipes-devtools/go/go-1.18/CVE-2022-41720.patch (revision 4882a59341e53eb6f0b4789bf948001014eff981)
1*4882a593SmuzhiyunFrom f8896a97a0630b0f2f8c488310147f7f20b3ec7d Mon Sep 17 00:00:00 2001
2*4882a593SmuzhiyunFrom: Damien Neil <dneil@google.com>
3*4882a593SmuzhiyunDate: Thu, 10 Nov 2022 12:16:27 -0800
4*4882a593SmuzhiyunSubject: [PATCH] os, net/http: avoid escapes from os.DirFS and http.Dir on
5*4882a593Smuzhiyun Windows
6*4882a593Smuzhiyun
7*4882a593SmuzhiyunDo not permit access to Windows reserved device names (NUL, COM1, etc.)
8*4882a593Smuzhiyunvia os.DirFS and http.Dir filesystems.
9*4882a593Smuzhiyun
10*4882a593SmuzhiyunAvoid escapes from os.DirFS(`\`) on Windows. DirFS would join the
11*4882a593Smuzhiyunthe root to the relative path with a path separator, making
12*4882a593Smuzhiyunos.DirFS(`\`).Open(`/foo/bar`) open the path `\\foo\bar`, which is
13*4882a593Smuzhiyuna UNC name. Not only does this not open the intended file, but permits
14*4882a593Smuzhiyunreference to any file on the system rather than only files on the
15*4882a593Smuzhiyuncurrent drive.
16*4882a593Smuzhiyun
17*4882a593SmuzhiyunMake os.DirFS("") invalid, with all file access failing. Previously,
18*4882a593Smuzhiyuna root of "" was interpreted as "/", which is surprising and probably
19*4882a593Smuzhiyununintentional.
20*4882a593Smuzhiyun
21*4882a593SmuzhiyunFixes CVE-2022-41720.
22*4882a593SmuzhiyunFixes #56694.
23*4882a593Smuzhiyun
24*4882a593SmuzhiyunChange-Id: I275b5fa391e6ad7404309ea98ccc97405942e0f0
25*4882a593SmuzhiyunReviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1663832
26*4882a593SmuzhiyunReviewed-by: Julie Qiu <julieqiu@google.com>
27*4882a593SmuzhiyunReviewed-by: Tatiana Bradley <tatianabradley@google.com>
28*4882a593SmuzhiyunReviewed-on: https://go-review.googlesource.com/c/go/+/455360
29*4882a593SmuzhiyunReviewed-by: Michael Pratt <mpratt@google.com>
30*4882a593SmuzhiyunTryBot-Result: Gopher Robot <gobot@golang.org>
31*4882a593SmuzhiyunRun-TryBot: Jenny Rakoczy <jenny@golang.org>
32*4882a593Smuzhiyun
33*4882a593SmuzhiyunCVE: CVE-2022-41720
34*4882a593SmuzhiyunUpstream-Status: Backport [7013a4f5f816af62033ad63dd06b77c30d7a62a7]
35*4882a593SmuzhiyunSigned-off-by: Sakib Sajal <sakib.sajal@windriver.com>
36*4882a593Smuzhiyun---
37*4882a593Smuzhiyun src/go/build/deps_test.go                 |  1 +
38*4882a593Smuzhiyun src/internal/safefilepath/path.go         | 21 +++++
39*4882a593Smuzhiyun src/internal/safefilepath/path_other.go   | 23 ++++++
40*4882a593Smuzhiyun src/internal/safefilepath/path_test.go    | 88 +++++++++++++++++++++
41*4882a593Smuzhiyun src/internal/safefilepath/path_windows.go | 95 +++++++++++++++++++++++
42*4882a593Smuzhiyun src/net/http/fs.go                        |  8 +-
43*4882a593Smuzhiyun src/net/http/fs_test.go                   | 28 +++++++
44*4882a593Smuzhiyun src/os/file.go                            | 36 +++++++--
45*4882a593Smuzhiyun src/os/os_test.go                         | 38 +++++++++
46*4882a593Smuzhiyun 9 files changed, 328 insertions(+), 10 deletions(-)
47*4882a593Smuzhiyun create mode 100644 src/internal/safefilepath/path.go
48*4882a593Smuzhiyun create mode 100644 src/internal/safefilepath/path_other.go
49*4882a593Smuzhiyun create mode 100644 src/internal/safefilepath/path_test.go
50*4882a593Smuzhiyun create mode 100644 src/internal/safefilepath/path_windows.go
51*4882a593Smuzhiyun
52*4882a593Smuzhiyundiff --git a/src/go/build/deps_test.go b/src/go/build/deps_test.go
53*4882a593Smuzhiyunindex 45e2f25..dc3bb8c 100644
54*4882a593Smuzhiyun--- a/src/go/build/deps_test.go
55*4882a593Smuzhiyun+++ b/src/go/build/deps_test.go
56*4882a593Smuzhiyun@@ -165,6 +165,7 @@ var depsRules = `
57*4882a593Smuzhiyun 	io/fs
58*4882a593Smuzhiyun 	< internal/testlog
59*4882a593Smuzhiyun 	< internal/poll
60*4882a593Smuzhiyun+	< internal/safefilepath
61*4882a593Smuzhiyun 	< os
62*4882a593Smuzhiyun 	< os/signal;
63*4882a593Smuzhiyun
64*4882a593Smuzhiyundiff --git a/src/internal/safefilepath/path.go b/src/internal/safefilepath/path.go
65*4882a593Smuzhiyunnew file mode 100644
66*4882a593Smuzhiyunindex 0000000..0f0a270
67*4882a593Smuzhiyun--- /dev/null
68*4882a593Smuzhiyun+++ b/src/internal/safefilepath/path.go
69*4882a593Smuzhiyun@@ -0,0 +1,21 @@
70*4882a593Smuzhiyun+// Copyright 2022 The Go Authors. All rights reserved.
71*4882a593Smuzhiyun+// Use of this source code is governed by a BSD-style
72*4882a593Smuzhiyun+// license that can be found in the LICENSE file.
73*4882a593Smuzhiyun+
74*4882a593Smuzhiyun+// Package safefilepath manipulates operating-system file paths.
75*4882a593Smuzhiyun+package safefilepath
76*4882a593Smuzhiyun+
77*4882a593Smuzhiyun+import (
78*4882a593Smuzhiyun+	"errors"
79*4882a593Smuzhiyun+)
80*4882a593Smuzhiyun+
81*4882a593Smuzhiyun+var errInvalidPath = errors.New("invalid path")
82*4882a593Smuzhiyun+
83*4882a593Smuzhiyun+// FromFS converts a slash-separated path into an operating-system path.
84*4882a593Smuzhiyun+//
85*4882a593Smuzhiyun+// FromFS returns an error if the path cannot be represented by the operating
86*4882a593Smuzhiyun+// system. For example, paths containing '\' and ':' characters are rejected
87*4882a593Smuzhiyun+// on Windows.
88*4882a593Smuzhiyun+func FromFS(path string) (string, error) {
89*4882a593Smuzhiyun+	return fromFS(path)
90*4882a593Smuzhiyun+}
91*4882a593Smuzhiyundiff --git a/src/internal/safefilepath/path_other.go b/src/internal/safefilepath/path_other.go
92*4882a593Smuzhiyunnew file mode 100644
93*4882a593Smuzhiyunindex 0000000..f93da18
94*4882a593Smuzhiyun--- /dev/null
95*4882a593Smuzhiyun+++ b/src/internal/safefilepath/path_other.go
96*4882a593Smuzhiyun@@ -0,0 +1,23 @@
97*4882a593Smuzhiyun+// Copyright 2022 The Go Authors. All rights reserved.
98*4882a593Smuzhiyun+// Use of this source code is governed by a BSD-style
99*4882a593Smuzhiyun+// license that can be found in the LICENSE file.
100*4882a593Smuzhiyun+
101*4882a593Smuzhiyun+//go:build !windows
102*4882a593Smuzhiyun+
103*4882a593Smuzhiyun+package safefilepath
104*4882a593Smuzhiyun+
105*4882a593Smuzhiyun+import "runtime"
106*4882a593Smuzhiyun+
107*4882a593Smuzhiyun+func fromFS(path string) (string, error) {
108*4882a593Smuzhiyun+	if runtime.GOOS == "plan9" {
109*4882a593Smuzhiyun+		if len(path) > 0 && path[0] == '#' {
110*4882a593Smuzhiyun+			return path, errInvalidPath
111*4882a593Smuzhiyun+		}
112*4882a593Smuzhiyun+	}
113*4882a593Smuzhiyun+	for i := range path {
114*4882a593Smuzhiyun+		if path[i] == 0 {
115*4882a593Smuzhiyun+			return "", errInvalidPath
116*4882a593Smuzhiyun+		}
117*4882a593Smuzhiyun+	}
118*4882a593Smuzhiyun+	return path, nil
119*4882a593Smuzhiyun+}
120*4882a593Smuzhiyundiff --git a/src/internal/safefilepath/path_test.go b/src/internal/safefilepath/path_test.go
121*4882a593Smuzhiyunnew file mode 100644
122*4882a593Smuzhiyunindex 0000000..dc662c1
123*4882a593Smuzhiyun--- /dev/null
124*4882a593Smuzhiyun+++ b/src/internal/safefilepath/path_test.go
125*4882a593Smuzhiyun@@ -0,0 +1,88 @@
126*4882a593Smuzhiyun+// Copyright 2022 The Go Authors. All rights reserved.
127*4882a593Smuzhiyun+// Use of this source code is governed by a BSD-style
128*4882a593Smuzhiyun+// license that can be found in the LICENSE file.
129*4882a593Smuzhiyun+
130*4882a593Smuzhiyun+package safefilepath_test
131*4882a593Smuzhiyun+
132*4882a593Smuzhiyun+import (
133*4882a593Smuzhiyun+	"internal/safefilepath"
134*4882a593Smuzhiyun+	"os"
135*4882a593Smuzhiyun+	"path/filepath"
136*4882a593Smuzhiyun+	"runtime"
137*4882a593Smuzhiyun+	"testing"
138*4882a593Smuzhiyun+)
139*4882a593Smuzhiyun+
140*4882a593Smuzhiyun+type PathTest struct {
141*4882a593Smuzhiyun+	path, result string
142*4882a593Smuzhiyun+}
143*4882a593Smuzhiyun+
144*4882a593Smuzhiyun+const invalid = ""
145*4882a593Smuzhiyun+
146*4882a593Smuzhiyun+var fspathtests = []PathTest{
147*4882a593Smuzhiyun+	{".", "."},
148*4882a593Smuzhiyun+	{"/a/b/c", "/a/b/c"},
149*4882a593Smuzhiyun+	{"a\x00b", invalid},
150*4882a593Smuzhiyun+}
151*4882a593Smuzhiyun+
152*4882a593Smuzhiyun+var winreservedpathtests = []PathTest{
153*4882a593Smuzhiyun+	{`a\b`, `a\b`},
154*4882a593Smuzhiyun+	{`a:b`, `a:b`},
155*4882a593Smuzhiyun+	{`a/b:c`, `a/b:c`},
156*4882a593Smuzhiyun+	{`NUL`, `NUL`},
157*4882a593Smuzhiyun+	{`./com1`, `./com1`},
158*4882a593Smuzhiyun+	{`a/nul/b`, `a/nul/b`},
159*4882a593Smuzhiyun+}
160*4882a593Smuzhiyun+
161*4882a593Smuzhiyun+// Whether a reserved name with an extension is reserved or not varies by
162*4882a593Smuzhiyun+// Windows version.
163*4882a593Smuzhiyun+var winreservedextpathtests = []PathTest{
164*4882a593Smuzhiyun+	{"nul.txt", "nul.txt"},
165*4882a593Smuzhiyun+	{"a/nul.txt/b", "a/nul.txt/b"},
166*4882a593Smuzhiyun+}
167*4882a593Smuzhiyun+
168*4882a593Smuzhiyun+var plan9reservedpathtests = []PathTest{
169*4882a593Smuzhiyun+	{`#c`, `#c`},
170*4882a593Smuzhiyun+}
171*4882a593Smuzhiyun+
172*4882a593Smuzhiyun+func TestFromFS(t *testing.T) {
173*4882a593Smuzhiyun+	switch runtime.GOOS {
174*4882a593Smuzhiyun+	case "windows":
175*4882a593Smuzhiyun+		if canWriteFile(t, "NUL") {
176*4882a593Smuzhiyun+			t.Errorf("can unexpectedly write a file named NUL on Windows")
177*4882a593Smuzhiyun+		}
178*4882a593Smuzhiyun+		if canWriteFile(t, "nul.txt") {
179*4882a593Smuzhiyun+			fspathtests = append(fspathtests, winreservedextpathtests...)
180*4882a593Smuzhiyun+		} else {
181*4882a593Smuzhiyun+			winreservedpathtests = append(winreservedpathtests, winreservedextpathtests...)
182*4882a593Smuzhiyun+		}
183*4882a593Smuzhiyun+		for i := range winreservedpathtests {
184*4882a593Smuzhiyun+			winreservedpathtests[i].result = invalid
185*4882a593Smuzhiyun+		}
186*4882a593Smuzhiyun+		for i := range fspathtests {
187*4882a593Smuzhiyun+			fspathtests[i].result = filepath.FromSlash(fspathtests[i].result)
188*4882a593Smuzhiyun+		}
189*4882a593Smuzhiyun+	case "plan9":
190*4882a593Smuzhiyun+		for i := range plan9reservedpathtests {
191*4882a593Smuzhiyun+			plan9reservedpathtests[i].result = invalid
192*4882a593Smuzhiyun+		}
193*4882a593Smuzhiyun+	}
194*4882a593Smuzhiyun+	tests := fspathtests
195*4882a593Smuzhiyun+	tests = append(tests, winreservedpathtests...)
196*4882a593Smuzhiyun+	tests = append(tests, plan9reservedpathtests...)
197*4882a593Smuzhiyun+	for _, test := range tests {
198*4882a593Smuzhiyun+		got, err := safefilepath.FromFS(test.path)
199*4882a593Smuzhiyun+		if (got == "") != (err != nil) {
200*4882a593Smuzhiyun+			t.Errorf(`FromFS(%q) = %q, %v; want "" only if err != nil`, test.path, got, err)
201*4882a593Smuzhiyun+		}
202*4882a593Smuzhiyun+		if got != test.result {
203*4882a593Smuzhiyun+			t.Errorf("FromFS(%q) = %q, %v; want %q", test.path, got, err, test.result)
204*4882a593Smuzhiyun+		}
205*4882a593Smuzhiyun+	}
206*4882a593Smuzhiyun+}
207*4882a593Smuzhiyun+
208*4882a593Smuzhiyun+func canWriteFile(t *testing.T, name string) bool {
209*4882a593Smuzhiyun+	path := filepath.Join(t.TempDir(), name)
210*4882a593Smuzhiyun+	os.WriteFile(path, []byte("ok"), 0666)
211*4882a593Smuzhiyun+	b, _ := os.ReadFile(path)
212*4882a593Smuzhiyun+	return string(b) == "ok"
213*4882a593Smuzhiyun+}
214*4882a593Smuzhiyundiff --git a/src/internal/safefilepath/path_windows.go b/src/internal/safefilepath/path_windows.go
215*4882a593Smuzhiyunnew file mode 100644
216*4882a593Smuzhiyunindex 0000000..909c150
217*4882a593Smuzhiyun--- /dev/null
218*4882a593Smuzhiyun+++ b/src/internal/safefilepath/path_windows.go
219*4882a593Smuzhiyun@@ -0,0 +1,95 @@
220*4882a593Smuzhiyun+// Copyright 2022 The Go Authors. All rights reserved.
221*4882a593Smuzhiyun+// Use of this source code is governed by a BSD-style
222*4882a593Smuzhiyun+// license that can be found in the LICENSE file.
223*4882a593Smuzhiyun+
224*4882a593Smuzhiyun+package safefilepath
225*4882a593Smuzhiyun+
226*4882a593Smuzhiyun+import (
227*4882a593Smuzhiyun+	"syscall"
228*4882a593Smuzhiyun+	"unicode/utf8"
229*4882a593Smuzhiyun+)
230*4882a593Smuzhiyun+
231*4882a593Smuzhiyun+func fromFS(path string) (string, error) {
232*4882a593Smuzhiyun+	if !utf8.ValidString(path) {
233*4882a593Smuzhiyun+		return "", errInvalidPath
234*4882a593Smuzhiyun+	}
235*4882a593Smuzhiyun+	for len(path) > 1 && path[0] == '/' && path[1] == '/' {
236*4882a593Smuzhiyun+		path = path[1:]
237*4882a593Smuzhiyun+	}
238*4882a593Smuzhiyun+	containsSlash := false
239*4882a593Smuzhiyun+	for p := path; p != ""; {
240*4882a593Smuzhiyun+		// Find the next path element.
241*4882a593Smuzhiyun+		i := 0
242*4882a593Smuzhiyun+		dot := -1
243*4882a593Smuzhiyun+		for i < len(p) && p[i] != '/' {
244*4882a593Smuzhiyun+			switch p[i] {
245*4882a593Smuzhiyun+			case 0, '\\', ':':
246*4882a593Smuzhiyun+				return "", errInvalidPath
247*4882a593Smuzhiyun+			case '.':
248*4882a593Smuzhiyun+				if dot < 0 {
249*4882a593Smuzhiyun+					dot = i
250*4882a593Smuzhiyun+				}
251*4882a593Smuzhiyun+			}
252*4882a593Smuzhiyun+			i++
253*4882a593Smuzhiyun+		}
254*4882a593Smuzhiyun+		part := p[:i]
255*4882a593Smuzhiyun+		if i < len(p) {
256*4882a593Smuzhiyun+			containsSlash = true
257*4882a593Smuzhiyun+			p = p[i+1:]
258*4882a593Smuzhiyun+		} else {
259*4882a593Smuzhiyun+			p = ""
260*4882a593Smuzhiyun+		}
261*4882a593Smuzhiyun+		// Trim the extension and look for a reserved name.
262*4882a593Smuzhiyun+		base := part
263*4882a593Smuzhiyun+		if dot >= 0 {
264*4882a593Smuzhiyun+			base = part[:dot]
265*4882a593Smuzhiyun+		}
266*4882a593Smuzhiyun+		if isReservedName(base) {
267*4882a593Smuzhiyun+			if dot < 0 {
268*4882a593Smuzhiyun+				return "", errInvalidPath
269*4882a593Smuzhiyun+			}
270*4882a593Smuzhiyun+			// The path element is a reserved name with an extension.
271*4882a593Smuzhiyun+			// Some Windows versions consider this a reserved name,
272*4882a593Smuzhiyun+			// while others do not. Use FullPath to see if the name is
273*4882a593Smuzhiyun+			// reserved.
274*4882a593Smuzhiyun+			if p, _ := syscall.FullPath(part); len(p) >= 4 && p[:4] == `\\.\` {
275*4882a593Smuzhiyun+				return "", errInvalidPath
276*4882a593Smuzhiyun+			}
277*4882a593Smuzhiyun+		}
278*4882a593Smuzhiyun+	}
279*4882a593Smuzhiyun+	if containsSlash {
280*4882a593Smuzhiyun+		// We can't depend on strings, so substitute \ for / manually.
281*4882a593Smuzhiyun+		buf := []byte(path)
282*4882a593Smuzhiyun+		for i, b := range buf {
283*4882a593Smuzhiyun+			if b == '/' {
284*4882a593Smuzhiyun+				buf[i] = '\\'
285*4882a593Smuzhiyun+			}
286*4882a593Smuzhiyun+		}
287*4882a593Smuzhiyun+		path = string(buf)
288*4882a593Smuzhiyun+	}
289*4882a593Smuzhiyun+	return path, nil
290*4882a593Smuzhiyun+}
291*4882a593Smuzhiyun+
292*4882a593Smuzhiyun+// isReservedName reports if name is a Windows reserved device name.
293*4882a593Smuzhiyun+// It does not detect names with an extension, which are also reserved on some Windows versions.
294*4882a593Smuzhiyun+//
295*4882a593Smuzhiyun+// For details, search for PRN in
296*4882a593Smuzhiyun+// https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file.
297*4882a593Smuzhiyun+func isReservedName(name string) bool {
298*4882a593Smuzhiyun+	if 3 <= len(name) && len(name) <= 4 {
299*4882a593Smuzhiyun+		switch string([]byte{toUpper(name[0]), toUpper(name[1]), toUpper(name[2])}) {
300*4882a593Smuzhiyun+		case "CON", "PRN", "AUX", "NUL":
301*4882a593Smuzhiyun+			return len(name) == 3
302*4882a593Smuzhiyun+		case "COM", "LPT":
303*4882a593Smuzhiyun+			return len(name) == 4 && '1' <= name[3] && name[3] <= '9'
304*4882a593Smuzhiyun+		}
305*4882a593Smuzhiyun+	}
306*4882a593Smuzhiyun+	return false
307*4882a593Smuzhiyun+}
308*4882a593Smuzhiyun+
309*4882a593Smuzhiyun+func toUpper(c byte) byte {
310*4882a593Smuzhiyun+	if 'a' <= c && c <= 'z' {
311*4882a593Smuzhiyun+		return c - ('a' - 'A')
312*4882a593Smuzhiyun+	}
313*4882a593Smuzhiyun+	return c
314*4882a593Smuzhiyun+}
315*4882a593Smuzhiyundiff --git a/src/net/http/fs.go b/src/net/http/fs.go
316*4882a593Smuzhiyunindex 57e731e..43ee4b5 100644
317*4882a593Smuzhiyun--- a/src/net/http/fs.go
318*4882a593Smuzhiyun+++ b/src/net/http/fs.go
319*4882a593Smuzhiyun@@ -9,6 +9,7 @@ package http
320*4882a593Smuzhiyun import (
321*4882a593Smuzhiyun 	"errors"
322*4882a593Smuzhiyun 	"fmt"
323*4882a593Smuzhiyun+	"internal/safefilepath"
324*4882a593Smuzhiyun 	"io"
325*4882a593Smuzhiyun 	"io/fs"
326*4882a593Smuzhiyun 	"mime"
327*4882a593Smuzhiyun@@ -69,14 +70,15 @@ func mapDirOpenError(originalErr error, name string) error {
328*4882a593Smuzhiyun // Open implements FileSystem using os.Open, opening files for reading rooted
329*4882a593Smuzhiyun // and relative to the directory d.
330*4882a593Smuzhiyun func (d Dir) Open(name string) (File, error) {
331*4882a593Smuzhiyun-	if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) {
332*4882a593Smuzhiyun-		return nil, errors.New("http: invalid character in file path")
333*4882a593Smuzhiyun+	path, err := safefilepath.FromFS(path.Clean("/" + name))
334*4882a593Smuzhiyun+	if err != nil {
335*4882a593Smuzhiyun+		return nil, errors.New("http: invalid or unsafe file path")
336*4882a593Smuzhiyun 	}
337*4882a593Smuzhiyun 	dir := string(d)
338*4882a593Smuzhiyun 	if dir == "" {
339*4882a593Smuzhiyun 		dir = "."
340*4882a593Smuzhiyun 	}
341*4882a593Smuzhiyun-	fullName := filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name)))
342*4882a593Smuzhiyun+	fullName := filepath.Join(dir, path)
343*4882a593Smuzhiyun 	f, err := os.Open(fullName)
344*4882a593Smuzhiyun 	if err != nil {
345*4882a593Smuzhiyun 		return nil, mapDirOpenError(err, fullName)
346*4882a593Smuzhiyundiff --git a/src/net/http/fs_test.go b/src/net/http/fs_test.go
347*4882a593Smuzhiyunindex b42ade1..941448a 100644
348*4882a593Smuzhiyun--- a/src/net/http/fs_test.go
349*4882a593Smuzhiyun+++ b/src/net/http/fs_test.go
350*4882a593Smuzhiyun@@ -648,6 +648,34 @@ func TestFileServerZeroByte(t *testing.T) {
351*4882a593Smuzhiyun 	}
352*4882a593Smuzhiyun }
353*4882a593Smuzhiyun
354*4882a593Smuzhiyun+func TestFileServerNamesEscape(t *testing.T) {
355*4882a593Smuzhiyun+	t.Run("h1", func(t *testing.T) {
356*4882a593Smuzhiyun+		testFileServerNamesEscape(t, h1Mode)
357*4882a593Smuzhiyun+	})
358*4882a593Smuzhiyun+	t.Run("h2", func(t *testing.T) {
359*4882a593Smuzhiyun+		testFileServerNamesEscape(t, h2Mode)
360*4882a593Smuzhiyun+	})
361*4882a593Smuzhiyun+}
362*4882a593Smuzhiyun+func testFileServerNamesEscape(t *testing.T, h2 bool) {
363*4882a593Smuzhiyun+	defer afterTest(t)
364*4882a593Smuzhiyun+	ts := newClientServerTest(t, h2, FileServer(Dir("testdata"))).ts
365*4882a593Smuzhiyun+	defer ts.Close()
366*4882a593Smuzhiyun+	for _, path := range []string{
367*4882a593Smuzhiyun+		"/../testdata/file",
368*4882a593Smuzhiyun+		"/NUL", // don't read from device files on Windows
369*4882a593Smuzhiyun+	} {
370*4882a593Smuzhiyun+		res, err := ts.Client().Get(ts.URL + path)
371*4882a593Smuzhiyun+		if err != nil {
372*4882a593Smuzhiyun+			t.Fatal(err)
373*4882a593Smuzhiyun+		}
374*4882a593Smuzhiyun+		res.Body.Close()
375*4882a593Smuzhiyun+		if res.StatusCode < 400 || res.StatusCode > 599 {
376*4882a593Smuzhiyun+			t.Errorf("Get(%q): got status %v, want 4xx or 5xx", path, res.StatusCode)
377*4882a593Smuzhiyun+		}
378*4882a593Smuzhiyun+
379*4882a593Smuzhiyun+	}
380*4882a593Smuzhiyun+}
381*4882a593Smuzhiyun+
382*4882a593Smuzhiyun type fakeFileInfo struct {
383*4882a593Smuzhiyun 	dir      bool
384*4882a593Smuzhiyun 	basename string
385*4882a593Smuzhiyundiff --git a/src/os/file.go b/src/os/file.go
386*4882a593Smuzhiyunindex e717f17..cb87158 100644
387*4882a593Smuzhiyun--- a/src/os/file.go
388*4882a593Smuzhiyun+++ b/src/os/file.go
389*4882a593Smuzhiyun@@ -37,12 +37,12 @@
390*4882a593Smuzhiyun // Note: The maximum number of concurrent operations on a File may be limited by
391*4882a593Smuzhiyun // the OS or the system. The number should be high, but exceeding it may degrade
392*4882a593Smuzhiyun // performance or cause other issues.
393*4882a593Smuzhiyun-//
394*4882a593Smuzhiyun package os
395*4882a593Smuzhiyun
396*4882a593Smuzhiyun import (
397*4882a593Smuzhiyun 	"errors"
398*4882a593Smuzhiyun 	"internal/poll"
399*4882a593Smuzhiyun+	"internal/safefilepath"
400*4882a593Smuzhiyun 	"internal/testlog"
401*4882a593Smuzhiyun 	"internal/unsafeheader"
402*4882a593Smuzhiyun 	"io"
403*4882a593Smuzhiyun@@ -623,6 +623,8 @@ func isWindowsNulName(name string) bool {
404*4882a593Smuzhiyun // the /prefix tree, then using DirFS does not stop the access any more than using
405*4882a593Smuzhiyun // os.Open does. DirFS is therefore not a general substitute for a chroot-style security
406*4882a593Smuzhiyun // mechanism when the directory tree contains arbitrary content.
407*4882a593Smuzhiyun+//
408*4882a593Smuzhiyun+// The directory dir must not be "".
409*4882a593Smuzhiyun func DirFS(dir string) fs.FS {
410*4882a593Smuzhiyun 	return dirFS(dir)
411*4882a593Smuzhiyun }
412*4882a593Smuzhiyun@@ -641,10 +643,11 @@ func containsAny(s, chars string) bool {
413*4882a593Smuzhiyun type dirFS string
414*4882a593Smuzhiyun
415*4882a593Smuzhiyun func (dir dirFS) Open(name string) (fs.File, error) {
416*4882a593Smuzhiyun-	if !fs.ValidPath(name) || runtime.GOOS == "windows" && containsAny(name, `\:`) {
417*4882a593Smuzhiyun-		return nil, &PathError{Op: "open", Path: name, Err: ErrInvalid}
418*4882a593Smuzhiyun+	fullname, err := dir.join(name)
419*4882a593Smuzhiyun+	if err != nil {
420*4882a593Smuzhiyun+		return nil, &PathError{Op: "stat", Path: name, Err: err}
421*4882a593Smuzhiyun 	}
422*4882a593Smuzhiyun-	f, err := Open(string(dir) + "/" + name)
423*4882a593Smuzhiyun+	f, err := Open(fullname)
424*4882a593Smuzhiyun 	if err != nil {
425*4882a593Smuzhiyun 		return nil, err // nil fs.File
426*4882a593Smuzhiyun 	}
427*4882a593Smuzhiyun@@ -652,16 +655,35 @@ func (dir dirFS) Open(name string) (fs.File, error) {
428*4882a593Smuzhiyun }
429*4882a593Smuzhiyun
430*4882a593Smuzhiyun func (dir dirFS) Stat(name string) (fs.FileInfo, error) {
431*4882a593Smuzhiyun-	if !fs.ValidPath(name) || runtime.GOOS == "windows" && containsAny(name, `\:`) {
432*4882a593Smuzhiyun-		return nil, &PathError{Op: "stat", Path: name, Err: ErrInvalid}
433*4882a593Smuzhiyun+	fullname, err := dir.join(name)
434*4882a593Smuzhiyun+	if err != nil {
435*4882a593Smuzhiyun+		return nil, &PathError{Op: "stat", Path: name, Err: err}
436*4882a593Smuzhiyun 	}
437*4882a593Smuzhiyun-	f, err := Stat(string(dir) + "/" + name)
438*4882a593Smuzhiyun+	f, err := Stat(fullname)
439*4882a593Smuzhiyun 	if err != nil {
440*4882a593Smuzhiyun 		return nil, err
441*4882a593Smuzhiyun 	}
442*4882a593Smuzhiyun 	return f, nil
443*4882a593Smuzhiyun }
444*4882a593Smuzhiyun
445*4882a593Smuzhiyun+// join returns the path for name in dir.
446*4882a593Smuzhiyun+func (dir dirFS) join(name string) (string, error) {
447*4882a593Smuzhiyun+	if dir == "" {
448*4882a593Smuzhiyun+		return "", errors.New("os: DirFS with empty root")
449*4882a593Smuzhiyun+	}
450*4882a593Smuzhiyun+	if !fs.ValidPath(name) {
451*4882a593Smuzhiyun+		return "", ErrInvalid
452*4882a593Smuzhiyun+	}
453*4882a593Smuzhiyun+	name, err := safefilepath.FromFS(name)
454*4882a593Smuzhiyun+	if err != nil {
455*4882a593Smuzhiyun+		return "", ErrInvalid
456*4882a593Smuzhiyun+	}
457*4882a593Smuzhiyun+	if IsPathSeparator(dir[len(dir)-1]) {
458*4882a593Smuzhiyun+		return string(dir) + name, nil
459*4882a593Smuzhiyun+	}
460*4882a593Smuzhiyun+	return string(dir) + string(PathSeparator) + name, nil
461*4882a593Smuzhiyun+}
462*4882a593Smuzhiyun+
463*4882a593Smuzhiyun // ReadFile reads the named file and returns the contents.
464*4882a593Smuzhiyun // A successful call returns err == nil, not err == EOF.
465*4882a593Smuzhiyun // Because ReadFile reads the whole file, it does not treat an EOF from Read
466*4882a593Smuzhiyundiff --git a/src/os/os_test.go b/src/os/os_test.go
467*4882a593Smuzhiyunindex 506f1fb..be269bb 100644
468*4882a593Smuzhiyun--- a/src/os/os_test.go
469*4882a593Smuzhiyun+++ b/src/os/os_test.go
470*4882a593Smuzhiyun@@ -2702,6 +2702,44 @@ func TestDirFS(t *testing.T) {
471*4882a593Smuzhiyun 	if err == nil {
472*4882a593Smuzhiyun 		t.Fatalf(`Open testdata\dirfs succeeded`)
473*4882a593Smuzhiyun 	}
474*4882a593Smuzhiyun+
475*4882a593Smuzhiyun+	// Test that Open does not open Windows device files.
476*4882a593Smuzhiyun+	_, err = d.Open(`NUL`)
477*4882a593Smuzhiyun+	if err == nil {
478*4882a593Smuzhiyun+		t.Errorf(`Open NUL succeeded`)
479*4882a593Smuzhiyun+	}
480*4882a593Smuzhiyun+}
481*4882a593Smuzhiyun+
482*4882a593Smuzhiyun+func TestDirFSRootDir(t *testing.T) {
483*4882a593Smuzhiyun+	cwd, err := os.Getwd()
484*4882a593Smuzhiyun+	if err != nil {
485*4882a593Smuzhiyun+		t.Fatal(err)
486*4882a593Smuzhiyun+	}
487*4882a593Smuzhiyun+	cwd = cwd[len(filepath.VolumeName(cwd)):] // trim volume prefix (C:) on Windows
488*4882a593Smuzhiyun+	cwd = filepath.ToSlash(cwd)               // convert \ to /
489*4882a593Smuzhiyun+	cwd = strings.TrimPrefix(cwd, "/")        // trim leading /
490*4882a593Smuzhiyun+
491*4882a593Smuzhiyun+	// Test that Open can open a path starting at /.
492*4882a593Smuzhiyun+	d := DirFS("/")
493*4882a593Smuzhiyun+	f, err := d.Open(cwd + "/testdata/dirfs/a")
494*4882a593Smuzhiyun+	if err != nil {
495*4882a593Smuzhiyun+		t.Fatal(err)
496*4882a593Smuzhiyun+	}
497*4882a593Smuzhiyun+	f.Close()
498*4882a593Smuzhiyun+}
499*4882a593Smuzhiyun+
500*4882a593Smuzhiyun+func TestDirFSEmptyDir(t *testing.T) {
501*4882a593Smuzhiyun+	d := DirFS("")
502*4882a593Smuzhiyun+	cwd, _ := os.Getwd()
503*4882a593Smuzhiyun+	for _, path := range []string{
504*4882a593Smuzhiyun+		"testdata/dirfs/a",                          // not DirFS(".")
505*4882a593Smuzhiyun+		filepath.ToSlash(cwd) + "/testdata/dirfs/a", // not DirFS("/")
506*4882a593Smuzhiyun+	} {
507*4882a593Smuzhiyun+		_, err := d.Open(path)
508*4882a593Smuzhiyun+		if err == nil {
509*4882a593Smuzhiyun+			t.Fatalf(`DirFS("").Open(%q) succeeded`, path)
510*4882a593Smuzhiyun+		}
511*4882a593Smuzhiyun+	}
512*4882a593Smuzhiyun }
513*4882a593Smuzhiyun
514*4882a593Smuzhiyun func TestDirFSPathsValid(t *testing.T) {
515