234 lines
9.3 KiB
Markdown
234 lines
9.3 KiB
Markdown
# fs-mistrust
|
|
|
|
## `fs-mistrust`: check whether file permissions are private.
|
|
|
|
This crate provides a set of functionality to check the permissions on files
|
|
and directories to ensure that they are effectively private—that is, that
|
|
they are only readable or writable by trusted[^1] users.
|
|
|
|
This kind of check can protect your users' data against misconfigurations,
|
|
such as cases where they've accidentally made their home directory
|
|
world-writable, or where they're using a symlink stored in a directory owned
|
|
by another user.
|
|
|
|
The checks in this crate try to guarantee that, after a path has been shown
|
|
to be private, no action by a _non-trusted user_ can make that path private.
|
|
It's still possible for a _trusted user_ to change a path after it has been
|
|
checked. Because of that, you may want to use other mechanisms if you are
|
|
concerned about time-of-check/time-of-use issues caused by _trusted_ users
|
|
altering the filesystem.
|
|
|
|
Also see the [Limitations](#limitations) section below.
|
|
|
|
[^1]: we define "trust" here in the computer-security sense of the word: a
|
|
user is "trusted" if they have the opportunity to break our security
|
|
guarantees. For example, `root` on a Unix environment is "trusted",
|
|
whether you actually trust them or not.
|
|
|
|
### What's so hard about checking permissions?
|
|
|
|
Suppose that we want to know whether a given path can be read or modified by
|
|
an untrusted user. That's trickier than it sounds:
|
|
|
|
* Even if the permissions on the file itself are correct, we also need to
|
|
check the permissions on the directory holding it, since they might allow
|
|
an untrusted user to replace the file, or change its permissions.
|
|
* Similarly, we need to check the permissions on the parent of _that_
|
|
directory, since they might let an untrusted user replace the directory or
|
|
change _its_ permissions. (And so on!)
|
|
* It can be tricky to define "a trusted user". On Unix systems, we usually
|
|
say that each user is trusted by themself, and that root (UID 0) is
|
|
trusted. But it's hard to say which _groups_ are trusted: even if a given
|
|
group contains only trusted users today, there's no OS-level guarantee
|
|
that untrusted users won't be added to that group in the future.
|
|
* Symbolic links add another layer of confusion. If there are any symlinks
|
|
in the path you're checking, then you need to check permissions on the
|
|
directory containing the symlink, and then the permissions on the target
|
|
path, _and all of its ancestors_ too.
|
|
* Many programs first canonicalize the path being checked, removing all
|
|
`..`s and symlinks. That's sufficient for telling whether the _final_
|
|
file can be modified by an untrusted user, but not for whether the _path_
|
|
can be modified by an untrusted user. If there is a modifiable symlink in
|
|
the middle of the path, or at any stage of the path resolution, somebody
|
|
who can modify that symlink can change which file the path points to.
|
|
* Even if you have checked a directory as being writeable only by a trusted
|
|
user, that doesn't mean that the objects _in_ that directory are only
|
|
writeable by trusted users. Those objects might be symlinks to some other
|
|
(more writeable) place on the file system; or they might be accessible
|
|
with hard links stored elsewhere on the file system.
|
|
|
|
Different programs try to solve this problem in different ways, often with
|
|
very little rationale. This crate tries to give a reasonable implementation
|
|
for file privacy checking and enforcement, along with clear justifications
|
|
in its source for why it behaves that way.
|
|
|
|
|
|
### What we actually do
|
|
|
|
To make sure that every step in the file resolution process is checked, we
|
|
emulate that process on our own. We inspect each component in the provided
|
|
path, to see whether it is modifiable by an untrusted user. If we encounter
|
|
one or more symlinks, then we resolve every component of the path added by
|
|
those symlink, until we finally reach the target.
|
|
|
|
In effect, we are emulating `realpath` (or `fs::canonicalize` if you
|
|
prefer), and looking at the permissions on every part of the filesystem we
|
|
touch in doing so, to see who has permissions to change our target file or
|
|
the process that led us to it.
|
|
|
|
For groups, we use the following heuristic: If there is a group with the
|
|
same name as the current user, and the current user belongs to that group,
|
|
we assume that group is trusted. Otherwise, we treat all groups as
|
|
untrusted.
|
|
|
|
### Examples
|
|
|
|
#### Simple cases
|
|
|
|
Make sure that a directory is only readable or writeable by us (simple
|
|
case):
|
|
|
|
```rust
|
|
use fs_mistrust::Mistrust;
|
|
match Mistrust::new().check_directory("/home/itchy/.local/hat-swap") {
|
|
Ok(()) => println!("directory is good"),
|
|
Err(e) => println!("problem with our hat-swap directory: {}", e),
|
|
}
|
|
```
|
|
|
|
As above, but create the directory, and its parents if they do not already
|
|
exist.
|
|
|
|
```rust
|
|
use fs_mistrust::Mistrust;
|
|
match Mistrust::new().make_directory("/home/itchy/.local/hat-swap") {
|
|
Ok(()) => println!("directory exists (or was created without trouble"),
|
|
Err(e) => println!("problem with our hat-swap directory: {}", e),
|
|
}
|
|
```
|
|
|
|
#### Configuring a [`Mistrust`]
|
|
|
|
You can adjust the [`Mistrust`] object to change what it permits:
|
|
|
|
```rust
|
|
use fs_mistrust::Mistrust;
|
|
|
|
let my_mistrust = Mistrust::builder()
|
|
// Assume that our home directory and its parents are all well-configured.
|
|
.ignore_prefix("/home/doze/")
|
|
// Assume that a given group will only contain trusted users.
|
|
.trust_group(413)
|
|
.build()?;
|
|
```
|
|
|
|
See [`Mistrust`] for more options.
|
|
|
|
#### Using [`Verifier`] for more fine-grained checks
|
|
|
|
For more fine-grained control over a specific check, you can use the
|
|
[`Verifier`] API. Unlike [`Mistrust`], which generally you'll want to
|
|
configure for several requests, the changes in [`Verifier`] generally make
|
|
sense only for one request at a time.
|
|
|
|
```rust
|
|
use fs_mistrust::Mistrust;
|
|
let mistrust = Mistrust::new();
|
|
|
|
// Require that an object is a regular file; allow it to be world-
|
|
// readable.
|
|
mistrust
|
|
.verifier()
|
|
.permit_readable()
|
|
.require_file()
|
|
.check("/home/trace/.path_cfg")?;
|
|
|
|
// Make sure that a directory _and all of its contents_ are private.
|
|
// Create the directory if it does not exist.
|
|
// Return an error object containing _all_ of the problems discovered.
|
|
mistrust
|
|
.verifier()
|
|
.require_directory()
|
|
.check_content()
|
|
.all_errors()
|
|
.make_directory("/home/trace/private_keys/");
|
|
```
|
|
|
|
See [`Verifier`] for more options.
|
|
|
|
#### Using [`CheckedDir`] for safety.
|
|
|
|
You can use the [`CheckedDir`] API to ensure not only that a directory is
|
|
private, but that all of your accesses to its contents continue to verify
|
|
and enforce _their_ permissions.
|
|
|
|
```rust
|
|
use fs_mistrust::{Mistrust, CheckedDir};
|
|
use std::fs::{File, OpenOptions};
|
|
let dir = Mistrust::new()
|
|
.verifier()
|
|
.secure_dir("/Users/clover/riddles")?;
|
|
|
|
// You can use the CheckedDir object to access files and directories.
|
|
// All of these must be relative paths within the path you used to
|
|
// build the CheckedDir.
|
|
dir.make_directory("timelines")?;
|
|
let file = dir.open("timelines/vault-destroyed.md",
|
|
OpenOptions::new().write(true).create(true))?;
|
|
// (... use file...)
|
|
```
|
|
|
|
### Limitations
|
|
|
|
As noted above, this crate only checks whether a path can be changed by
|
|
_non-trusted_ users. After the path has been checked, a _trusted_ user can
|
|
still change its permissions. (For example, the user could make their home
|
|
directory world-writable.) This crate does not try to defend against _that
|
|
kind_ of time-of-check/time-of-use issue.
|
|
|
|
We currently assume a fairly vanilla Unix environment: we'll tolerate other
|
|
systems, but we don't actually look at the details of any of these:
|
|
* Windows security (ACLs, SecurityDescriptors, etc)
|
|
* SELinux capabilities
|
|
* POSIX (and other) ACLs.
|
|
|
|
We use a somewhat inaccurate heuristic when we're checking the permissions
|
|
of items _inside_ a target directory (using [`Verifier::check_content`] or
|
|
[`CheckedDir`]): we continue to forbid untrusted-writeable directories and
|
|
files, but we still allow readable ones, even if we insisted that the target
|
|
directory itself was required to to be unreadable. This is too permissive
|
|
in the case of readable objects with hard links: if there is a hard link to
|
|
the file somewhere else, then an untrusted user can read it. It is also too
|
|
restrictive in the case of writeable objects _without_ hard links: if
|
|
untrusted users have no path to those objects, they can't actually write
|
|
them.
|
|
|
|
On Windows, we accept all file permissions and owners.
|
|
|
|
We don't check for mount-points and the privacy of filesystem devices
|
|
themselves. (For example, we don't distinguish between our local
|
|
administrator and the administrator of a remote filesystem. We also don't
|
|
distinguish between local filesystems and insecure networked filesystems.)
|
|
|
|
This code has not been audited for correct operation in a setuid
|
|
environment; there are almost certainly security holes in that case.
|
|
|
|
This is fairly new software, and hasn't been audited yet.
|
|
|
|
All of the above issues are considered "good to fix, if practical".
|
|
|
|
### Acknowledgements
|
|
|
|
The list of checks performed here was inspired by the lists from OpenSSH's
|
|
[safe_path], GnuPG's [check_permissions], and Tor's [check_private_dir]. All
|
|
errors are my own.
|
|
|
|
[safe_path]:
|
|
https://github.com/openssh/openssh-portable/blob/master/misc.c#L2177
|
|
[check_permissions]:
|
|
https://github.com/gpg/gnupg/blob/master/g10/gpg.c#L1551
|
|
[check_private_dir]:
|
|
https://gitlab.torproject.org/tpo/core/tor/-/blob/main/src/lib/fs/dir.c#L70
|
|
|
|
License: MIT OR Apache-2.0
|