diff --git a/src/security/policy.rs b/src/security/policy.rs index 9383f3a..57d50ae 100644 --- a/src/security/policy.rs +++ b/src/security/policy.rs @@ -350,7 +350,12 @@ impl SecurityPolicy { // Block subshell/expansion operators — these allow hiding arbitrary // commands inside an allowed command (e.g. `echo $(rm -rf /)`) - if command.contains('`') || command.contains("$(") || command.contains("${") { + if command.contains('`') + || command.contains("$(") + || command.contains("${") + || command.contains("<(") + || command.contains(">(") + { return false; } @@ -359,6 +364,15 @@ impl SecurityPolicy { return false; } + // Block `tee` — it can write to arbitrary files, bypassing the + // redirect check above (e.g. `echo secret | tee /etc/crontab`) + if command + .split_whitespace() + .any(|w| w == "tee" || w.ends_with("/tee")) + { + return false; + } + // Block background command chaining (`&`), which can hide extra // sub-commands and outlive timeout expectations. Keep `&&` allowed. if contains_single_ampersand(command) { @@ -988,6 +1002,21 @@ mod tests { assert!(!p.is_command_allowed("echo ${IFS}cat${IFS}/etc/passwd")); } + #[test] + fn command_injection_tee_blocked() { + let p = default_policy(); + assert!(!p.is_command_allowed("echo secret | tee /etc/crontab")); + assert!(!p.is_command_allowed("ls | /usr/bin/tee outfile")); + assert!(!p.is_command_allowed("tee file.txt")); + } + + #[test] + fn command_injection_process_substitution_blocked() { + let p = default_policy(); + assert!(!p.is_command_allowed("cat <(echo pwned)")); + assert!(!p.is_command_allowed("ls >(cat /etc/passwd)")); + } + #[test] fn command_env_var_prefix_with_allowed_cmd() { let p = default_policy();