github.com/demonoid81/moby@v0.0.0-20200517203328-62dd8e17c460/pkg/symlink/fs.go (about) 1 // Copyright 2012 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE.BSD file. 4 5 // This code is a modified version of path/filepath/symlink.go from the Go standard library. 6 7 package symlink // import "github.com/demonoid81/moby/pkg/symlink" 8 9 import ( 10 "bytes" 11 "errors" 12 "os" 13 "path/filepath" 14 "strings" 15 ) 16 17 // FollowSymlinkInScope is a wrapper around evalSymlinksInScope that returns an 18 // absolute path. This function handles paths in a platform-agnostic manner. 19 func FollowSymlinkInScope(path, root string) (string, error) { 20 path, err := filepath.Abs(filepath.FromSlash(path)) 21 if err != nil { 22 return "", err 23 } 24 root, err = filepath.Abs(filepath.FromSlash(root)) 25 if err != nil { 26 return "", err 27 } 28 return evalSymlinksInScope(path, root) 29 } 30 31 // evalSymlinksInScope will evaluate symlinks in `path` within a scope `root` and return 32 // a result guaranteed to be contained within the scope `root`, at the time of the call. 33 // Symlinks in `root` are not evaluated and left as-is. 34 // Errors encountered while attempting to evaluate symlinks in path will be returned. 35 // Non-existing paths are valid and do not constitute an error. 36 // `path` has to contain `root` as a prefix, or else an error will be returned. 37 // Trying to break out from `root` does not constitute an error. 38 // 39 // Example: 40 // If /foo/bar -> /outside, 41 // FollowSymlinkInScope("/foo/bar", "/foo") == "/foo/outside" instead of "/outside" 42 // 43 // IMPORTANT: it is the caller's responsibility to call evalSymlinksInScope *after* relevant symlinks 44 // are created and not to create subsequently, additional symlinks that could potentially make a 45 // previously-safe path, unsafe. Example: if /foo/bar does not exist, evalSymlinksInScope("/foo/bar", "/foo") 46 // would return "/foo/bar". If one makes /foo/bar a symlink to /baz subsequently, then "/foo/bar" should 47 // no longer be considered safely contained in "/foo". 48 func evalSymlinksInScope(path, root string) (string, error) { 49 root = filepath.Clean(root) 50 if path == root { 51 return path, nil 52 } 53 if !strings.HasPrefix(path, root) { 54 return "", errors.New("evalSymlinksInScope: " + path + " is not in " + root) 55 } 56 const maxIter = 255 57 originalPath := path 58 // given root of "/a" and path of "/a/b/../../c" we want path to be "/b/../../c" 59 path = path[len(root):] 60 if root == string(filepath.Separator) { 61 path = string(filepath.Separator) + path 62 } 63 if !strings.HasPrefix(path, string(filepath.Separator)) { 64 return "", errors.New("evalSymlinksInScope: " + path + " is not in " + root) 65 } 66 path = filepath.Clean(path) 67 // consume path by taking each frontmost path element, 68 // expanding it if it's a symlink, and appending it to b 69 var b bytes.Buffer 70 // b here will always be considered to be the "current absolute path inside 71 // root" when we append paths to it, we also append a slash and use 72 // filepath.Clean after the loop to trim the trailing slash 73 for n := 0; path != ""; n++ { 74 if n > maxIter { 75 return "", errors.New("evalSymlinksInScope: too many links in " + originalPath) 76 } 77 78 // find next path component, p 79 i := strings.IndexRune(path, filepath.Separator) 80 var p string 81 if i == -1 { 82 p, path = path, "" 83 } else { 84 p, path = path[:i], path[i+1:] 85 } 86 87 if p == "" { 88 continue 89 } 90 91 // this takes a b.String() like "b/../" and a p like "c" and turns it 92 // into "/b/../c" which then gets filepath.Cleaned into "/c" and then 93 // root gets prepended and we Clean again (to remove any trailing slash 94 // if the first Clean gave us just "/") 95 cleanP := filepath.Clean(string(filepath.Separator) + b.String() + p) 96 if isDriveOrRoot(cleanP) { 97 // never Lstat "/" itself, or drive letters on Windows 98 b.Reset() 99 continue 100 } 101 fullP := filepath.Clean(root + cleanP) 102 103 fi, err := os.Lstat(fullP) 104 if os.IsNotExist(err) { 105 // if p does not exist, accept it 106 b.WriteString(p) 107 b.WriteRune(filepath.Separator) 108 continue 109 } 110 if err != nil { 111 return "", err 112 } 113 if fi.Mode()&os.ModeSymlink == 0 { 114 b.WriteString(p) 115 b.WriteRune(filepath.Separator) 116 continue 117 } 118 119 // it's a symlink, put it at the front of path 120 dest, err := os.Readlink(fullP) 121 if err != nil { 122 return "", err 123 } 124 if isAbs(dest) { 125 b.Reset() 126 } 127 path = dest + string(filepath.Separator) + path 128 } 129 130 // see note above on "fullP := ..." for why this is double-cleaned and 131 // what's happening here 132 return filepath.Clean(root + filepath.Clean(string(filepath.Separator)+b.String())), nil 133 } 134 135 // EvalSymlinks returns the path name after the evaluation of any symbolic 136 // links. 137 // If path is relative the result will be relative to the current directory, 138 // unless one of the components is an absolute symbolic link. 139 // This version has been updated to support long paths prepended with `\\?\`. 140 func EvalSymlinks(path string) (string, error) { 141 return evalSymlinks(path) 142 }