From a2986db3d651d26b80ae47dbdb72311d560be72a Mon Sep 17 00:00:00 2001 From: fettpl <38704082+fettpl@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:54:26 +0100 Subject: [PATCH] fix(security): enhance shell redirection blocking in security policy (#521) * fix(security): enhance shell redirection blocking in security policy Block process substitution (<(...) and >(...)) and tee command in is_command_allowed() to close shell escape vectors that bypass existing redirect and subshell checks. Closes #514 Co-Authored-By: Claude Opus 4.6 * style: apply rustfmt to providers/mod.rs Fix pre-existing formatting issue from main. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- src/security/policy.rs | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) 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();