#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg))]
#![doc = include_str!("../README.md")]
#![allow(renamed_and_removed_lints)] #![allow(unknown_lints)] #![warn(missing_docs)]
#![warn(noop_method_call)]
#![warn(unreachable_pub)]
#![warn(clippy::all)]
#![deny(clippy::await_holding_lock)]
#![deny(clippy::cargo_common_metadata)]
#![deny(clippy::cast_lossless)]
#![deny(clippy::checked_conversions)]
#![warn(clippy::cognitive_complexity)]
#![deny(clippy::debug_assert_with_mut_call)]
#![deny(clippy::exhaustive_enums)]
#![deny(clippy::exhaustive_structs)]
#![deny(clippy::expl_impl_clone_on_copy)]
#![deny(clippy::fallible_impl_from)]
#![deny(clippy::implicit_clone)]
#![deny(clippy::large_stack_arrays)]
#![warn(clippy::manual_ok_or)]
#![deny(clippy::missing_docs_in_private_items)]
#![warn(clippy::needless_borrow)]
#![warn(clippy::needless_pass_by_value)]
#![warn(clippy::option_option)]
#![deny(clippy::print_stderr)]
#![deny(clippy::print_stdout)]
#![warn(clippy::rc_buffer)]
#![deny(clippy::ref_option_ref)]
#![warn(clippy::semicolon_if_nothing_returned)]
#![warn(clippy::trait_duplication_in_bounds)]
#![deny(clippy::unchecked_duration_subtraction)]
#![deny(clippy::unnecessary_wraps)]
#![warn(clippy::unseparated_literal_suffix)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::mod_module_files)]
#![allow(clippy::let_unit_value)] #![allow(clippy::uninlined_format_args)]
#![allow(clippy::significant_drop_in_scrutinee)] #![allow(clippy::result_large_err)] #![allow(clippy::needless_raw_string_hashes)] #![allow(clippy::needless_lifetimes)]
#![forbid(unsafe_code)]
mod dir;
mod disable;
mod err;
mod file_access;
mod imp;
#[cfg(all(
target_family = "unix",
not(target_os = "ios"),
not(target_os = "android"),
not(target_os = "tvos")
))]
mod user;
#[cfg(feature = "anon_home")]
pub mod anon_home;
#[cfg(test)]
pub(crate) mod testing;
pub mod walk;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::{
fs::DirBuilder,
path::{Path, PathBuf},
sync::Arc,
};
pub use dir::CheckedDir;
pub use disable::GLOBAL_DISABLE_VAR;
pub use err::{format_access_bits, Error};
pub use file_access::FileAccess;
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(all(
target_family = "unix",
not(target_os = "ios"),
not(target_os = "android"),
not(target_os = "tvos")
))]
pub use user::{TrustedGroup, TrustedUser};
#[derive(Debug, Clone, derive_builder::Builder, Eq, PartialEq)]
#[cfg_attr(feature = "serde", builder(derive(Debug, Serialize, Deserialize)))]
#[cfg_attr(not(feature = "serde"), builder(derive(Debug)))]
#[builder(build_fn(error = "Error"))]
#[cfg_attr(feature = "serde", builder_struct_attr(serde(default)))]
pub struct Mistrust {
#[builder(
setter(into, strip_option),
field(build = "canonicalize_opt_prefix(&self.ignore_prefix)?")
)]
ignore_prefix: Option<PathBuf>,
#[builder(setter(custom), field(type = "Option<bool>", build = "()"))]
dangerously_trust_everyone: (),
#[builder(setter(custom), field(type = "Option<disable::Disable>", build = "()"))]
#[cfg_attr(feature = "serde", builder_field_attr(serde(skip)))]
disable_by_environment: (),
#[builder(setter(custom), field(build = "self.should_be_enabled()"))]
#[cfg_attr(feature = "serde", builder_field_attr(serde(skip)))]
status: disable::Status,
#[cfg(all(
target_family = "unix",
not(target_os = "ios"),
not(target_os = "android"),
not(target_os = "tvos")
))]
#[builder(
setter(into),
field(type = "TrustedUser", build = "self.trust_user.get_uid()?")
)]
trust_user: Option<u32>,
#[cfg(all(
target_family = "unix",
not(target_os = "ios"),
not(target_os = "android"),
not(target_os = "tvos")
))]
#[builder(
setter(into),
field(type = "TrustedGroup", build = "self.trust_group.get_gid()?")
)]
trust_group: Option<u32>,
}
#[allow(clippy::option_option)]
fn canonicalize_opt_prefix(prefix: &Option<Option<PathBuf>>) -> Result<Option<PathBuf>> {
match prefix {
Some(Some(path)) if path.as_os_str().is_empty() => Ok(None),
Some(Some(path)) => Ok(Some(
path.canonicalize()
.map_err(|e| Error::inspecting(e, path))?,
)),
_ => Ok(None),
}
}
impl MistrustBuilder {
#[cfg(all(
target_family = "unix",
not(target_os = "ios"),
not(target_os = "android"),
not(target_os = "tvos"),
))]
pub fn trust_admin_only(&mut self) -> &mut Self {
self.trust_user = TrustedUser::None;
self.trust_group = TrustedGroup::None;
self
}
#[cfg(all(
target_family = "unix",
not(target_os = "ios"),
not(target_os = "android"),
not(target_os = "tvos"),
))]
pub fn trust_no_group_id(&mut self) -> &mut Self {
self.trust_group = TrustedGroup::None;
self
}
pub fn dangerously_trust_everyone(&mut self) -> &mut Self {
self.dangerously_trust_everyone = Some(true);
self
}
pub fn remove_ignored_prefix(&mut self) -> &mut Self {
self.ignore_prefix = Some(None);
self
}
pub fn controlled_by_env_var(&mut self, var: &str) -> &mut Self {
self.disable_by_environment = Some(disable::Disable::OnUserEnvVar(var.to_string()));
self
}
pub fn controlled_by_env_var_if_not_set(&mut self, var: &str) -> &mut Self {
if self.disable_by_environment.is_none() {
self.controlled_by_env_var(var)
} else {
self
}
}
pub fn controlled_by_default_env_var(&mut self) -> &mut Self {
self.disable_by_environment = Some(disable::Disable::OnGlobalEnvVar);
self
}
pub fn ignore_environment(&mut self) -> &mut Self {
self.disable_by_environment = Some(disable::Disable::Never);
self
}
fn should_be_enabled(&self) -> disable::Status {
if self.dangerously_trust_everyone == Some(true) {
return disable::Status::DisableChecks;
}
self.disable_by_environment
.as_ref()
.unwrap_or(&disable::Disable::default())
.should_disable_checks()
}
}
impl Default for Mistrust {
fn default() -> Self {
MistrustBuilder::default()
.build()
.expect("Could not build default")
}
}
#[derive(Clone, Debug)]
#[must_use]
pub struct Verifier<'a> {
mistrust: &'a Mistrust,
readable_okay: bool,
collect_multiple_errors: bool,
enforce_type: Type,
check_contents: bool,
}
#[derive(Debug, Clone, Copy)]
enum Type {
Dir,
File,
DirOrFile,
Anything,
}
impl Mistrust {
pub fn builder() -> MistrustBuilder {
MistrustBuilder::default()
}
pub fn new() -> Self {
Self::default()
}
pub fn new_dangerously_trust_everyone() -> Self {
Self::builder()
.dangerously_trust_everyone()
.build()
.expect("Could not construct a Mistrust")
}
pub fn verifier(&self) -> Verifier<'_> {
Verifier {
mistrust: self,
readable_okay: false,
collect_multiple_errors: false,
enforce_type: Type::DirOrFile,
check_contents: false,
}
}
pub fn check_directory<P: AsRef<Path>>(&self, dir: P) -> Result<()> {
self.verifier().require_directory().check(dir)
}
pub fn make_directory<P: AsRef<Path>>(&self, dir: P) -> Result<()> {
self.verifier().make_directory(dir)
}
pub(crate) fn is_disabled(&self) -> bool {
self.status.disabled()
}
pub fn file_access(&self) -> FileAccess<'_> {
self.verifier().file_access()
}
}
impl<'a> Verifier<'a> {
pub fn file_access(self) -> FileAccess<'a> {
FileAccess::from_verifier(self)
}
pub fn require_file(mut self) -> Self {
self.enforce_type = Type::File;
self
}
pub fn require_directory(mut self) -> Self {
self.enforce_type = Type::Dir;
self
}
pub fn permit_all_object_types(mut self) -> Self {
self.enforce_type = Type::Anything;
self
}
pub fn permit_readable(mut self) -> Self {
self.readable_okay = true;
self
}
pub fn all_errors(mut self) -> Self {
self.collect_multiple_errors = true;
self
}
#[cfg(feature = "walkdir")]
pub fn check_content(mut self) -> Self {
self.check_contents = true;
self.require_directory()
}
pub fn check<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let path = path.as_ref();
let mut error_iterator = self
.check_errors(path.as_ref())
.chain(self.check_content_errors(path.as_ref()));
let opt_error: Option<Error> = if self.collect_multiple_errors {
error_iterator.collect()
} else {
let next = error_iterator.next();
drop(error_iterator); next
};
if let Some(err) = opt_error {
return Err(err);
}
Ok(())
}
pub fn make_directory<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
self.enforce_type = Type::Dir;
let path = path.as_ref();
match self.clone().check(path) {
Err(Error::NotFound(_)) => {}
Err(other_error) => return Err(other_error),
Ok(()) => return Ok(()), }
let mut bld = DirBuilder::new();
#[cfg(target_family = "unix")]
{
use std::os::unix::fs::DirBuilderExt;
bld.mode(0o700);
}
bld.recursive(true)
.create(path)
.map_err(|e| Error::CreatingDir(Arc::new(e)))?;
self.check(path)
}
pub fn secure_dir<P: AsRef<Path>>(self, path: P) -> Result<CheckedDir> {
let path = path.as_ref();
self.clone().require_directory().check(path)?;
CheckedDir::new(&self, path)
}
pub fn make_secure_dir<P: AsRef<Path>>(self, path: P) -> Result<CheckedDir> {
let path = path.as_ref();
self.clone().require_directory().make_directory(path)?;
CheckedDir::new(&self, path)
}
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_duration_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use super::*;
use testing::{mistrust_build, Dir, MistrustOp};
#[cfg(target_family = "unix")]
use testing::LinkType;
#[cfg(target_family = "unix")]
#[test]
fn simple_cases() {
let d = Dir::new();
d.dir("a/b/c");
d.dir("e/f/g");
d.chmod("a", 0o755);
d.chmod("a/b", 0o755);
d.chmod("a/b/c", 0o700);
d.chmod("e", 0o755);
d.chmod("e/f", 0o777);
d.link_rel(LinkType::Dir, "a/b/c", "d");
let m = mistrust_build(&[
MistrustOp::IgnorePrefix(d.canonical_root()),
MistrustOp::TrustNoGroupId(),
]);
m.check_directory(d.path("a/b/c")).unwrap();
let e = m.check_directory(d.path("e/f/g")).unwrap_err();
assert!(matches!(e, Error::BadPermission(_, 0o777, 0o022)));
assert_eq!(e.path().unwrap(), d.path("e/f").canonicalize().unwrap());
m.check_directory(d.path("d")).unwrap();
}
#[cfg(target_family = "unix")]
#[test]
fn admin_only() {
use std::os::unix::prelude::MetadataExt;
let d = Dir::new();
d.dir("a/b");
d.chmod("a", 0o700);
d.chmod("a/b", 0o700);
if d.path("a/b").metadata().unwrap().uid() == 0 {
return;
}
let m = mistrust_build(&[MistrustOp::IgnorePrefix(d.canonical_root())]);
m.check_directory(d.path("a/b")).unwrap();
let m = mistrust_build(&[
MistrustOp::IgnorePrefix(d.canonical_root()),
MistrustOp::TrustAdminOnly(),
]);
let err = m.check_directory(d.path("a/b")).unwrap_err();
assert!(matches!(err, Error::BadOwner(_, _)));
assert_eq!(err.path().unwrap(), d.path("a").canonicalize().unwrap());
}
#[test]
fn want_type() {
let d = Dir::new();
d.dir("a");
d.file("b");
d.chmod("a", 0o700);
d.chmod("b", 0o600);
let m = mistrust_build(&[
MistrustOp::IgnorePrefix(d.canonical_root()),
MistrustOp::TrustNoGroupId(),
]);
m.verifier().require_directory().check(d.path("a")).unwrap();
m.verifier().require_file().check(d.path("b")).unwrap();
let e = m
.verifier()
.require_directory()
.check(d.path("b"))
.unwrap_err();
assert!(matches!(e, Error::BadType(_)));
assert_eq!(e.path().unwrap(), d.path("b").canonicalize().unwrap());
let e = m.verifier().require_file().check(d.path("a")).unwrap_err();
assert!(matches!(e, Error::BadType(_)));
assert_eq!(e.path().unwrap(), d.path("a").canonicalize().unwrap());
}
#[cfg(target_family = "unix")]
#[test]
fn readable_ok() {
let d = Dir::new();
d.dir("a/b");
d.file("a/b/c");
d.chmod("a", 0o750);
d.chmod("a/b", 0o750);
d.chmod("a/b/c", 0o640);
let m = mistrust_build(&[
MistrustOp::IgnorePrefix(d.canonical_root()),
MistrustOp::TrustNoGroupId(),
]);
let e = m.verifier().check(d.path("a/b")).unwrap_err();
assert!(matches!(e, Error::BadPermission(..)));
assert_eq!(e.path().unwrap(), d.path("a/b").canonicalize().unwrap());
let e = m.verifier().check(d.path("a/b/c")).unwrap_err();
assert!(matches!(e, Error::BadPermission(..)));
assert_eq!(e.path().unwrap(), d.path("a/b/c").canonicalize().unwrap());
m.verifier().permit_readable().check(d.path("a/b")).unwrap();
m.verifier()
.permit_readable()
.check(d.path("a/b/c"))
.unwrap();
}
#[cfg(target_family = "unix")]
#[test]
fn multiple_errors() {
let d = Dir::new();
d.dir("a/b");
d.chmod("a", 0o700);
d.chmod("a/b", 0o700);
let m = mistrust_build(&[
MistrustOp::IgnorePrefix(d.canonical_root()),
MistrustOp::TrustNoGroupId(),
]);
let e = m
.verifier()
.all_errors()
.check(d.path("a/b/c"))
.unwrap_err();
assert!(matches!(e, Error::NotFound(_)));
assert_eq!(1, e.errors().count());
d.chmod("a/b", 0o770);
let e = m
.verifier()
.all_errors()
.check(d.path("a/b/c"))
.unwrap_err();
assert!(matches!(e, Error::Multiple(_)));
let errs: Vec<_> = e.errors().collect();
assert_eq!(2, errs.len());
assert!(matches!(&errs[0], Error::BadPermission(..)));
assert!(matches!(&errs[1], Error::NotFound(_)));
}
#[cfg(target_family = "unix")]
#[test]
fn sticky() {
let d = Dir::new();
d.dir("a/b/c");
d.chmod("a", 0o777);
d.chmod("a/b", 0o755);
d.chmod("a/b/c", 0o700);
let m = mistrust_build(&[MistrustOp::IgnorePrefix(d.canonical_root())]);
m.check_directory(d.path("a/b/c")).unwrap_err();
d.chmod("a", 0o777 | crate::imp::STICKY_BIT);
m.check_directory(d.path("a/b/c")).unwrap();
#[allow(clippy::useless_conversion)]
{
assert_eq!(crate::imp::STICKY_BIT, u32::from(libc::S_ISVTX));
}
}
#[cfg(target_family = "unix")]
#[test]
fn trust_gid() {
use std::os::unix::prelude::MetadataExt;
let d = Dir::new();
d.dir("a/b");
d.chmod("a", 0o770);
d.chmod("a/b", 0o770);
let m = mistrust_build(&[
MistrustOp::IgnorePrefix(d.canonical_root()),
MistrustOp::TrustNoGroupId(),
]);
let e = m.check_directory(d.path("a/b")).unwrap_err();
assert!(matches!(e, Error::BadPermission(..)));
let gid = d.path("a/b").metadata().unwrap().gid();
let m = mistrust_build(&[
MistrustOp::IgnorePrefix(d.canonical_root()),
MistrustOp::TrustGroup(gid),
]);
m.check_directory(d.path("a/b")).unwrap();
let m = mistrust_build(&[
MistrustOp::IgnorePrefix(d.canonical_root()),
MistrustOp::TrustGroup(gid ^ 1),
]);
let e = m.check_directory(d.path("a/b")).unwrap_err();
assert!(matches!(e, Error::BadPermission(..)));
}
#[test]
fn make_directory() {
let d = Dir::new();
d.dir("a/b");
let m = mistrust_build(&[MistrustOp::IgnorePrefix(d.canonical_root())]);
#[cfg(target_family = "unix")]
{
d.chmod("a", 0o777);
let e = m.make_directory(d.path("a/b/c/d")).unwrap_err();
assert!(matches!(e, Error::BadPermission(..)));
d.chmod("a", 0o0700);
d.chmod("a/b", 0o0700);
}
m.make_directory(d.path("a/b/c/d")).unwrap();
m.check_directory(d.path("a/b/c/d")).unwrap();
m.make_directory(d.path("a/b/c/d")).unwrap();
}
#[cfg(target_family = "unix")]
#[cfg(feature = "walkdir")]
#[test]
fn check_contents() {
let d = Dir::new();
d.dir("a/b/c");
d.file("a/b/c/d");
d.chmod("a", 0o700);
d.chmod("a/b", 0o700);
d.chmod("a/b/c", 0o755);
d.chmod("a/b/c/d", 0o666);
let m = mistrust_build(&[MistrustOp::IgnorePrefix(d.canonical_root())]);
m.check_directory(d.path("a/b")).unwrap();
let e = m
.verifier()
.all_errors()
.check_content()
.check(d.path("a/b"))
.unwrap_err();
assert_eq!(1, e.errors().count());
assert_eq!(e.path().unwrap(), d.path("a/b/c/d"));
}
#[test]
fn trust_everyone() {
let d = Dir::new();
d.dir("a/b/c");
d.file("a/b/c/d");
d.chmod("a", 0o777);
d.chmod("a/b", 0o777);
d.chmod("a/b/c", 0o777);
d.chmod("a/b/c/d", 0o666);
let m = mistrust_build(&[MistrustOp::DangerouslyTrustEveryone()]);
m.check_directory(d.path("a/b/c")).unwrap();
let err = m.check_directory(d.path("a/b/c/d")).unwrap_err();
assert!(matches!(err, Error::BadType(_)));
m.verifier()
.require_file()
.check(d.path("a/b/c/d"))
.unwrap();
}
#[test]
fn default_mistrust() {
let _m = Mistrust::default();
}
}