a40da90ef9
- Reformatted build script with proper indentation and line breaks - Added tonic-health dependency to Cargo.toml and updated lock file - Improved error handling in disk cache with concurrent deletion checks - Refactored conditional chains using && and let expressions - Reformatted struct initialization and function parameter lists - Added proper spacing and alignment in language stats processing - Improved assertion formatting in test cases - Reorganized import statements and code layout in multiple files - Updated metrics functions with better parameter handling and formatting
173 lines
5.4 KiB
Rust
173 lines
5.4 KiB
Rust
//! 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.
|
|
/// This is a blocklist approach - we also add pattern-based detection.
|
|
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",
|
|
// Additional patterns to catch encoding/obfuscation attempts
|
|
"eval ", // eval can execute arbitrary strings
|
|
"exec ", // exec can replace process
|
|
"$(", // command substitution
|
|
"`", // backtick command substitution
|
|
"${", // variable expansion (can be used for obfuscation)
|
|
"|bash", // piping to bash
|
|
"|sh", // piping to sh
|
|
"|dash", // piping to dash
|
|
"|zsh", // piping to zsh
|
|
"base64", // base64 encoding/decoding (common for obfuscation)
|
|
"python -c", // inline python execution
|
|
"perl -e", // inline perl execution
|
|
"ruby -e", // inline ruby execution
|
|
"node -e", // inline node execution
|
|
"/dev/tcp", // bash reverse shell
|
|
"nc -e", // netcat reverse shell
|
|
"ncat", // netcat alternative
|
|
"socat", // socket relay
|
|
];
|
|
|
|
/// Additional regex-like patterns that indicate dangerous constructs.
|
|
/// These are checked with simple string matching for complexity reasons.
|
|
const DANGEROUS_PREFIXES: &[&str] = &[
|
|
"rm -rf /", // rm -rf with absolute path
|
|
"rm -rf ~", // rm -rf with home directory
|
|
"rm -rf .", // rm -rf with relative path (current dir)
|
|
"rm -rf *", // rm -rf with wildcard
|
|
];
|
|
|
|
/// 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()
|
|
)));
|
|
}
|
|
if content.contains('\0') {
|
|
return Err(GitError::InvalidArgument(
|
|
"hook content cannot contain null bytes".into(),
|
|
));
|
|
}
|
|
|
|
// Check for forbidden patterns (case-insensitive where appropriate)
|
|
let content_lower = content.to_lowercase();
|
|
for pattern in FORBIDDEN_PATTERNS {
|
|
if content_lower.contains(&pattern.to_lowercase()) {
|
|
return Err(GitError::InvalidArgument(format!(
|
|
"hook content contains forbidden pattern: '{pattern}'"
|
|
)));
|
|
}
|
|
}
|
|
|
|
// Check for dangerous prefixes (exact case)
|
|
for prefix in DANGEROUS_PREFIXES {
|
|
if content.contains(prefix) {
|
|
return Err(GitError::InvalidArgument(format!(
|
|
"hook content contains dangerous command: '{prefix}'"
|
|
)));
|
|
}
|
|
}
|
|
|
|
// Check for obfuscation techniques
|
|
check_obfuscation_attempts(content)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Check for common obfuscation attempts.
|
|
fn check_obfuscation_attempts(content: &str) -> GitResult<()> {
|
|
// Check for excessive use of special characters that might indicate obfuscation
|
|
let special_char_count = content
|
|
.chars()
|
|
.filter(|c| {
|
|
matches!(
|
|
c,
|
|
'$' | '`' | '\\' | '|' | ';' | '&' | '(' | ')' | '{' | '}' | '[' | ']'
|
|
)
|
|
})
|
|
.count();
|
|
let total_chars = content.chars().count();
|
|
|
|
// If more than 30% of content is special characters, it's suspicious
|
|
if total_chars > 0 && (special_char_count * 100 / total_chars) > 30 {
|
|
return Err(GitError::InvalidArgument(
|
|
"hook content appears obfuscated (too many special characters)".into(),
|
|
));
|
|
}
|
|
|
|
// Check for hex encoding attempts (e.g., \x41\x42)
|
|
if content.contains("\\x") {
|
|
let hex_count = content.matches("\\x").count();
|
|
if hex_count > 5 {
|
|
return Err(GitError::InvalidArgument(
|
|
"hook content contains hex encoding (potential obfuscation)".into(),
|
|
));
|
|
}
|
|
}
|
|
|
|
// Check for unicode escape sequences
|
|
if content.contains("\\u") {
|
|
let unicode_count = content.matches("\\u").count();
|
|
if unicode_count > 5 {
|
|
return Err(GitError::InvalidArgument(
|
|
"hook content contains unicode escapes (potential obfuscation)".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(())
|
|
}
|