join.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  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. "os"
  14. "path/filepath"
  15. "strings"
  16. "syscall"
  17. "github.com/pkg/errors"
  18. )
  19. // ErrSymlinkLoop is returned by SecureJoinVFS when too many symlinks have been
  20. // evaluated in attempting to securely join the two given paths.
  21. var ErrSymlinkLoop = errors.Wrap(syscall.ELOOP, "secure join")
  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. // If it's a bone-fide ENOENT just bail.
  27. if os.IsNotExist(errors.Cause(err)) {
  28. return true
  29. }
  30. // Check that it's not actually an ENOTDIR, which in some cases is a more
  31. // convoluted case of ENOENT (usually involving weird paths).
  32. var errno error
  33. switch err := errors.Cause(err).(type) {
  34. case *os.PathError:
  35. errno = err.Err
  36. case *os.LinkError:
  37. errno = err.Err
  38. case *os.SyscallError:
  39. errno = err.Err
  40. }
  41. return errno == syscall.ENOTDIR || errno == syscall.ENOENT
  42. }
  43. // SecureJoinVFS joins the two given path components (similar to Join) except
  44. // that the returned path is guaranteed to be scoped inside the provided root
  45. // path (when evaluated). Any symbolic links in the path are evaluated with the
  46. // given root treated as the root of the filesystem, similar to a chroot. The
  47. // filesystem state is evaluated through the given VFS interface (if nil, the
  48. // standard os.* family of functions are used).
  49. //
  50. // Note that the guarantees provided by this function only apply if the path
  51. // components in the returned string are not modified (in other words are not
  52. // replaced with symlinks on the filesystem) after this function has returned.
  53. // Such a symlink race is necessarily out-of-scope of SecureJoin.
  54. func SecureJoinVFS(root, unsafePath string, vfs VFS) (string, error) {
  55. // Use the os.* VFS implementation if none was specified.
  56. if vfs == nil {
  57. vfs = osVFS{}
  58. }
  59. var path bytes.Buffer
  60. n := 0
  61. for unsafePath != "" {
  62. if n > 255 {
  63. return "", ErrSymlinkLoop
  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. // Create a cleaned path, using the lexical semantics of /../a, to
  74. // create a "scoped" path component which can safely be joined to fullP
  75. // for evaluation. At this point, path.String() doesn't contain any
  76. // symlink components.
  77. cleanP := filepath.Clean(string(filepath.Separator) + path.String() + p)
  78. if cleanP == string(filepath.Separator) {
  79. path.Reset()
  80. continue
  81. }
  82. fullP := filepath.Clean(root + cleanP)
  83. // Figure out whether the path is a symlink.
  84. fi, err := vfs.Lstat(fullP)
  85. if err != nil && !IsNotExist(err) {
  86. return "", err
  87. }
  88. // Treat non-existent path components the same as non-symlinks (we
  89. // can't do any better here).
  90. if IsNotExist(err) || fi.Mode()&os.ModeSymlink == 0 {
  91. path.WriteString(p)
  92. path.WriteRune(filepath.Separator)
  93. continue
  94. }
  95. // Only increment when we actually dereference a link.
  96. n++
  97. // It's a symlink, expand it by prepending it to the yet-unparsed path.
  98. dest, err := vfs.Readlink(fullP)
  99. if err != nil {
  100. return "", err
  101. }
  102. // Absolute symlinks reset any work we've already done.
  103. if filepath.IsAbs(dest) {
  104. path.Reset()
  105. }
  106. unsafePath = dest + string(filepath.Separator) + unsafePath
  107. }
  108. // We have to clean path.String() here because it may contain '..'
  109. // components that are entirely lexical, but would be misleading otherwise.
  110. // And finally do a final clean to ensure that root is also lexically
  111. // clean.
  112. fullP := filepath.Clean(string(filepath.Separator) + path.String())
  113. return filepath.Clean(root + fullP), nil
  114. }
  115. // SecureJoin is a wrapper around SecureJoinVFS that just uses the os.* library
  116. // of functions as the VFS. If in doubt, use this function over SecureJoinVFS.
  117. func SecureJoin(root, unsafePath string) (string, error) {
  118. return SecureJoinVFS(root, unsafePath, nil)
  119. }