mirror of
https://github.com/uutils/coreutils.git
synced 2026-06-10 15:48:22 -07:00
coreutils: Protect against env -a for security (#10773)
This prevents an attacker from spoofing argv[0] to bypass apparmor restrictions. - `env -a false ls` now correctly runs `ls` instead of dispatching as `false` - Also works under masked `/proc` (does not rely on /proc/self/exe). Closes #10135
This commit is contained in:
@@ -366,6 +366,7 @@ uutests
|
||||
uutils
|
||||
|
||||
# * function names
|
||||
execfn
|
||||
getcwd
|
||||
setpipe
|
||||
|
||||
|
||||
Generated
+1
@@ -576,6 +576,7 @@ dependencies = [
|
||||
"rstest",
|
||||
"rstest_reuse",
|
||||
"rustc-hash",
|
||||
"rustix",
|
||||
"selinux",
|
||||
"sha1",
|
||||
"tempfile",
|
||||
|
||||
+7
-2
@@ -1,7 +1,7 @@
|
||||
# coreutils (uutils)
|
||||
# * see the repository LICENSE, README, and CONTRIBUTING files for more information
|
||||
|
||||
# spell-checker:ignore (libs) bigdecimal datetime foldhash serde gethostid kqueue libselinux mangen memmap uuhelp startswith constness expl unnested logind cfgs interner
|
||||
# spell-checker:ignore (libs) bigdecimal datetime foldhash serde gethostid kqueue libselinux mangen memmap uuhelp startswith constness expl unnested logind cfgs interner getauxval
|
||||
|
||||
[package]
|
||||
name = "coreutils"
|
||||
@@ -454,7 +454,9 @@ rstest = "0.26.0"
|
||||
rstest_reuse = "0.7.0"
|
||||
rustc-hash = "2.1.1"
|
||||
rust-ini = "0.21.0"
|
||||
rustix = "1.1.4"
|
||||
# binary name of coreutils can be hijacked by overriding getauxval via LD_PRELOAD
|
||||
# So we use param and avoid libc backend
|
||||
rustix = { version = "1.1.4", features = ["param"] }
|
||||
same-file = "1.0.6"
|
||||
self_cell = "1.0.4"
|
||||
selinux = "=0.6.0"
|
||||
@@ -624,6 +626,9 @@ who = { optional = true, version = "0.8.0", package = "uu_who", path = "src/uu/w
|
||||
whoami = { optional = true, version = "0.8.0", package = "uu_whoami", path = "src/uu/whoami" }
|
||||
yes = { optional = true, version = "0.8.0", package = "uu_yes", path = "src/uu/yes" }
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
|
||||
rustix.workspace = true
|
||||
|
||||
# this breaks clippy linting with: "tests/by-util/test_factor_benches.rs: No such file or directory (os error 2)"
|
||||
# factor_benches = { optional = true, version = "0.0.0", package = "uu_factor_benches", path = "tests/benches/factor" }
|
||||
|
||||
|
||||
@@ -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 memfd_create prefixcat rsplit testcat
|
||||
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::io::{Write, stderr};
|
||||
@@ -73,15 +73,41 @@ 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),
|
||||
// the fallback is valid only for hardlinks
|
||||
_ => std::env::current_exe().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get actual binary path from kernel, not argv0, to prevent `env -a` from bypassing
|
||||
/// AppArmor, SELinux policies on hard-linked binaries
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn binary_path(args: &mut impl Iterator<Item = OsString>) -> PathBuf {
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
let execfn = rustix::param::linux_execfn();
|
||||
let execfn_bytes = execfn.to_bytes();
|
||||
let exec_path = Path::new(OsStr::from_bytes(execfn_bytes));
|
||||
let argv0 = args.next().unwrap();
|
||||
let mut shebang_buf = [0u8; 2];
|
||||
// exec_path is wrong when called from shebang or memfd_create (/proc/self/fd/*)
|
||||
// argv0 is not full-path when called from PATH
|
||||
if execfn_bytes.rsplit(|&b| b == b'/').next() == argv0.as_bytes().rsplit(|&b| b == b'/').next()
|
||||
|| execfn_bytes.starts_with(b"/proc/")
|
||||
|| (File::open(Path::new(exec_path))
|
||||
.and_then(|mut f| f.read_exact(&mut shebang_buf))
|
||||
.is_ok()
|
||||
&& &shebang_buf == b"#!")
|
||||
{
|
||||
argv0.into()
|
||||
} else {
|
||||
exec_path.into()
|
||||
}
|
||||
}
|
||||
/// Extracts the binary name from a path
|
||||
pub fn name(binary_path: &Path) -> Option<&str> {
|
||||
binary_path.file_stem()?.to_str()
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user