| ## `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. |