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