1 // Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved. 2 // Copyright (C) 2017 SUSE LLC. All rights reserved. 3 // Use of this source code is governed by a BSD-style 4 // license that can be found in the LICENSE file. 5 6 // Package securejoin is an implementation of the hopefully-soon-to-be-included 7 // SecureJoin helper that is meant to be part of the "path/filepath" package. 8 // The purpose of this project is to provide a PoC implementation to make the 9 // SecureJoin proposal (https://github.com/golang/go/issues/20126) more 10 // tangible. 11 package securejoin 12 13 import ( 14 "bytes" 15 "errors" 16 "os" 17 "path/filepath" 18 "strings" 19 "syscall" 20 ) 21 22 // IsNotExist tells you if err is an error that implies that either the path 23 // accessed does not exist (or path components don't exist). This is 24 // effectively a more broad version of os.IsNotExist. 25 func IsNotExist(err error) bool { 26 // Check that it's not actually an ENOTDIR, which in some cases is a more 27 // convoluted case of ENOENT (usually involving weird paths). 28 return errors.Is(err, os.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) || errors.Is(err, syscall.ENOENT) 29 } 30 31 // SecureJoinVFS joins the two given path components (similar to Join) except 32 // that the returned path is guaranteed to be scoped inside the provided root 33 // path (when evaluated). Any symbolic links in the path are evaluated with the 34 // given root treated as the root of the filesystem, similar to a chroot. The 35 // filesystem state is evaluated through the given VFS interface (if nil, the 36 // standard os.* family of functions are used). 37 // 38 // Note that the guarantees provided by this function only apply if the path 39 // components in the returned string are not modified (in other words are not 40 // replaced with symlinks on the filesystem) after this function has returned. 41 // Such a symlink race is necessarily out-of-scope of SecureJoin. 42 // 43 // Volume names in unsafePath are always discarded, regardless if they are 44 // provided via direct input or when evaluating symlinks. Therefore: 45 // 46 // "C:\Temp" + "D:\path\to\file.txt" results in "C:\Temp\path\to\file.txt" 47 func SecureJoinVFS(root, unsafePath string, vfs VFS) (string, error) { 48 // Use the os.* VFS implementation if none was specified. 49 if vfs == nil { 50 vfs = osVFS{} 51 } 52 53 unsafePath = filepath.FromSlash(unsafePath) 54 var path bytes.Buffer 55 n := 0 56 for unsafePath != "" { 57 if n > 255 { 58 return "", &os.PathError{Op: "SecureJoin", Path: root + string(filepath.Separator) + unsafePath, Err: syscall.ELOOP} 59 } 60 61 if v := filepath.VolumeName(unsafePath); v != "" { 62 unsafePath = unsafePath[len(v):] 63 } 64 65 // Next path component, p. 66 i := strings.IndexRune(unsafePath, filepath.Separator) 67 var p string 68 if i == -1 { 69 p, unsafePath = unsafePath, "" 70 } else { 71 p, unsafePath = unsafePath[:i], unsafePath[i+1:] 72 } 73 74 // Create a cleaned path, using the lexical semantics of /../a, to 75 // create a "scoped" path component which can safely be joined to fullP 76 // for evaluation. At this point, path.String() doesn't contain any 77 // symlink components. 78 cleanP := filepath.Clean(string(filepath.Separator) + path.String() + p) 79 if cleanP == string(filepath.Separator) { 80 path.Reset() 81 continue 82 } 83 fullP := filepath.Clean(root + cleanP) 84 85 // Figure out whether the path is a symlink. 86 fi, err := vfs.Lstat(fullP) 87 if err != nil && !IsNotExist(err) { 88 return "", err 89 } 90 // Treat non-existent path components the same as non-symlinks (we 91 // can't do any better here). 92 if IsNotExist(err) || fi.Mode()&os.ModeSymlink == 0 { 93 path.WriteString(p) 94 path.WriteRune(filepath.Separator) 95 continue 96 } 97 98 // Only increment when we actually dereference a link. 99 n++ 100 101 // It's a symlink, expand it by prepending it to the yet-unparsed path. 102 dest, err := vfs.Readlink(fullP) 103 if err != nil { 104 return "", err 105 } 106 // Absolute symlinks reset any work we've already done. 107 if filepath.IsAbs(dest) { 108 path.Reset() 109 } 110 unsafePath = dest + string(filepath.Separator) + unsafePath 111 } 112 113 // We have to clean path.String() here because it may contain '..' 114 // components that are entirely lexical, but would be misleading otherwise. 115 // And finally do a final clean to ensure that root is also lexically 116 // clean. 117 fullP := filepath.Clean(string(filepath.Separator) + path.String()) 118 return filepath.Clean(root + fullP), nil 119 } 120 121 // SecureJoin is a wrapper around SecureJoinVFS that just uses the os.* library 122 // of functions as the VFS. If in doubt, use this function over SecureJoinVFS. 123 func SecureJoin(root, unsafePath string) (string, error) { 124 return SecureJoinVFS(root, unsafePath, nil) 125 } 126