security: fix argument injection in shell command validation (#465)

This commit is contained in:
Rin 2026-02-17 20:11:20 +07:00 committed by GitHub
parent 529a3d0242
commit 9ec1106f53
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -343,6 +343,7 @@ impl SecurityPolicy {
/// validates each sub-command against the allowlist
/// - Blocks single `&` background chaining (`&&` remains supported)
/// - Blocks output redirections (`>`, `>>`) that could write outside workspace
/// - Blocks dangerous arguments (e.g. `find -exec`, `git config`)
pub fn is_command_allowed(&self, command: &str) -> bool {
if self.autonomy == AutonomyLevel::ReadOnly {
return false;
@ -398,13 +399,9 @@ impl SecurityPolicy {
// Strip leading env var assignments (e.g. FOO=bar cmd)
let cmd_part = skip_env_assignments(segment);
let base_cmd = cmd_part
.split_whitespace()
.next()
.unwrap_or("")
.rsplit('/')
.next()
.unwrap_or("");
let mut words = cmd_part.split_whitespace();
let base_raw = words.next().unwrap_or("");
let base_cmd = base_raw.rsplit('/').next().unwrap_or("");
if base_cmd.is_empty() {
continue;
@ -417,6 +414,12 @@ impl SecurityPolicy {
{
return false;
}
// Validate arguments for the command
let args: Vec<String> = words.map(|w| w.to_ascii_lowercase()).collect();
if !self.is_args_safe(base_cmd, &args) {
return false;
}
}
// At least one command must be present
@ -428,6 +431,29 @@ impl SecurityPolicy {
has_cmd
}
/// Check for dangerous arguments that allow sub-command execution.
fn is_args_safe(&self, base: &str, args: &[String]) -> bool {
let base = base.to_ascii_lowercase();
match base.as_str() {
"find" => {
// find -exec and find -ok allow arbitrary command execution
!args.iter().any(|arg| arg == "-exec" || arg == "-ok")
}
"git" => {
// git config, alias, and -c can be used to set dangerous options
// (e.g. git config core.editor "rm -rf /")
!args.iter().any(|arg| {
arg == "config"
|| arg.starts_with("config.")
|| arg == "alias"
|| arg.starts_with("alias.")
|| arg == "-c"
})
}
_ => true,
}
}
/// Check if a file path is allowed (no path traversal, within workspace)
pub fn is_path_allowed(&self, path: &str) -> bool {
// Block null bytes (can truncate paths in C-backed syscalls)
@ -996,6 +1022,22 @@ mod tests {
assert!(!p.is_command_allowed("ls >> /tmp/exfil.txt"));
}
#[test]
fn command_argument_injection_blocked() {
let p = default_policy();
// find -exec is a common bypass
assert!(!p.is_command_allowed("find . -exec rm -rf {} +"));
assert!(!p.is_command_allowed("find / -ok cat {} \\;"));
// git config/alias can execute commands
assert!(!p.is_command_allowed("git config core.editor \"rm -rf /\""));
assert!(!p.is_command_allowed("git alias.st status"));
assert!(!p.is_command_allowed("git -c core.editor=calc.exe commit"));
// Legitimate commands should still work
assert!(p.is_command_allowed("find . -name '*.txt'"));
assert!(p.is_command_allowed("git status"));
assert!(p.is_command_allowed("git add ."));
}
#[test]
fn command_injection_dollar_brace_blocked() {
let p = default_policy();