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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion src/bin/coreutils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
20 changes: 17 additions & 3 deletions src/common/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<Item = OsString>) -> 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<Item = OsString>) -> 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()
Expand Down
82 changes: 25 additions & 57 deletions tests/by-util/test_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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("");
}

Expand Down
14 changes: 14 additions & 0 deletions tests/test_util_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading