Skip to content

Commit

Permalink
[release-branch.go1.21] path/filepath: fix various issues in parsing …
Browse files Browse the repository at this point in the history
…Windows paths

On Windows, A root local device path is a path which begins with
\\?\ or \??\.  A root local device path accesses the DosDevices
object directory, and permits access to any file or device on the
system. For example \??\C:\foo is equivalent to common C:\foo.

The Clean, IsAbs, IsLocal, and VolumeName functions did not
recognize root local device paths beginning with \??\.

Clean could convert a rooted path such as \a\..\??\b into
the root local device path \??\b. It will now convert this
path into .\??\b.

IsAbs now correctly reports paths beginning with \??\
as absolute.

IsLocal now correctly reports paths beginning with \??\
as non-local.

VolumeName now reports the \??\ prefix as a volume name.

Join(`\`, `??`, `b`) could convert a seemingly innocent
sequence of path elements into the root local device path
\??\b. It will now convert this to \.\??\b.

In addition, the IsLocal function did not correctly
detect reserved names in some cases:

  - reserved names followed by spaces, such as "COM1 ".
  - "COM" or "LPT" followed by a superscript 1, 2, or 3.

IsLocal now correctly reports these names as non-local.

For #63713
Fixes #63715
Fixes CVE-2023-45283
Fixes CVE-2023-45284

Change-Id: I446674a58977adfa54de7267d716ac23ab496c54
Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/2040691
Reviewed-by: Roland Shoemaker <bracewell@google.com>
Reviewed-by: Tatiana Bradley <tatianabradley@google.com>
Run-TryBot: Damien Neil <dneil@google.com>
Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/2072596
Reviewed-by: Cherry Mui <cherryyz@google.com>
Reviewed-on: https://go-review.googlesource.com/c/go/+/540276
Auto-Submit: Heschi Kreinick <heschi@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
  • Loading branch information
neild authored and gopherbot committed Nov 7, 2023
1 parent 434af85 commit 9e933c1
Show file tree
Hide file tree
Showing 6 changed files with 269 additions and 118 deletions.
2 changes: 1 addition & 1 deletion src/go/build/deps_test.go
Expand Up @@ -157,7 +157,7 @@ var depsRules = `
unicode, fmt !< net, os, os/signal;
os/signal, STR
os/signal, internal/safefilepath, STR
< path/filepath
< io/ioutil;
Expand Down
98 changes: 72 additions & 26 deletions src/internal/safefilepath/path_windows.go
Expand Up @@ -20,15 +20,10 @@ func fromFS(path string) (string, error) {
for p := path; p != ""; {
// Find the next path element.
i := 0
dot := -1
for i < len(p) && p[i] != '/' {
switch p[i] {
case 0, '\\', ':':
return "", errInvalidPath
case '.':
if dot < 0 {
dot = i
}
}
i++
}
Expand All @@ -39,22 +34,8 @@ func fromFS(path string) (string, error) {
} else {
p = ""
}
// Trim the extension and look for a reserved name.
base := part
if dot >= 0 {
base = part[:dot]
}
if isReservedName(base) {
if dot < 0 {
return "", errInvalidPath
}
// The path element is a reserved name with an extension.
// Some Windows versions consider this a reserved name,
// while others do not. Use FullPath to see if the name is
// reserved.
if p, _ := syscall.FullPath(part); len(p) >= 4 && p[:4] == `\\.\` {
return "", errInvalidPath
}
if IsReservedName(part) {
return "", errInvalidPath
}
}
if containsSlash {
Expand All @@ -70,23 +51,88 @@ func fromFS(path string) (string, error) {
return path, nil
}

// isReservedName reports if name is a Windows reserved device name.
// IsReservedName reports if name is a Windows reserved device name.
// It does not detect names with an extension, which are also reserved on some Windows versions.
//
// For details, search for PRN in
// https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file.
func isReservedName(name string) bool {
if 3 <= len(name) && len(name) <= 4 {
func IsReservedName(name string) bool {
// Device names can have arbitrary trailing characters following a dot or colon.
base := name
for i := 0; i < len(base); i++ {
switch base[i] {
case ':', '.':
base = base[:i]
}
}
// Trailing spaces in the last path element are ignored.
for len(base) > 0 && base[len(base)-1] == ' ' {
base = base[:len(base)-1]
}
if !isReservedBaseName(base) {
return false
}
if len(base) == len(name) {
return true
}
// The path element is a reserved name with an extension.
// Some Windows versions consider this a reserved name,
// while others do not. Use FullPath to see if the name is
// reserved.
if p, _ := syscall.FullPath(name); len(p) >= 4 && p[:4] == `\\.\` {
return true
}
return false
}

func isReservedBaseName(name string) bool {
if len(name) == 3 {
switch string([]byte{toUpper(name[0]), toUpper(name[1]), toUpper(name[2])}) {
case "CON", "PRN", "AUX", "NUL":
return len(name) == 3
return true
}
}
if len(name) >= 4 {
switch string([]byte{toUpper(name[0]), toUpper(name[1]), toUpper(name[2])}) {
case "COM", "LPT":
return len(name) == 4 && '1' <= name[3] && name[3] <= '9'
if len(name) == 4 && '1' <= name[3] && name[3] <= '9' {
return true
}
// Superscript ¹, ², and ³ are considered numbers as well.
switch name[3:] {
case "\u00b2", "\u00b3", "\u00b9":
return true
}
return false
}
}

// Passing CONIN$ or CONOUT$ to CreateFile opens a console handle.
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#consoles
//
// While CONIN$ and CONOUT$ aren't documented as being files,
// they behave the same as CON. For example, ./CONIN$ also opens the console input.
if len(name) == 6 && name[5] == '$' && equalFold(name, "CONIN$") {
return true
}
if len(name) == 7 && name[6] == '$' && equalFold(name, "CONOUT$") {
return true
}
return false
}

func equalFold(a, b string) bool {
if len(a) != len(b) {
return false
}
for i := 0; i < len(a); i++ {
if toUpper(a[i]) != toUpper(b[i]) {
return false
}
}
return true
}

func toUpper(c byte) byte {
if 'a' <= c && c <= 'z' {
return c - ('a' - 'A')
Expand Down
17 changes: 1 addition & 16 deletions src/path/filepath/path.go
Expand Up @@ -15,7 +15,6 @@ import (
"errors"
"io/fs"
"os"
"runtime"
"sort"
"strings"
)
Expand Down Expand Up @@ -167,21 +166,7 @@ func Clean(path string) string {
out.append('.')
}

if runtime.GOOS == "windows" && out.volLen == 0 && out.buf != nil {
// If a ':' appears in the path element at the start of a Windows path,
// insert a .\ at the beginning to avoid converting relative paths
// like a/../c: into c:.
for _, c := range out.buf {
if os.IsPathSeparator(c) {
break
}
if c == ':' {
out.prepend('.', Separator)
break
}
}
}

postClean(&out) // avoid creating absolute paths on Windows
return FromSlash(out.string())
}

Expand Down
9 changes: 9 additions & 0 deletions src/path/filepath/path_nonwindows.go
@@ -0,0 +1,9 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build !windows

package filepath

func postClean(out *lazybuf) {}
67 changes: 65 additions & 2 deletions src/path/filepath/path_test.go
Expand Up @@ -116,6 +116,9 @@ var wincleantests = []PathTest{
{`a/../c:/a`, `.\c:\a`},
{`a/../../c:`, `..\c:`},
{`foo:bar`, `foo:bar`},

// Don't allow cleaning to create a Root Local Device path like \??\a.
{`/a/../??/a`, `\.\??\a`},
}

func TestClean(t *testing.T) {
Expand Down Expand Up @@ -177,8 +180,28 @@ var islocaltests = []IsLocalTest{
var winislocaltests = []IsLocalTest{
{"NUL", false},
{"nul", false},
{"nul ", false},
{"nul.", false},
{"a/nul:", false},
{"a/nul : a", false},
{"com0", true},
{"com1", false},
{"com2", false},
{"com3", false},
{"com4", false},
{"com5", false},
{"com6", false},
{"com7", false},
{"com8", false},
{"com9", false},
{"com¹", false},
{"com²", false},
{"com³", false},
{"com¹ : a", false},
{"cOm1", false},
{"lpt1", false},
{"LPT1", false},
{"lpt³", false},
{"./nul", false},
{`\`, false},
{`\a`, false},
Expand Down Expand Up @@ -384,6 +407,7 @@ var winjointests = []JoinTest{
{[]string{`\\a\`, `b`, `c`}, `\\a\b\c`},
{[]string{`//`, `a`}, `\\a`},
{[]string{`a:\b\c`, `x\..\y:\..\..\z`}, `a:\b\z`},
{[]string{`\`, `??\a`}, `\.\??\a`},
}

func TestJoin(t *testing.T) {
Expand Down Expand Up @@ -1060,6 +1084,8 @@ var winisabstests = []IsAbsTest{
{`\\host\share\`, true},
{`\\host\share\foo`, true},
{`//host/share/foo/bar`, true},
{`\\?\a\b\c`, true},
{`\??\a\b\c`, true},
}

func TestIsAbs(t *testing.T) {
Expand Down Expand Up @@ -1560,7 +1586,8 @@ type VolumeNameTest struct {
var volumenametests = []VolumeNameTest{
{`c:/foo/bar`, `c:`},
{`c:`, `c:`},
{`2:`, ``},
{`c:\`, `c:`},
{`2:`, `2:`},
{``, ``},
{`\\\host`, `\\\host`},
{`\\\host\`, `\\\host`},
Expand All @@ -1580,12 +1607,23 @@ var volumenametests = []VolumeNameTest{
{`//host/share//foo///bar////baz`, `\\host\share`},
{`\\host\share\foo\..\bar`, `\\host\share`},
{`//host/share/foo/../bar`, `\\host\share`},
{`//.`, `\\.`},
{`//./`, `\\.\`},
{`//./NUL`, `\\.\NUL`},
{`//?/NUL`, `\\?\NUL`},
{`//?/`, `\\?`},
{`//./a/b`, `\\.\a`},
{`//?/`, `\\?`},
{`//?/`, `\\?`},
{`//./C:`, `\\.\C:`},
{`//./C:/`, `\\.\C:`},
{`//./C:/a/b/c`, `\\.\C:`},
{`//./UNC/host/share/a/b/c`, `\\.\UNC\host\share`},
{`//./UNC/host`, `\\.\UNC\host`},
{`//./UNC/host\`, `\\.\UNC\host\`},
{`//./UNC`, `\\.\UNC`},
{`//./UNC/`, `\\.\UNC\`},
{`\\?\x`, `\\?`},
{`\??\x`, `\??`},
}

func TestVolumeName(t *testing.T) {
Expand Down Expand Up @@ -1855,3 +1893,28 @@ func TestIssue51617(t *testing.T) {
t.Errorf("got directories %v, want %v", saw, want)
}
}

func TestEscaping(t *testing.T) {
dir1 := t.TempDir()
dir2 := t.TempDir()
chdir(t, dir1)

for _, p := range []string{
filepath.Join(dir2, "x"),
} {
if !filepath.IsLocal(p) {
continue
}
f, err := os.Create(p)
if err != nil {
f.Close()
}
ents, err := os.ReadDir(dir2)
if err != nil {
t.Fatal(err)
}
for _, e := range ents {
t.Fatalf("found: %v", e.Name())
}
}
}

0 comments on commit 9e933c1

Please sign in to comment.