// Package installer provides installation utilities for MPV.Rocks. // This file contains defensive validation utilities to help catch potential issues // during development and improve code quality. // // SECURITY NOTE: // These validation functions are DEFENSIVE PROGRAMMING UTILITIES, not security guarantees. // Go's exec.Command already handles proper argument escaping, and the installer avoids shell commands. // These utilities are intended for: // - Catching accidental mistakes during development // - Code review and linting // - Preventing common path manipulation errors // // These should NOT be used as the sole security mechanism. // They are designed to be permissive (block obvious issues) rather than restrictive (block everything). package installer import ( "errors" "fmt" "os" "path/filepath" "runtime" "strings" ) // Unsafe characters that could cause issues in file paths // \x00 (null byte) is particularly dangerous as it can cause silent truncation const unsafePathChars = "\x00" // Shell metacharacters that could enable command injection if shell commands were used const shellMetachars = "`$;|<>()&\n\r\t" // Maximum reasonable path length to prevent potential issues const maxPathLength = 4096 // ValidatePath checks if a path contains potentially unsafe characters or patterns. // // This is a defensive check to catch obvious issues before file operations. // It does NOT replace proper sanitization and is not a security guarantee. // // Purpose: // - Detect null bytes that could cause path truncation // - Validate reasonable path length // - Normalize paths using filepath.Clean() // // Note: filepath.Join() and os.Open() already handle most path traversal attempts safely. // This function is for catching accidental issues during development. // // Returns: // - nil if path passes validation // - error with descriptive message if validation fails // // Example: // // err := ValidatePath("/home/user/.config/mpv") // if err != nil { // return fmt.Errorf("invalid path: %w", err) // } func ValidatePath(path string) error { if path == "" { return errors.New("path cannot be empty") } // Check for null bytes (most dangerous character) if strings.Contains(path, "\x00") { return errors.New("path contains null byte (\\x00) which can cause truncation") } // Check for other unsafe characters if ContainsSpecialChars(path, unsafePathChars) { return errors.New("path contains unsafe characters") } // Check path length if len(path) > maxPathLength { return fmt.Errorf("path length (%d) exceeds maximum allowed length (%d)", len(path), maxPathLength) } // Normalize path to check for obvious traversal patterns cleanPath := filepath.Clean(path) // Check for obvious relative path traversal (though filepath.Join handles this) // This is defensive - the actual operation would still be safe, but we warn developers if strings.Contains(cleanPath, "../") { // Don't error on this, just return info - it might be intentional // We're being permissive, not restrictive // This is logged/warned, not blocked // Commented out as we're being permissive: // return errors.New("path contains parent directory references (..)") } return nil } // ValidatePathForWrite performs additional validation for paths that will be written to. // // In addition to ValidatePath() checks, this validates: // - Path is not a device file // - Path is not a system directory // - Path is reasonably safe for write operations // // Purpose: // - Prevent accidental writes to system directories // - Prevent accidental writes to device files // - Catch configuration errors during development // // Note: The actual file write operations should still use proper error handling. // This function is defensive to catch obvious mistakes early. // // Returns: // - nil if path passes validation // - error with descriptive message if validation fails // // Example: // // err := ValidatePathForWrite("/home/user/.config/mpv/mpv.conf") // if err != nil { // return fmt.Errorf("path not safe for writing: %w", err) // } func ValidatePathForWrite(path string) error { // First, run basic path validation if err := ValidatePath(path); err != nil { return err } // Normalize path absPath, err := filepath.Abs(path) if err != nil { return fmt.Errorf("failed to get absolute path: %w", err) } // Platform-specific system directory checks switch runtime.GOOS { case "windows": if err := validateWindowsPathForWrite(absPath); err != nil { return err } case "linux", "darwin": if err := validateUnixPathForWrite(absPath); err != nil { return err } default: // For unknown platforms, skip platform-specific checks // Be permissive rather than blocking on unfamiliar systems } return nil } // validateWindowsPathForWrite checks Windows-specific paths for safety. // // Blocks writes to: // - Windows directory (C:\Windows) // - System32 directory // - Program Files directories // - Root of drive (C:\) // // This is defensive to prevent accidental system modification. func validateWindowsPathForWrite(absPath string) error { // Check for Windows directory if strings.Contains(strings.ToLower(absPath), `\windows\system32`) { return errors.New("cannot write to Windows system directory") } if strings.Contains(strings.ToLower(absPath), `\windows`) { return errors.New("cannot write to Windows directory") } // Check for Program Files if strings.Contains(strings.ToLower(absPath), `\program files`) { return errors.New("cannot write to Program Files directory") } // Check for root of drive (C:\, D:\, etc.) if len(absPath) == 3 && absPath[1] == ':' && (absPath[2] == '\\' || absPath[2] == '/') { return errors.New("cannot write to root of drive") } return nil } // validateUnixPathForWrite checks Unix-specific paths for safety. // // Blocks writes to: // - /bin, /sbin, /usr/bin, /usr/sbin (executable directories) // - /etc (system configuration) // - /dev (device files) // - /proc, /sys (pseudo-filesystems) // - Root directory (/) // // This is defensive to prevent accidental system modification. func validateUnixPathForWrite(absPath string) error { // List of system directories that should never be written to systemDirs := []string{ "/bin", "/sbin", "/usr/bin", "/usr/sbin", "/usr/local/bin", "/usr/local/sbin", "/etc", "/dev", "/proc", "/sys", "/boot", "/lib", "/lib64", } // Check each system directory for _, sysDir := range systemDirs { if absPath == sysDir { return fmt.Errorf("cannot write to system directory: %s", sysDir) } if strings.HasPrefix(absPath, sysDir+"/") { return fmt.Errorf("cannot write to system directory: %s", sysDir) } } // Check for root directory if absPath == "/" { return errors.New("cannot write to root directory") } return nil } // ValidateShellCommand checks for shell metacharacters and command substitution patterns. // // SECURITY NOTE: // This function is intended for CODE REVIEW and TESTING, not production use. // The installer avoids shell commands entirely and uses Go's exec.Command with proper escaping. // Use this function in tests or linters to detect accidental shell command usage. // // Checks for: // - Shell metacharacters: `, $, ;, &, |, (, ), <, >, newlines, tabs // - Command substitution: $(...) and backticks // - Pipes and chaining // // Purpose: // - Detect accidental shell command inclusion during code review // - Catch patterns that would be dangerous if shell commands were used // - Provide warnings for code that should be reviewed // // DO NOT use this in production code to "sanitize" commands. // The correct approach is to NOT use shell commands at all. // // Returns: // - nil if no shell metacharacters found (command appears safe) // - error with descriptive message if shell metacharacters found // // Example (in tests): // // // This should be caught during code review // cmd := "mpv --config=" + userConfigPath // if err := ValidateShellCommand(cmd); err != nil { // t.Logf("WARNING: Shell command detected: %v", err) // t.Logf("Use exec.Command with properly escaped arguments instead") // } func ValidateShellCommand(cmd string) error { // Check for shell metacharacters if ContainsSpecialChars(cmd, shellMetachars) { return errors.New("command contains shell metacharacters that could enable injection") } // Check for command substitution with $() if strings.Contains(cmd, "$(") { return errors.New("command contains command substitution pattern $()") } // Check for backticks (another form of command substitution) if strings.Contains(cmd, "`") { return errors.New("command contains backticks for command substitution") } // Check for multiple commands chained with && if strings.Contains(cmd, "&&") { return errors.New("command contains command chaining (&&)") } return nil } // ContainsSpecialChars checks if input contains any characters from the provided set. // // This is a helper function used by other validation functions. // // Parameters: // - input: string to check // - chars: string containing characters to look for (each character is checked individually) // // Returns: // - true if any character from chars is found in input // - false if input contains none of the specified characters // // Example: // // unsafe := ContainsSpecialChars("/path/to/file", "\x00") // if unsafe { // log.Println("Path contains null byte") // } func ContainsSpecialChars(input, chars string) bool { if input == "" || chars == "" { return false } return strings.ContainsAny(input, chars) } // IsSafePathForConfig checks if a path is safe for storing configuration files. // // This is a convenience function that combines validation with config-specific checks. // It's useful for validating paths that will store MPV configuration files. // // Parameters: // - path: path to validate // // Returns: // - nil if path is safe for config storage // - error with descriptive message if path is unsafe // // Example: // // configPath := filepath.Join(homeDir, ".config", "mpv", "mpv.conf") // if err := IsSafePathForConfig(configPath); err != nil { // return fmt.Errorf("unsafe config path: %w", err) // } func IsSafePathForConfig(path string) error { // Basic validation if err := ValidatePathForWrite(path); err != nil { return err } // Additional config-specific checks absPath, err := filepath.Abs(path) if err != nil { return fmt.Errorf("failed to get absolute path: %w", err) } // Check if parent directory exists and is writable parentDir := filepath.Dir(absPath) if _, err := os.Stat(parentDir); os.IsNotExist(err) { // Parent directory doesn't exist - this might be okay // but we should create it first if err := os.MkdirAll(parentDir, 0755); err != nil { return fmt.Errorf("failed to create parent directory: %w", err) } } return nil }