join.go 4.1 KB

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