mirror of
https://github.com/rocky-linux/peridot.git
synced 2024-11-30 16:46:27 +00:00
169 lines
6.9 KiB
Markdown
169 lines
6.9 KiB
Markdown
## `filepath-securejoin` ##
|
|
|
|
[![Build Status](https://github.com/cyphar/filepath-securejoin/actions/workflows/ci.yml/badge.svg)](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 4.18 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.
|