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