diff --git a/Cargo.toml b/Cargo.toml index 6f0ae9947..0736c5a41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,10 +16,10 @@ default-run = "sudo" [dependencies] libc = "0.2.152" -glob = "0.3.0" +glob = { version = "0.3.0", optional = true } [features] -default = [] +default = ["rust-glob"] # when enabled, use "sudo-i" PAM service name for sudo -i instead of "sudo" # ONLY ENABLE THIS FEATURE if you know that original sudo uses "sudo-i" @@ -29,6 +29,9 @@ pam-login = [] # this enables enforcing of AppArmor profiles apparmor = [] +# this enables the use or the Rust glob crate as instead of fnmatch(3) +rust-glob = ["dep:glob"] + # enable detailed logging (use for development only) to /tmp # this will compromise the security of sudo-rs somewhat dev = [] diff --git a/src/cutils/mod.rs b/src/cutils/mod.rs index 5fdc94a83..e8fc1934c 100644 --- a/src/cutils/mod.rs +++ b/src/cutils/mod.rs @@ -104,6 +104,29 @@ pub fn is_fifo(fildes: BorrowedFd) -> bool { fstat_mode_set(&fildes, libc::S_IFIFO) } +/// Wrapper around fnmatch for globbing +#[cfg(not(feature = "rust-glob"))] +pub fn fnmatch( + pattern: &crate::common::SudoString, + name: &std::path::Path, +) -> std::io::Result { + let pattern = pattern.as_cstr(); + let name = std::ffi::CString::new(name.as_os_str().as_bytes()).expect("path is not a C string"); + + // equivalent to "require_literal_separator" + let flags = libc::FNM_PATHNAME; + + // SAFETY: fnmatch is passed two valid pointers to a CString + match unsafe { libc::fnmatch(pattern.as_ptr(), name.as_ptr(), flags) } { + 0 => Ok(true), + libc::FNM_NOMATCH => Ok(false), + _ => Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "pattern error", + )), + } +} + #[allow(clippy::undocumented_unsafe_blocks)] #[cfg(test)] mod test { diff --git a/src/sudoers/entry.rs b/src/sudoers/entry.rs index dfadc2674..4c08f3d77 100644 --- a/src/sudoers/entry.rs +++ b/src/sudoers/entry.rs @@ -253,7 +253,7 @@ fn write_spec<'a>( Meta::All => f.write_str("ALL")?, Meta::Only((cmd, args)) => { - write!(f, "{cmd}")?; + write!(f, "{}", cmd.as_str())?; if let Some(args) = args { for arg in args.iter() { write!(f, " {arg}")?; diff --git a/src/sudoers/mod.rs b/src/sudoers/mod.rs index 3e07e3e52..1a61cb3e1 100644 --- a/src/sudoers/mod.rs +++ b/src/sudoers/mod.rs @@ -628,6 +628,7 @@ fn match_token>( move |token| token.as_str() == text } +#[cfg(feature = "rust-glob")] fn match_command<'a>((cmd, args): (&'a Path, &'a [String])) -> impl Fn(&Command) -> bool + 'a { let opts = glob::MatchOptions { require_literal_separator: true, @@ -639,6 +640,14 @@ fn match_command<'a>((cmd, args): (&'a Path, &'a [String])) -> impl Fn(&Command) } } +#[cfg(not(feature = "rust-glob"))] +fn match_command<'a>((cmd, args): (&'a Path, &'a [String])) -> impl Fn(&Command) -> bool + 'a { + move |(cmdpat, argpat)| { + crate::cutils::fnmatch(cmdpat, cmd).unwrap_or(false) + && argpat.as_ref().map_or(true, |vec| args == vec.as_ref()) + } +} + /// Find all the aliases that a object is a member of; this requires [sanitize_alias_table] to have run first; /// I.e. this function should not be "pub". fn get_aliases(table: &VecOrd>, pred: &Predicate) -> FoundAliases diff --git a/src/sudoers/tokens.rs b/src/sudoers/tokens.rs index 42ef6fdc6..0ec4aa980 100644 --- a/src/sudoers/tokens.rs +++ b/src/sudoers/tokens.rs @@ -168,7 +168,18 @@ pub type Command = (SimpleCommand, Option>); /// A type that is specific to 'only commands', that can only happen in "Defaults!command" contexts; /// which is essentially a subset of "Command" -pub type SimpleCommand = glob::Pattern; +pub struct SimpleCommand(::Target); + +impl std::ops::Deref for SimpleCommand { + #[cfg(feature = "rust-glob")] + type Target = glob::Pattern; + #[cfg(not(feature = "rust-glob"))] + type Target = SudoString; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} impl Token for Command { const MAX_LEN: usize = 1024; @@ -224,13 +235,24 @@ impl Token for SimpleCommand { const MAX_LEN: usize = 1024; fn construct(mut cmd: String) -> Result { - let cvt_err = |pat: Result<_, glob::PatternError>| { - pat.map_err(|err| format!("wildcard pattern error {err}")) + #[cfg(feature = "rust-glob")] + let make_pattern = |pat: String| { + glob::Pattern::new(&pat) + .map(SimpleCommand) + .map_err(|_| "wildcard pattern error".to_string()) + }; + + #[cfg(not(feature = "rust-glob"))] + let make_pattern = |pat| match SudoString::new(pat) { + Ok(pattern) if crate::cutils::fnmatch(&pattern, &std::path::PathBuf::new()).is_ok() => { + Ok(SimpleCommand(pattern)) + } + _ => Err("wildcard pattern error".to_string()), }; // detect the two edges cases if cmd == "list" || cmd == "sudoedit" { - return cvt_err(glob::Pattern::new(&cmd)); + return make_pattern(cmd); } else if cmd.starts_with("sha") { return Err("digest specifications are not supported".to_string()); } else if cmd.starts_with('^') { @@ -258,7 +280,7 @@ impl Token for SimpleCommand { cmd.push_str("/*"); } - cvt_err(glob::Pattern::new(&cmd)) + make_pattern(cmd) } // all commands start with "/" except "sudoedit" or "list"