webdrop-bridge/src/webdrop_bridge/core/validator.py

97 lines
2.9 KiB
Python

"""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