97 lines
2.9 KiB
Python
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
|