diff --git a/Cargo.toml b/Cargo.toml index c358721eb5d..24d2da1a436 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -422,6 +422,7 @@ clap_mangen = { workspace = true, optional = true } clap.workspace = true fluent-syntax = { workspace = true, optional = true } itertools.workspace = true +libc.workspace = true phf.workspace = true selinux = { workspace = true, optional = true } textwrap.workspace = true diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index 59634849a6b..aef100bbcd9 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -55,8 +55,12 @@ fn main() { let is_coreutils = binary_as_util.ends_with("utils"); let matched_util = utils .keys() + //*utils is not ls .filter(|&&u| binary_as_util.ends_with(u) && !is_coreutils) - .max_by_key(|u| u.len()); //Prefer stty more than tty. *utils is not ls + //Prefer stty more than tty + .max_by_key(|u| u.len()) + // todo: with coreutils -> ls -> blah symlink chain, blah calls ls + ; let util_name = if let Some(&util) = matched_util { Some(OsString::from(util)) diff --git a/src/common/validation.rs b/src/common/validation.rs index 81ae1351c64..c0cc32fbf94 100644 --- a/src/common/validation.rs +++ b/src/common/validation.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore prefixcat testcat +// spell-checker:ignore prefixcat testcat getauxval EXECFN use std::ffi::{OsStr, OsString}; use std::path::{Path, PathBuf}; @@ -67,15 +67,29 @@ fn get_canonical_util_name(util_name: &str) -> &str { } /// Gets the binary path from command line arguments -/// # Panics /// Panics if the binary path cannot be determined +#[cfg(not(any(target_os = "linux", target_os = "android")))] pub fn binary_path(args: &mut impl Iterator) -> PathBuf { match args.next() { Some(ref s) if !s.is_empty() => PathBuf::from(s), _ => std::env::current_exe().unwrap(), } } - +/// protect against env -a +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn binary_path(args: &mut impl Iterator) -> PathBuf { + use std::ffi::{CStr, OsString}; + use std::os::unix::ffi::OsStringExt; + let p: *const libc::c_char = unsafe { libc::getauxval(libc::AT_EXECFN) as _ }; + if p.is_null() { + use std::io::{Write as _, stderr}; + let _ = writeln!(stderr(), "getauxval failed"); + process::exit(1); + } + let _ = args.next(); + let n = unsafe { CStr::from_ptr(p) }; + OsString::from_vec(n.to_bytes().to_vec()).into() +} /// Extracts the binary name from a path pub fn name(binary_path: &Path) -> Option<&str> { binary_path.file_stem()?.to_str() diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index 074d29c29c6..b000c25ba4f 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -721,133 +721,101 @@ fn test_env_with_empty_executable_double_quotes() { .stderr_is("env: '': No such file or directory\n"); } +// Our coreutils does not dispatch at Linux for security #[test] -#[cfg(all(unix, feature = "dirname", feature = "echo"))] +#[cfg(unix)] fn test_env_overwrite_arg0() { let ts = TestScenario::new(util_name!()); - let bin = ts.bin_path.clone(); - ts.ucmd() - .args(&["--argv0", "echo"]) - .arg(&bin) - .args(&["-n", "hello", "world!"]) + .args(&["--argv0", "hijacked", "sh", "-c", "echo $0"]) .succeeds() - .stdout_is("hello world!") - .stderr_is(""); - - ts.ucmd() - .args(&["-a", "dirname"]) - .arg(bin) - .args(&["aa/bb/cc"]) - .succeeds() - .stdout_is("aa/bb\n") + .stdout_is("hijacked\n") .stderr_is(""); } +// Our coreutils does not dispatch at Linux for security #[test] -#[cfg(all(unix, feature = "echo"))] +#[cfg(unix)] fn test_env_arg_argv0_overwrite() { let ts = TestScenario::new(util_name!()); - let bin = &ts.bin_path; - // overwrite --argv0 by --argv0 ts.ucmd() .args(&["--argv0", "dirname"]) - .args(&["--argv0", "echo"]) - .arg(bin) - .args(&["aa/bb/cc"]) + .args(&["--argv0", "hijacked", "sh", "-c", "echo $0"]) .succeeds() - .stdout_is("aa/bb/cc\n") + .stdout_is("hijacked\n") .stderr_is(""); // overwrite -a by -a ts.ucmd() .args(&["-a", "dirname"]) - .args(&["-a", "echo"]) - .arg(bin) - .args(&["aa/bb/cc"]) + .args(&["-a", "hijacked", "sh", "-c", "echo $0"]) .succeeds() - .stdout_is("aa/bb/cc\n") + .stdout_is("hijacked\n") .stderr_is(""); // overwrite --argv0 by -a ts.ucmd() .args(&["--argv0", "dirname"]) - .args(&["-a", "echo"]) - .arg(bin) - .args(&["aa/bb/cc"]) + .args(&["-a", "hijacked", "sh", "-c", "echo $0"]) .succeeds() - .stdout_is("aa/bb/cc\n") + .stdout_is("hijacked\n") .stderr_is(""); // overwrite -a by --argv0 ts.ucmd() .args(&["-a", "dirname"]) - .args(&["--argv0", "echo"]) - .arg(bin) - .args(&["aa/bb/cc"]) + .args(&["--argv0", "hijacked", "sh", "-c", "echo $0"]) .succeeds() - .stdout_is("aa/bb/cc\n") + .stdout_is("hijacked\n") .stderr_is(""); } #[test] -#[cfg(all(unix, feature = "echo"))] +#[cfg(unix)] fn test_env_arg_argv0_overwrite_mixed_with_string_args() { let ts = TestScenario::new(util_name!()); - let bin = &ts.bin_path; - // string arg following normal ts.ucmd() .args(&["-S--argv0 dirname"]) - .args(&["--argv0", "echo"]) - .arg(bin) - .args(&["aa/bb/cc"]) + .args(&["--argv0", "hijacked", "sh", "-c", "echo $0"]) .succeeds() - .stdout_is("aa/bb/cc\n") + .stdout_is("hijacked\n") .stderr_is(""); // normal following string arg ts.ucmd() .args(&["-a", "dirname"]) - .args(&["-S-a echo"]) - .arg(bin) - .args(&["aa/bb/cc"]) + .args(&["-S-a hijacked sh -c 'echo $0'"]) .succeeds() - .stdout_is("aa/bb/cc\n") + .stdout_is("hijacked\n") .stderr_is(""); // one large string arg ts.ucmd() - .args(&["-S--argv0 dirname -a echo"]) - .arg(bin) - .args(&["aa/bb/cc"]) + .args(&["-S--argv0 dirname -a hijacked sh -c 'echo $0'"]) .succeeds() - .stdout_is("aa/bb/cc\n") + .stdout_is("hijacked\n") .stderr_is(""); // two string args ts.ucmd() .args(&["-S-a dirname"]) - .args(&["-S--argv0 echo"]) - .arg(bin) - .args(&["aa/bb/cc"]) + .args(&["-S--argv0 hijacked sh -c 'echo $0'"]) .succeeds() - .stdout_is("aa/bb/cc\n") + .stdout_is("hijacked\n") .stderr_is(""); // three args: normal, string, normal ts.ucmd() .args(&["-a", "sleep"]) .args(&["-S-a dirname"]) - .args(&["-a", "echo"]) - .arg(bin) - .args(&["aa/bb/cc"]) + .args(&["-a", "hijacked", "sh", "-c", "echo $0"]) .succeeds() - .stdout_is("aa/bb/cc\n") + .stdout_is("hijacked\n") .stderr_is(""); } diff --git a/tests/test_util_name.rs b/tests/test_util_name.rs index 17b9dc36e5e..53c3ef55334 100644 --- a/tests/test_util_name.rs +++ b/tests/test_util_name.rs @@ -26,6 +26,20 @@ fn init() { eprintln!("Setting UUTESTS_BINARY_PATH={TESTS_BINARY}"); } +#[test] +#[cfg(all(feature = "env", any(target_os = "linux", target_os = "android")))] +fn binary_name_protection() { + let ts = TestScenario::new("env"); + let bin = ts.bin_path.clone(); + ts.ucmd() + .arg("-a") + .arg("hijacked") + .arg(&bin) + .arg("--version") + .succeeds() + .stdout_contains("coreutils"); +} + #[test] #[cfg(feature = "ls")] fn execution_phrase_double() {