forked from toolshed/abra
		
	
		
			
				
	
	
		
			170 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			170 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
## `filepath-securejoin` ##
 | 
						|
 | 
						|
[](https://pkg.go.dev/github.com/cyphar/filepath-securejoin)
 | 
						|
[](https://github.com/cyphar/filepath-securejoin/actions/workflows/ci.yml)
 | 
						|
 | 
						|
### Old API ###
 | 
						|
 | 
						|
This library was originally just an implementation of `SecureJoin` which was
 | 
						|
[intended to be included in the Go standard library][go#20126] as a safer
 | 
						|
`filepath.Join` that would restrict the path lookup to be inside a root
 | 
						|
directory.
 | 
						|
 | 
						|
The implementation was based on code that existed in several container
 | 
						|
runtimes. Unfortunately, this API is **fundamentally unsafe** against attackers
 | 
						|
that can modify path components after `SecureJoin` returns and before the
 | 
						|
caller uses the path, allowing for some fairly trivial TOCTOU attacks.
 | 
						|
 | 
						|
`SecureJoin` (and `SecureJoinVFS`) are still provided by this library to
 | 
						|
support legacy users, but new users are strongly suggested to avoid using
 | 
						|
`SecureJoin` and instead use the [new api](#new-api) or switch to
 | 
						|
[libpathrs][libpathrs].
 | 
						|
 | 
						|
With the above limitations in mind, this library guarantees the following:
 | 
						|
 | 
						|
* If no error is set, the resulting string **must** be a child path of
 | 
						|
  `root` and will not contain any symlink path components (they will all be
 | 
						|
  expanded).
 | 
						|
 | 
						|
* When expanding symlinks, all symlink path components **must** be resolved
 | 
						|
  relative to the provided root. In particular, this can be considered a
 | 
						|
  userspace implementation of how `chroot(2)` operates on file paths. Note that
 | 
						|
  these symlinks will **not** be expanded lexically (`filepath.Clean` is not
 | 
						|
  called on the input before processing).
 | 
						|
 | 
						|
* Non-existent path components are unaffected by `SecureJoin` (similar to
 | 
						|
  `filepath.EvalSymlinks`'s semantics).
 | 
						|
 | 
						|
* The returned path will always be `filepath.Clean`ed and thus not contain any
 | 
						|
  `..` components.
 | 
						|
 | 
						|
A (trivial) implementation of this function on GNU/Linux systems could be done
 | 
						|
with the following (note that this requires root privileges and is far more
 | 
						|
opaque than the implementation in this library, and also requires that
 | 
						|
`readlink` is inside the `root` path and is trustworthy):
 | 
						|
 | 
						|
```go
 | 
						|
package securejoin
 | 
						|
 | 
						|
import (
 | 
						|
	"os/exec"
 | 
						|
	"path/filepath"
 | 
						|
)
 | 
						|
 | 
						|
func SecureJoin(root, unsafePath string) (string, error) {
 | 
						|
	unsafePath = string(filepath.Separator) + unsafePath
 | 
						|
	cmd := exec.Command("chroot", root,
 | 
						|
		"readlink", "--canonicalize-missing", "--no-newline", unsafePath)
 | 
						|
	output, err := cmd.CombinedOutput()
 | 
						|
	if err != nil {
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
	expanded := string(output)
 | 
						|
	return filepath.Join(root, expanded), nil
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
[libpathrs]: https://github.com/openSUSE/libpathrs
 | 
						|
[go#20126]: https://github.com/golang/go/issues/20126
 | 
						|
 | 
						|
### New API ###
 | 
						|
 | 
						|
While we recommend users switch to [libpathrs][libpathrs] as soon as it has a
 | 
						|
stable release, some methods implemented by libpathrs have been ported to this
 | 
						|
library to ease the transition. These APIs are only supported on Linux.
 | 
						|
 | 
						|
These APIs are implemented such that `filepath-securejoin` will
 | 
						|
opportunistically use certain newer kernel APIs that make these operations far
 | 
						|
more secure. In particular:
 | 
						|
 | 
						|
* All of the lookup operations will use [`openat2`][openat2.2] on new enough
 | 
						|
  kernels (Linux 5.6 or later) to restrict lookups through magic-links and
 | 
						|
  bind-mounts (for certain operations) and to make use of `RESOLVE_IN_ROOT` to
 | 
						|
  efficiently resolve symlinks within a rootfs.
 | 
						|
 | 
						|
* The APIs provide hardening against a malicious `/proc` mount to either detect
 | 
						|
  or avoid being tricked by a `/proc` that is not legitimate. This is done
 | 
						|
  using [`openat2`][openat2.2] for all users, and privileged users will also be
 | 
						|
  further protected by using [`fsopen`][fsopen.2] and [`open_tree`][open_tree.2]
 | 
						|
  (Linux 5.2 or later).
 | 
						|
 | 
						|
[openat2.2]: https://www.man7.org/linux/man-pages/man2/openat2.2.html
 | 
						|
[fsopen.2]: https://github.com/brauner/man-pages-md/blob/main/fsopen.md
 | 
						|
[open_tree.2]: https://github.com/brauner/man-pages-md/blob/main/open_tree.md
 | 
						|
 | 
						|
#### `OpenInRoot` ####
 | 
						|
 | 
						|
```go
 | 
						|
func OpenInRoot(root, unsafePath string) (*os.File, error)
 | 
						|
func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error)
 | 
						|
func Reopen(handle *os.File, flags int) (*os.File, error)
 | 
						|
```
 | 
						|
 | 
						|
`OpenInRoot` is a much safer version of
 | 
						|
 | 
						|
```go
 | 
						|
path, err := securejoin.SecureJoin(root, unsafePath)
 | 
						|
file, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC)
 | 
						|
```
 | 
						|
 | 
						|
that protects against various race attacks that could lead to serious security
 | 
						|
issues, depending on the application. Note that the returned `*os.File` is an
 | 
						|
`O_PATH` file descriptor, which is quite restricted. Callers will probably need
 | 
						|
to use `Reopen` to get a more usable handle (this split is done to provide
 | 
						|
useful features like PTY spawning and to avoid users accidentally opening bad
 | 
						|
inodes that could cause a DoS).
 | 
						|
 | 
						|
Callers need to be careful in how they use the returned `*os.File`. Usually it
 | 
						|
is only safe to operate on the handle directly, and it is very easy to create a
 | 
						|
security issue. [libpathrs][libpathrs] provides far more helpers to make using
 | 
						|
these handles safer -- there is currently no plan to port them to
 | 
						|
`filepath-securejoin`.
 | 
						|
 | 
						|
`OpenatInRoot` is like `OpenInRoot` except that the root is provided using an
 | 
						|
`*os.File`. This allows you to ensure that multiple `OpenatInRoot` (or
 | 
						|
`MkdirAllHandle`) calls are operating on the same rootfs.
 | 
						|
 | 
						|
> **NOTE**: Unlike `SecureJoin`, `OpenInRoot` will error out as soon as it hits
 | 
						|
> a dangling symlink or non-existent path. This is in contrast to `SecureJoin`
 | 
						|
> which treated non-existent components as though they were real directories,
 | 
						|
> and would allow for partial resolution of dangling symlinks. These behaviours
 | 
						|
> are at odds with how Linux treats non-existent paths and dangling symlinks,
 | 
						|
> and so these are no longer allowed.
 | 
						|
 | 
						|
#### `MkdirAll` ####
 | 
						|
 | 
						|
```go
 | 
						|
func MkdirAll(root, unsafePath string, mode int) error
 | 
						|
func MkdirAllHandle(root *os.File, unsafePath string, mode int) (*os.File, error)
 | 
						|
```
 | 
						|
 | 
						|
`MkdirAll` is a much safer version of
 | 
						|
 | 
						|
```go
 | 
						|
path, err := securejoin.SecureJoin(root, unsafePath)
 | 
						|
err = os.MkdirAll(path, mode)
 | 
						|
```
 | 
						|
 | 
						|
that protects against the same kinds of races that `OpenInRoot` protects
 | 
						|
against.
 | 
						|
 | 
						|
`MkdirAllHandle` is like `MkdirAll` except that the root is provided using an
 | 
						|
`*os.File` (the reason for this is the same as with `OpenatInRoot`) and an
 | 
						|
`*os.File` of the final created directory is returned (this directory is
 | 
						|
guaranteed to be effectively identical to the directory created by
 | 
						|
`MkdirAllHandle`, which is not possible to ensure by just using `OpenatInRoot`
 | 
						|
after `MkdirAll`).
 | 
						|
 | 
						|
> **NOTE**: Unlike `SecureJoin`, `MkdirAll` will error out as soon as it hits
 | 
						|
> a dangling symlink or non-existent path. This is in contrast to `SecureJoin`
 | 
						|
> which treated non-existent components as though they were real directories,
 | 
						|
> and would allow for partial resolution of dangling symlinks. These behaviours
 | 
						|
> are at odds with how Linux treats non-existent paths and dangling symlinks,
 | 
						|
> and so these are no longer allowed. This means that `MkdirAll` will not
 | 
						|
> create non-existent directories referenced by a dangling symlink.
 | 
						|
 | 
						|
### License ###
 | 
						|
 | 
						|
The license of this project is the same as Go, which is a BSD 3-clause license
 | 
						|
available in the `LICENSE` file.
 |