"""Path validation for secure file operations.""" from pathlib import Path from typing import List class ValidationError(Exception): """Raised when path validation fails.""" pass class PathValidator: """Validates file paths against security whitelist. Ensures that only files within allowed root directories can be accessed. All paths are resolved to absolute form before validation to prevent directory traversal attacks. """ def __init__(self, allowed_roots: List[Path]): """Initialize validator with allowed root directories. Args: allowed_roots: List of Path objects representing allowed root dirs Raises: ValidationError: If any root doesn't exist or isn't a directory """ self.allowed_roots = [] for root in allowed_roots: root_path = Path(root).resolve() if not root_path.exists(): raise ValidationError( f"Allowed root '{root}' does not exist" ) if not root_path.is_dir(): raise ValidationError( f"Allowed root '{root}' is not a directory" ) self.allowed_roots.append(root_path) def validate(self, path: Path) -> bool: """Validate that path is within an allowed root directory. Args: path: File path to validate Returns: True if path is valid and accessible Raises: ValidationError: If path fails validation """ try: # Resolve to absolute path (handles symlinks, .., etc) file_path = Path(path).resolve() except (OSError, ValueError) as e: raise ValidationError(f"Cannot resolve path '{path}': {e}") from e # Check file exists if not file_path.exists(): raise ValidationError(f"File does not exist: {path}") # Check it's a regular file (not directory, symlink to dir, etc) if not file_path.is_file(): raise ValidationError(f"Path is not a regular file: {path}") # Check path is within an allowed root for allowed_root in self.allowed_roots: try: # This raises ValueError if file_path is not relative to root file_path.relative_to(allowed_root) return True except ValueError: continue # Not in any allowed root raise ValidationError( f"Path '{file_path}' is not within allowed roots: " f"{self.allowed_roots}" ) def is_valid(self, path: Path) -> bool: """Check if path is valid without raising exception. Args: path: File path to check Returns: True if valid, False otherwise """ try: return self.validate(path) except ValidationError: return False