Crates.io | fs-mistrust |
lib.rs | fs-mistrust |
version | 0.8.2 |
source | src |
created_at | 2022-05-06 15:06:04.683061 |
updated_at | 2024-12-02 17:55:06.19804 |
description | Ensure that files can only be read or written by trusted users |
homepage | https://gitlab.torproject.org/tpo/core/arti/-/wikis/home |
repository | https://gitlab.torproject.org/tpo/core/arti.git/ |
max_upload_size | |
id | 581732 |
size | 161,155 |
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 trusted1 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 section below.
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:
..
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.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.
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.
Make sure that a directory is only readable or writeable by us (simple case):
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.
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),
}
Mistrust
]You can adjust the [Mistrust
] object to change what it permits:
# fn main() -> Result<(), fs_mistrust::Error> {
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 (this feature is only
// available on Unix-like platforms).
// .trust_group(413)
.build()?;
# Ok(())
# }
See [Mistrust
] for more options.
Verifier
] for more fine-grained checksFor 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.
# fn main() -> Result<(), fs_mistrust::Error> {
# #[cfg(feature = "walkdir")] {
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/");
# }
# Ok(())
# }
See [Verifier
] for more options.
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.
# fn main() -> Result<(), fs_mistrust::Error> {
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...)
# Ok(())
# }
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:
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".
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.
License: MIT OR Apache-2.0
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. ↩