Skip to content

Commit 3f57dd7

Browse files
committed
coreutils: Protect against env -a for security
1 parent 194d980 commit 3f57dd7

File tree

5 files changed

+62
-61
lines changed

5 files changed

+62
-61
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,7 @@ clap_mangen = { workspace = true, optional = true }
422422
clap.workspace = true
423423
fluent-syntax = { workspace = true, optional = true }
424424
itertools.workspace = true
425+
libc.workspace = true
425426
phf.workspace = true
426427
selinux = { workspace = true, optional = true }
427428
textwrap.workspace = true

src/bin/coreutils.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,12 @@ fn main() {
5555
let is_coreutils = binary_as_util.ends_with("utils");
5656
let matched_util = utils
5757
.keys()
58+
//*utils is not ls
5859
.filter(|&&u| binary_as_util.ends_with(u) && !is_coreutils)
59-
.max_by_key(|u| u.len()); //Prefer stty more than tty. *utils is not ls
60+
//Prefer stty more than tty
61+
.max_by_key(|u| u.len())
62+
// todo: with coreutils -> ls -> blah symlink chain, blah calls ls
63+
;
6064

6165
let util_name = if let Some(&util) = matched_util {
6266
Some(OsString::from(util))

src/common/validation.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
55

6-
// spell-checker:ignore prefixcat testcat
6+
// spell-checker:ignore prefixcat testcat getauxval EXECFN
77

88
use std::ffi::{OsStr, OsString};
99
use std::path::{Path, PathBuf};
@@ -67,15 +67,29 @@ fn get_canonical_util_name(util_name: &str) -> &str {
6767
}
6868

6969
/// Gets the binary path from command line arguments
70-
/// # Panics
7170
/// Panics if the binary path cannot be determined
71+
#[cfg(not(any(target_os = "linux", target_os = "android")))]
7272
pub fn binary_path(args: &mut impl Iterator<Item = OsString>) -> PathBuf {
7373
match args.next() {
7474
Some(ref s) if !s.is_empty() => PathBuf::from(s),
7575
_ => std::env::current_exe().unwrap(),
7676
}
7777
}
78-
78+
/// protect against env -a
79+
#[cfg(any(target_os = "linux", target_os = "android"))]
80+
pub fn binary_path(args: &mut impl Iterator<Item = OsString>) -> PathBuf {
81+
use std::ffi::{CStr, OsString};
82+
use std::os::unix::ffi::OsStringExt;
83+
let p: *const libc::c_char = unsafe { libc::getauxval(libc::AT_EXECFN) as _ };
84+
if p.is_null() {
85+
use std::io::{Write as _, stderr};
86+
let _ = writeln!(stderr(), "getauxval failed");
87+
process::exit(1);
88+
}
89+
let _ = args.next();
90+
let n = unsafe { CStr::from_ptr(p) };
91+
OsString::from_vec(n.to_bytes().to_vec()).into()
92+
}
7993
/// Extracts the binary name from a path
8094
pub fn name(binary_path: &Path) -> Option<&str> {
8195
binary_path.file_stem()?.to_str()

tests/by-util/test_env.rs

Lines changed: 25 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -721,133 +721,101 @@ fn test_env_with_empty_executable_double_quotes() {
721721
.stderr_is("env: '': No such file or directory\n");
722722
}
723723

724+
// Our coreutils does not dispatch at Linux for security
724725
#[test]
725-
#[cfg(all(unix, feature = "dirname", feature = "echo"))]
726+
#[cfg(unix)]
726727
fn test_env_overwrite_arg0() {
727728
let ts = TestScenario::new(util_name!());
728729

729-
let bin = ts.bin_path.clone();
730-
731730
ts.ucmd()
732-
.args(&["--argv0", "echo"])
733-
.arg(&bin)
734-
.args(&["-n", "hello", "world!"])
731+
.args(&["--argv0", "hijacked", "sh", "-c", "echo $0"])
735732
.succeeds()
736-
.stdout_is("hello world!")
737-
.stderr_is("");
738-
739-
ts.ucmd()
740-
.args(&["-a", "dirname"])
741-
.arg(bin)
742-
.args(&["aa/bb/cc"])
743-
.succeeds()
744-
.stdout_is("aa/bb\n")
733+
.stdout_is("hijacked\n")
745734
.stderr_is("");
746735
}
747736

737+
// Our coreutils does not dispatch at Linux for security
748738
#[test]
749-
#[cfg(all(unix, feature = "echo"))]
739+
#[cfg(unix)]
750740
fn test_env_arg_argv0_overwrite() {
751741
let ts = TestScenario::new(util_name!());
752742

753-
let bin = &ts.bin_path;
754-
755743
// overwrite --argv0 by --argv0
756744
ts.ucmd()
757745
.args(&["--argv0", "dirname"])
758-
.args(&["--argv0", "echo"])
759-
.arg(bin)
760-
.args(&["aa/bb/cc"])
746+
.args(&["--argv0", "hijacked", "sh", "-c", "echo $0"])
761747
.succeeds()
762-
.stdout_is("aa/bb/cc\n")
748+
.stdout_is("hijacked\n")
763749
.stderr_is("");
764750

765751
// overwrite -a by -a
766752
ts.ucmd()
767753
.args(&["-a", "dirname"])
768-
.args(&["-a", "echo"])
769-
.arg(bin)
770-
.args(&["aa/bb/cc"])
754+
.args(&["-a", "hijacked", "sh", "-c", "echo $0"])
771755
.succeeds()
772-
.stdout_is("aa/bb/cc\n")
756+
.stdout_is("hijacked\n")
773757
.stderr_is("");
774758

775759
// overwrite --argv0 by -a
776760
ts.ucmd()
777761
.args(&["--argv0", "dirname"])
778-
.args(&["-a", "echo"])
779-
.arg(bin)
780-
.args(&["aa/bb/cc"])
762+
.args(&["-a", "hijacked", "sh", "-c", "echo $0"])
781763
.succeeds()
782-
.stdout_is("aa/bb/cc\n")
764+
.stdout_is("hijacked\n")
783765
.stderr_is("");
784766

785767
// overwrite -a by --argv0
786768
ts.ucmd()
787769
.args(&["-a", "dirname"])
788-
.args(&["--argv0", "echo"])
789-
.arg(bin)
790-
.args(&["aa/bb/cc"])
770+
.args(&["--argv0", "hijacked", "sh", "-c", "echo $0"])
791771
.succeeds()
792-
.stdout_is("aa/bb/cc\n")
772+
.stdout_is("hijacked\n")
793773
.stderr_is("");
794774
}
795775

796776
#[test]
797-
#[cfg(all(unix, feature = "echo"))]
777+
#[cfg(unix)]
798778
fn test_env_arg_argv0_overwrite_mixed_with_string_args() {
799779
let ts = TestScenario::new(util_name!());
800780

801-
let bin = &ts.bin_path;
802-
803781
// string arg following normal
804782
ts.ucmd()
805783
.args(&["-S--argv0 dirname"])
806-
.args(&["--argv0", "echo"])
807-
.arg(bin)
808-
.args(&["aa/bb/cc"])
784+
.args(&["--argv0", "hijacked", "sh", "-c", "echo $0"])
809785
.succeeds()
810-
.stdout_is("aa/bb/cc\n")
786+
.stdout_is("hijacked\n")
811787
.stderr_is("");
812788

813789
// normal following string arg
814790
ts.ucmd()
815791
.args(&["-a", "dirname"])
816-
.args(&["-S-a echo"])
817-
.arg(bin)
818-
.args(&["aa/bb/cc"])
792+
.args(&["-S-a hijacked sh -c 'echo $0'"])
819793
.succeeds()
820-
.stdout_is("aa/bb/cc\n")
794+
.stdout_is("hijacked\n")
821795
.stderr_is("");
822796

823797
// one large string arg
824798
ts.ucmd()
825-
.args(&["-S--argv0 dirname -a echo"])
826-
.arg(bin)
827-
.args(&["aa/bb/cc"])
799+
.args(&["-S--argv0 dirname -a hijacked sh -c 'echo $0'"])
828800
.succeeds()
829-
.stdout_is("aa/bb/cc\n")
801+
.stdout_is("hijacked\n")
830802
.stderr_is("");
831803

832804
// two string args
833805
ts.ucmd()
834806
.args(&["-S-a dirname"])
835-
.args(&["-S--argv0 echo"])
836-
.arg(bin)
837-
.args(&["aa/bb/cc"])
807+
.args(&["-S--argv0 hijacked sh -c 'echo $0'"])
838808
.succeeds()
839-
.stdout_is("aa/bb/cc\n")
809+
.stdout_is("hijacked\n")
840810
.stderr_is("");
841811

842812
// three args: normal, string, normal
843813
ts.ucmd()
844814
.args(&["-a", "sleep"])
845815
.args(&["-S-a dirname"])
846-
.args(&["-a", "echo"])
847-
.arg(bin)
848-
.args(&["aa/bb/cc"])
816+
.args(&["-a", "hijacked", "sh", "-c", "echo $0"])
849817
.succeeds()
850-
.stdout_is("aa/bb/cc\n")
818+
.stdout_is("hijacked\n")
851819
.stderr_is("");
852820
}
853821

tests/test_util_name.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,20 @@ fn init() {
2626
eprintln!("Setting UUTESTS_BINARY_PATH={TESTS_BINARY}");
2727
}
2828

29+
#[test]
30+
#[cfg(all(feature = "env", any(target_os = "linux", target_os = "android")))]
31+
fn binary_name_protection() {
32+
let ts = TestScenario::new("env");
33+
let bin = ts.bin_path.clone();
34+
ts.ucmd()
35+
.arg("-a")
36+
.arg("hijacked")
37+
.arg(&bin)
38+
.arg("--version")
39+
.succeeds()
40+
.stdout_contains("coreutils");
41+
}
42+
2943
#[test]
3044
#[cfg(feature = "ls")]
3145
fn execution_phrase_double() {

0 commit comments

Comments
 (0)