@@ -9,6 +9,7 @@ package securejoin
99import (
1010 "os"
1111 "path/filepath"
12+ "runtime"
1213
1314 "golang.org/x/sys/unix"
1415)
@@ -21,35 +22,64 @@ func dupFile(f *os.File) (*os.File, error) {
2122 return os .NewFile (uintptr (fd ), f .Name ()), nil
2223}
2324
25+ // prepareAtWith returns -EBADF (an invalid fd) if dir is nil, otherwise using
26+ // the dir.Fd(). We use -EBADF because in filepath-securejoin we generally
27+ // don't want to allow relative-to-cwd paths. The returned path is an
28+ // *informational* string that describes a reasonable pathname for the given
29+ // *at(2) arguments. You must not use the full path for any actual filesystem
30+ // operations.
31+ func prepareAt (dir * os.File , path string ) (dirFd int , unsafeFullPath string ) {
32+ dirFd , dirPath := - int (unix .EBADF ), "."
33+ if dir != nil {
34+ dirFd , dirPath = int (dir .Fd ()), dir .Name ()
35+ }
36+ if ! filepath .IsAbs (path ) {
37+ // only prepend the dirfd path for relative paths
38+ path = dirPath + "/" + path
39+ }
40+ // NOTE: If path is "." or "", the returned path won't be filepath.Clean,
41+ // but that's okay since this path is either used for errors (in which case
42+ // a trailing "/" or "/." is important information) or will be
43+ // filepath.Clean'd later (in the case of openatFile).
44+ return dirFd , path
45+ }
46+
2447func openatFile (dir * os.File , path string , flags int , mode int ) (* os.File , error ) { //nolint:unparam // wrapper func
48+ dirFd , fullPath := prepareAt (dir , path )
2549 // Make sure we always set O_CLOEXEC.
2650 flags |= unix .O_CLOEXEC
27- fd , err := unix .Openat (int ( dir . Fd ()) , path , flags , uint32 (mode ))
51+ fd , err := unix .Openat (dirFd , path , flags , uint32 (mode ))
2852 if err != nil {
29- return nil , & os.PathError {Op : "openat" , Path : dir . Name () + "/" + path , Err : err }
53+ return nil , & os.PathError {Op : "openat" , Path : fullPath , Err : err }
3054 }
31- // All of the paths we use with openatFile(2) are guaranteed to be
32- // lexically safe, so we can use path.Join here.
33- fullPath := filepath .Join (dir .Name (), path )
55+ runtime .KeepAlive (dir )
56+ // openat is only used with lexically-safe paths so we can use
57+ // filepath.Clean here, and also the path itself is not going to be used
58+ // for actual path operations.
59+ fullPath = filepath .Clean (fullPath )
3460 return os .NewFile (uintptr (fd ), fullPath ), nil
3561}
3662
3763func fstatatFile (dir * os.File , path string , flags int ) (unix.Stat_t , error ) {
64+ dirFd , fullPath := prepareAt (dir , path )
3865 var stat unix.Stat_t
39- if err := unix .Fstatat (int ( dir . Fd ()) , path , & stat , flags ); err != nil {
40- return stat , & os.PathError {Op : "fstatat" , Path : dir . Name () + "/" + path , Err : err }
66+ if err := unix .Fstatat (dirFd , path , & stat , flags ); err != nil {
67+ return stat , & os.PathError {Op : "fstatat" , Path : fullPath , Err : err }
4168 }
69+ runtime .KeepAlive (dir )
4270 return stat , nil
4371}
4472
4573func readlinkatFile (dir * os.File , path string ) (string , error ) {
74+ dirFd , fullPath := prepareAt (dir , path )
4675 size := 4096
4776 for {
4877 linkBuf := make ([]byte , size )
49- n , err := unix .Readlinkat (int ( dir . Fd ()) , path , linkBuf )
78+ n , err := unix .Readlinkat (dirFd , path , linkBuf )
5079 if err != nil {
51- return "" , & os.PathError {Op : "readlinkat" , Path : dir . Name () + "/" + path , Err : err }
80+ return "" , & os.PathError {Op : "readlinkat" , Path : fullPath , Err : err }
5281 }
82+ runtime .KeepAlive (dir )
5383 if n != size {
5484 return string (linkBuf [:n ]), nil
5585 }
0 commit comments