Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 = []
Expand Down
23 changes: 23 additions & 0 deletions src/cutils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> {
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 {
Expand Down
2 changes: 1 addition & 1 deletion src/sudoers/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}")?;
Expand Down
9 changes: 9 additions & 0 deletions src/sudoers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,7 @@ fn match_token<T: basic_parser::Token + std::ops::Deref<Target = String>>(
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,
Expand All @@ -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<Predicate, T>(table: &VecOrd<Def<T>>, pred: &Predicate) -> FoundAliases
Expand Down
32 changes: 27 additions & 5 deletions src/sudoers/tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,18 @@ pub type Command = (SimpleCommand, Option<Box<[String]>>);

/// 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(<SimpleCommand as std::ops::Deref>::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;
Expand Down Expand Up @@ -224,13 +235,24 @@ impl Token for SimpleCommand {
const MAX_LEN: usize = 1024;

fn construct(mut cmd: String) -> Result<Self, String> {
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('^') {
Expand Down Expand Up @@ -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"
Expand Down
Loading