fix: Add trusted_shell_script_arg for internal scripts

- shell_script_arg blocks $( and backticks for user input safety
- trusted_shell_script_arg allows these for internal installer scripts
- Internal scripts need shell features like command substitution
- Updated bootstrap, installer, facade, and llm modules
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-01-09 12:13:35 -03:00
parent db267714ca
commit 00acf1c76e
5 changed files with 41 additions and 9 deletions

View file

@ -38,7 +38,7 @@ fn safe_pgrep(args: &[&str]) -> Option<std::process::Output> {
fn safe_sh_command(script: &str) -> Option<std::process::Output> {
SafeCommand::new("sh")
.and_then(|c| c.arg("-c"))
.and_then(|c| c.shell_script_arg(script))
.and_then(|c| c.trusted_shell_script_arg(script))
.ok()
.and_then(|cmd| cmd.execute().ok())
}

View file

@ -1065,7 +1065,7 @@ Store credentials in Vault:
trace!("Executing command: {}", rendered_cmd);
let output = SafeCommand::new("bash")
.and_then(|c| c.arg("-c"))
.and_then(|c| c.shell_script_arg(&rendered_cmd))
.and_then(|c| c.trusted_shell_script_arg(&rendered_cmd))
.and_then(|c| c.working_dir(&bin_path))
.map_err(|e| anyhow::anyhow!("Failed to build bash command: {}", e))?
.execute()

View file

@ -17,7 +17,7 @@ fn safe_nvcc_version() -> Option<std::process::Output> {
fn safe_sh_command(script: &str) -> Option<std::process::Output> {
SafeCommand::new("sh")
.and_then(|c| c.arg("-c"))
.and_then(|c| c.shell_script_arg(script))
.and_then(|c| c.trusted_shell_script_arg(script))
.ok()
.and_then(|cmd| cmd.execute().ok())
}
@ -1112,7 +1112,7 @@ EOF"#.to_string(),
trace!("[START] Working dir: {}", bin_path.display());
let child = SafeCommand::new("sh")
.and_then(|c| c.arg("-c"))
.and_then(|c| c.shell_script_arg(&rendered_cmd))
.and_then(|c| c.trusted_shell_script_arg(&rendered_cmd))
.and_then(|c| c.working_dir(&bin_path))
.and_then(|cmd| cmd.spawn_with_envs(&evaluated_envs))
.map_err(|e| anyhow::anyhow!("Failed to spawn process: {}", e));

View file

@ -90,7 +90,7 @@ pub async fn ensure_llama_servers_running(
let pkill_result = SafeCommand::new("sh")
.and_then(|c| c.arg("-c"))
.and_then(|c| c.shell_script_arg("pkill llama-server -9; true"));
.and_then(|c| c.trusted_shell_script_arg("pkill llama-server -9; true"));
match pkill_result {
Ok(cmd) => {
@ -366,7 +366,7 @@ pub fn start_llm_server(
);
let cmd = SafeCommand::new("cmd")
.and_then(|c| c.arg("/C"))
.and_then(|c| c.shell_script_arg(&cmd_arg))
.and_then(|c| c.trusted_shell_script_arg(&cmd_arg))
.map_err(|e| Box::new(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())) as Box<dyn std::error::Error + Send + Sync>)?;
cmd.execute().map_err(|e| Box::new(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())) as Box<dyn std::error::Error + Send + Sync>)?;
} else {
@ -378,7 +378,7 @@ pub fn start_llm_server(
);
let cmd = SafeCommand::new("sh")
.and_then(|c| c.arg("-c"))
.and_then(|c| c.shell_script_arg(&cmd_arg))
.and_then(|c| c.trusted_shell_script_arg(&cmd_arg))
.map_err(|e| Box::new(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())) as Box<dyn std::error::Error + Send + Sync>)?;
cmd.execute().map_err(|e| Box::new(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())) as Box<dyn std::error::Error + Send + Sync>)?;
}
@ -410,7 +410,7 @@ pub async fn start_embedding_server(
);
let cmd = SafeCommand::new("cmd")
.and_then(|c| c.arg("/c"))
.and_then(|c| c.shell_script_arg(&cmd_arg))
.and_then(|c| c.trusted_shell_script_arg(&cmd_arg))
.map_err(|e| Box::new(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())) as Box<dyn std::error::Error + Send + Sync>)?;
cmd.execute().map_err(|e| Box::new(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())) as Box<dyn std::error::Error + Send + Sync>)?;
} else {
@ -422,7 +422,7 @@ pub async fn start_embedding_server(
);
let cmd = SafeCommand::new("sh")
.and_then(|c| c.arg("-c"))
.and_then(|c| c.shell_script_arg(&cmd_arg))
.and_then(|c| c.trusted_shell_script_arg(&cmd_arg))
.map_err(|e| Box::new(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())) as Box<dyn std::error::Error + Send + Sync>)?;
cmd.execute().map_err(|e| Box::new(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())) as Box<dyn std::error::Error + Send + Sync>)?;
}

View file

@ -176,6 +176,38 @@ impl SafeCommand {
Ok(self)
}
pub fn trusted_shell_script_arg(mut self, script: &str) -> Result<Self, CommandGuardError> {
let is_unix_shell = self.command == "bash" || self.command == "sh";
let is_windows_cmd = self.command == "cmd";
if !is_unix_shell && !is_windows_cmd {
return Err(CommandGuardError::InvalidArgument(
"trusted_shell_script_arg only allowed for bash/sh/cmd commands".to_string(),
));
}
let valid_flag = if is_unix_shell {
self.args.last().is_some_and(|a| a == "-c")
} else {
self.args.last().is_some_and(|a| a == "/C" || a == "/c")
};
if !valid_flag {
return Err(CommandGuardError::InvalidArgument(
"trusted_shell_script_arg requires -c (unix) or /C (windows) flag to be set first".to_string(),
));
}
if script.is_empty() {
return Err(CommandGuardError::InvalidArgument(
"Empty script".to_string(),
));
}
if script.len() > 16384 {
return Err(CommandGuardError::InvalidArgument(
"Script too long".to_string(),
));
}
self.args.push(script.to_string());
Ok(self)
}
pub fn args(mut self, args: &[&str]) -> Result<Self, CommandGuardError> {
for arg in args {
validate_argument(arg)?;