//! Hook content sanitization. //! //! Validates custom hook scripts to prevent dangerous commands. use crate::error::{GitError, GitResult}; /// Commands/patterns that are never allowed in custom hook scripts. const FORBIDDEN_PATTERNS: &[&str] = &[ "rm -rf", "rm -r /", "chmod 777", "chmod 666", "mkfs", "dd if=", ":(){ :|:& };:", // fork bomb "> /dev/sda", "curl -o /", "wget -O /", "/etc/passwd", "/etc/shadow", "shutdown", "reboot", "init 0", "init 6", "poweroff", "halt", ]; /// Maximum hook script size (64KB). const MAX_HOOK_SIZE: usize = 65536; /// Validate a custom hook script content for safety. pub fn validate_hook_content(content: &str) -> GitResult<()> { if content.is_empty() { return Err(GitError::InvalidArgument( "hook content cannot be empty".into(), )); } if content.len() > MAX_HOOK_SIZE { return Err(GitError::InvalidArgument(format!( "hook content too large (max {} bytes): {} bytes", MAX_HOOK_SIZE, content.len() ))); } let content_lower = content.to_lowercase(); for pattern in FORBIDDEN_PATTERNS { if content_lower.contains(pattern) { return Err(GitError::InvalidArgument(format!( "hook content contains forbidden pattern: '{pattern}'" ))); } } if content.contains('\0') { return Err(GitError::InvalidArgument( "hook content cannot contain null bytes".into(), )); } Ok(()) } /// Validate a hook name (must be a recognized git hook name). pub fn validate_hook_name(name: &str) -> GitResult<()> { const VALID_HOOK_NAMES: &[&str] = &[ "pre-receive", "update", "post-receive", "pre-applypatch", "applypatch-msg", "post-applypatch", "pre-commit", "prepare-commit-msg", "commit-msg", "post-commit", "pre-auto-gc", ]; if !VALID_HOOK_NAMES.contains(&name) { return Err(GitError::InvalidArgument(format!( "invalid hook name: '{name}'. Must be one of: {}", VALID_HOOK_NAMES.join(", ") ))); } Ok(()) }