1## `filepath-securejoin` ##
2
3[](https://pkg.go.dev/github.com/cyphar/filepath-securejoin)
4[](https://github.com/cyphar/filepath-securejoin/actions/workflows/ci.yml)
5
6### Old API ###
7
8This library was originally just an implementation of `SecureJoin` which was
9[intended to be included in the Go standard library][go#20126] as a safer
10`filepath.Join` that would restrict the path lookup to be inside a root
11directory.
12
13The implementation was based on code that existed in several container
14runtimes. Unfortunately, this API is **fundamentally unsafe** against attackers
15that can modify path components after `SecureJoin` returns and before the
16caller uses the path, allowing for some fairly trivial TOCTOU attacks.
17
18`SecureJoin` (and `SecureJoinVFS`) are still provided by this library to
19support legacy users, but new users are strongly suggested to avoid using
20`SecureJoin` and instead use the [new api](#new-api) or switch to
21[libpathrs][libpathrs].
22
23With the above limitations in mind, this library guarantees the following:
24
25* If no error is set, the resulting string **must** be a child path of
26 `root` and will not contain any symlink path components (they will all be
27 expanded).
28
29* When expanding symlinks, all symlink path components **must** be resolved
30 relative to the provided root. In particular, this can be considered a
31 userspace implementation of how `chroot(2)` operates on file paths. Note that
32 these symlinks will **not** be expanded lexically (`filepath.Clean` is not
33 called on the input before processing).
34
35* Non-existent path components are unaffected by `SecureJoin` (similar to
36 `filepath.EvalSymlinks`'s semantics).
37
38* The returned path will always be `filepath.Clean`ed and thus not contain any
39 `..` components.
40
41A (trivial) implementation of this function on GNU/Linux systems could be done
42with the following (note that this requires root privileges and is far more
43opaque than the implementation in this library, and also requires that
44`readlink` is inside the `root` path and is trustworthy):
45
46```go
47package securejoin
48
49import (
50 "os/exec"
51 "path/filepath"
52)
53
54func SecureJoin(root, unsafePath string) (string, error) {
55 unsafePath = string(filepath.Separator) + unsafePath
56 cmd := exec.Command("chroot", root,
57 "readlink", "--canonicalize-missing", "--no-newline", unsafePath)
58 output, err := cmd.CombinedOutput()
59 if err != nil {
60 return "", err
61 }
62 expanded := string(output)
63 return filepath.Join(root, expanded), nil
64}
65```
66
67[libpathrs]: https://github.com/openSUSE/libpathrs
68[go#20126]: https://github.com/golang/go/issues/20126
69
70### <a name="new-api" /> New API ###
71[#new-api]: #new-api
72
73While we recommend users switch to [libpathrs][libpathrs] as soon as it has a
74stable release, some methods implemented by libpathrs have been ported to this
75library to ease the transition. These APIs are only supported on Linux.
76
77These APIs are implemented such that `filepath-securejoin` will
78opportunistically use certain newer kernel APIs that make these operations far
79more secure. In particular:
80
81* All of the lookup operations will use [`openat2`][openat2.2] on new enough
82 kernels (Linux 5.6 or later) to restrict lookups through magic-links and
83 bind-mounts (for certain operations) and to make use of `RESOLVE_IN_ROOT` to
84 efficiently resolve symlinks within a rootfs.
85
86* The APIs provide hardening against a malicious `/proc` mount to either detect
87 or avoid being tricked by a `/proc` that is not legitimate. This is done
88 using [`openat2`][openat2.2] for all users, and privileged users will also be
89 further protected by using [`fsopen`][fsopen.2] and [`open_tree`][open_tree.2]
90 (Linux 5.2 or later).
91
92[openat2.2]: https://www.man7.org/linux/man-pages/man2/openat2.2.html
93[fsopen.2]: https://github.com/brauner/man-pages-md/blob/main/fsopen.md
94[open_tree.2]: https://github.com/brauner/man-pages-md/blob/main/open_tree.md
95
96#### `OpenInRoot` ####
97
98```go
99func OpenInRoot(root, unsafePath string) (*os.File, error)
100func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error)
101func Reopen(handle *os.File, flags int) (*os.File, error)
102```
103
104`OpenInRoot` is a much safer version of
105
106```go
107path, err := securejoin.SecureJoin(root, unsafePath)
108file, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC)
109```
110
111that protects against various race attacks that could lead to serious security
112issues, depending on the application. Note that the returned `*os.File` is an
113`O_PATH` file descriptor, which is quite restricted. Callers will probably need
114to use `Reopen` to get a more usable handle (this split is done to provide
115useful features like PTY spawning and to avoid users accidentally opening bad
116inodes that could cause a DoS).
117
118Callers need to be careful in how they use the returned `*os.File`. Usually it
119is only safe to operate on the handle directly, and it is very easy to create a
120security issue. [libpathrs][libpathrs] provides far more helpers to make using
121these handles safer -- there is currently no plan to port them to
122`filepath-securejoin`.
123
124`OpenatInRoot` is like `OpenInRoot` except that the root is provided using an
125`*os.File`. This allows you to ensure that multiple `OpenatInRoot` (or
126`MkdirAllHandle`) calls are operating on the same rootfs.
127
128> **NOTE**: Unlike `SecureJoin`, `OpenInRoot` will error out as soon as it hits
129> a dangling symlink or non-existent path. This is in contrast to `SecureJoin`
130> which treated non-existent components as though they were real directories,
131> and would allow for partial resolution of dangling symlinks. These behaviours
132> are at odds with how Linux treats non-existent paths and dangling symlinks,
133> and so these are no longer allowed.
134
135#### `MkdirAll` ####
136
137```go
138func MkdirAll(root, unsafePath string, mode int) error
139func MkdirAllHandle(root *os.File, unsafePath string, mode int) (*os.File, error)
140```
141
142`MkdirAll` is a much safer version of
143
144```go
145path, err := securejoin.SecureJoin(root, unsafePath)
146err = os.MkdirAll(path, mode)
147```
148
149that protects against the same kinds of races that `OpenInRoot` protects
150against.
151
152`MkdirAllHandle` is like `MkdirAll` except that the root is provided using an
153`*os.File` (the reason for this is the same as with `OpenatInRoot`) and an
154`*os.File` of the final created directory is returned (this directory is
155guaranteed to be effectively identical to the directory created by
156`MkdirAllHandle`, which is not possible to ensure by just using `OpenatInRoot`
157after `MkdirAll`).
158
159> **NOTE**: Unlike `SecureJoin`, `MkdirAll` will error out as soon as it hits
160> a dangling symlink or non-existent path. This is in contrast to `SecureJoin`
161> which treated non-existent components as though they were real directories,
162> and would allow for partial resolution of dangling symlinks. These behaviours
163> are at odds with how Linux treats non-existent paths and dangling symlinks,
164> and so these are no longer allowed. This means that `MkdirAll` will not
165> create non-existent directories referenced by a dangling symlink.
166
167### License ###
168
169`SPDX-License-Identifier: BSD-3-Clause AND MPL-2.0`
170
171Some of the code in this project is derived from Go, and is licensed under a
172BSD 3-clause license (available in `LICENSE.BSD`). Other files (many of which
173are derived from [libpathrs][libpathrs]) are licensed under the Mozilla Public
174License version 2.0 (available in `LICENSE.MPL-2.0`). If you are using the
175["New API" described above][#new-api], you are probably using code from files
176released under this license.
177
178Every source file in this project has a copyright header describing its
179license. Please check the license headers of each file to see what license
180applies to it.
181
182See [COPYING.md](./COPYING.md) for some more details.
183
184[umoci]: https://github.com/opencontainers/umoci