]> Cypherpunks.ru repositories - gostls13.git/commitdiff
path/filepath: add IsLocal
authorDamien Neil <dneil@google.com>
Thu, 10 Nov 2022 01:49:44 +0000 (17:49 -0800)
committerDamien Neil <dneil@google.com>
Wed, 16 Nov 2022 23:17:58 +0000 (23:17 +0000)
IsLocal reports whether a path lexically refers to a location
contained within the directory in which it is evaluated.
It identifies paths that are absolute, escape a directory
with ".." elements, and (on Windows) paths that reference
reserved device names.

For #56219.

Change-Id: I35edfa3ce77b40b8e66f1fc8e0ff73cfd06f2313
Reviewed-on: https://go-review.googlesource.com/c/go/+/449239
Run-TryBot: Damien Neil <dneil@google.com>
Reviewed-by: Joseph Tsai <joetsai@digital-static.net>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@google.com>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
Reviewed-by: Joedian Reid <joedian@golang.org>
api/next/56219.txt [new file with mode: 0644]
doc/go1.20.html
src/path/filepath/path.go
src/path/filepath/path_plan9.go
src/path/filepath/path_test.go
src/path/filepath/path_unix.go
src/path/filepath/path_windows.go

diff --git a/api/next/56219.txt b/api/next/56219.txt
new file mode 100644 (file)
index 0000000..6379c06
--- /dev/null
@@ -0,0 +1 @@
+pkg path/filepath, func IsLocal(string) bool #56219
index 3d4eeb0f364194cc942599823b0b1eee2b00127f..7246e6efb2bea6909a8cc074290776d98c81948d 100644 (file)
@@ -665,6 +665,13 @@ proxyHandler := &httputil.ReverseProxy{
     <p><!-- CL 363814 --><!-- https://go.dev/issue/47209 -->
       TODO: <a href="https://go.dev/cl/363814">https://go.dev/cl/363814</a>: path/filepath, io/fs: add SkipAll; modified api/next/47209.txt
     </p>
+    <p><!-- https://go.dev/issue/56219 -->
+      The new <code>IsLocal</code> function reports whether a path is
+      lexically local to a directory.
+      For example, if <code>IsLocal(p)</code> is <code>true</code>,
+      then <code>Open(p)</code> will refer to a file that is lexically
+      within the subtree rooted at the current directory.
+    </p>
   </dd>
 </dl><!-- io -->
 
index c5c54fc9a510a8ce1226c4a6e3365ec272d28b8d..a6578cbb728b95462b50da148a2342ba74e9faf7 100644 (file)
@@ -172,6 +172,46 @@ func Clean(path string) string {
        return FromSlash(out.string())
 }
 
+// IsLocal reports whether path, using lexical analysis only, has all of these properties:
+//
+//   - is within the subtree rooted at the directory in which path is evaluated
+//   - is not an absolute path
+//   - is not empty
+//   - on Windows, is not a reserved name such as "NUL"
+//
+// If IsLocal(path) returns true, then
+// Join(base, path) will always produce a path contained within base and
+// Clean(path) will always produce an unrooted path with no ".." path elements.
+//
+// IsLocal is a purely lexical operation.
+// In particular, it does not account for the effect of any symbolic links
+// that may exist in the filesystem.
+func IsLocal(path string) bool {
+       return isLocal(path)
+}
+
+func unixIsLocal(path string) bool {
+       if IsAbs(path) || path == "" {
+               return false
+       }
+       hasDots := false
+       for p := path; p != ""; {
+               var part string
+               part, p, _ = strings.Cut(p, "/")
+               if part == "." || part == ".." {
+                       hasDots = true
+                       break
+               }
+       }
+       if hasDots {
+               path = Clean(path)
+       }
+       if path == ".." || strings.HasPrefix(path, "../") {
+               return false
+       }
+       return true
+}
+
 // ToSlash returns the result of replacing each separator character
 // in path with a slash ('/') character. Multiple separators are
 // replaced by multiple slashes.
index ec792fc831aefbb77380fa27aef591b2fd1f3430..453206aee3e04eb1289d9c2a0f626d42736a61f9 100644 (file)
@@ -6,6 +6,10 @@ package filepath
 
 import "strings"
 
+func isLocal(path string) bool {
+       return unixIsLocal(path)
+}
+
 // IsAbs reports whether the path is absolute.
 func IsAbs(path string) bool {
        return strings.HasPrefix(path, "/") || strings.HasPrefix(path, "#")
index 382381eb4e39dca9c65f491c5b284e67e83f19d3..771416770e9967d783f1d1adfa4fd65203049f25 100644 (file)
@@ -143,6 +143,60 @@ func TestClean(t *testing.T) {
        }
 }
 
+type IsLocalTest struct {
+       path    string
+       isLocal bool
+}
+
+var islocaltests = []IsLocalTest{
+       {"", false},
+       {".", true},
+       {"..", false},
+       {"../a", false},
+       {"/", false},
+       {"/a", false},
+       {"/a/../..", false},
+       {"a", true},
+       {"a/../a", true},
+       {"a/", true},
+       {"a/.", true},
+       {"a/./b/./c", true},
+}
+
+var winislocaltests = []IsLocalTest{
+       {"NUL", false},
+       {"nul", false},
+       {"nul.", false},
+       {"nul.txt", false},
+       {"com1", false},
+       {"./nul", false},
+       {"a/nul.txt/b", false},
+       {`\`, false},
+       {`\a`, false},
+       {`C:`, false},
+       {`C:\a`, false},
+       {`..\a`, false},
+}
+
+var plan9islocaltests = []IsLocalTest{
+       {"#a", false},
+}
+
+func TestIsLocal(t *testing.T) {
+       tests := islocaltests
+       if runtime.GOOS == "windows" {
+               tests = append(tests, winislocaltests...)
+       }
+       if runtime.GOOS == "plan9" {
+               tests = append(tests, plan9islocaltests...)
+       }
+       for _, test := range tests {
+               if got := filepath.IsLocal(test.path); got != test.isLocal {
+                       t.Errorf("IsLocal(%q) = %v, want %v", test.path, got, test.isLocal)
+               }
+       }
+}
+
 const sep = filepath.Separator
 
 var slashtests = []PathTest{
index 93fdfdd8a049d5620e7c06af114f8f33fa90af90..ab1d08d35654ac0fd0d434956e9a5c0d32ef63d5 100644 (file)
@@ -8,6 +8,10 @@ package filepath
 
 import "strings"
 
+func isLocal(path string) bool {
+       return unixIsLocal(path)
+}
+
 // IsAbs reports whether the path is absolute.
 func IsAbs(path string) bool {
        return strings.HasPrefix(path, "/")
index c754301bf450d966ab4c2f62b7a8ba10f7cbf9aa..b26658a9371aa922a391d81c6f3a39b72902eb5a 100644 (file)
@@ -20,6 +20,73 @@ func toUpper(c byte) byte {
        return c
 }
 
+// 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 {
+               switch string([]byte{toUpper(name[0]), toUpper(name[1]), toUpper(name[2])}) {
+               case "CON", "PRN", "AUX", "NUL":
+                       return len(name) == 3
+               case "COM", "LPT":
+                       return len(name) == 4 && '1' <= name[3] && name[3] <= '9'
+               }
+       }
+       return false
+}
+
+func isLocal(path string) bool {
+       if path == "" {
+               return false
+       }
+       if isSlash(path[0]) {
+               // Path rooted in the current drive.
+               return false
+       }
+       if strings.IndexByte(path, ':') >= 0 {
+               // Colons are only valid when marking a drive letter ("C:foo").
+               // Rejecting any path with a colon is conservative but safe.
+               return false
+       }
+       hasDots := false // contains . or .. path elements
+       for p := path; p != ""; {
+               var part string
+               part, p, _ = cutPath(p)
+               if part == "." || part == ".." {
+                       hasDots = true
+               }
+               // Trim the extension and look for a reserved name.
+               base, _, hasExt := strings.Cut(part, ".")
+               if isReservedName(base) {
+                       if !hasExt {
+                               return false
+                       }
+                       // 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.
+                       //
+                       // FullPath will convert references to reserved device names to their
+                       // canonical form: \\.\${DEVICE_NAME}
+                       //
+                       // FullPath does not perform this conversion for paths which contain
+                       // a reserved device name anywhere other than in the last element,
+                       // so check the part rather than the full path.
+                       if p, _ := syscall.FullPath(part); len(p) >= 4 && p[:4] == `\\.\` {
+                               return false
+                       }
+               }
+       }
+       if hasDots {
+               path = Clean(path)
+       }
+       if path == ".." || strings.HasPrefix(path, `..\`) {
+               return false
+       }
+       return true
+}
+
 // IsAbs reports whether the path is absolute.
 func IsAbs(path string) (b bool) {
        l := volumeNameLen(path)