**Issue:** - log_target accepted arbitrary paths, allowing authenticated users to write files as root via fail2ban (e.g., /etc/cron.d/bangui-pwned) - fail2ban runs as root and opens files specified in log_target **Solution:** 1. **Model layer validation:** Already existed in GlobalConfigUpdate, prevents invalid paths before reaching service 2. **Service layer validation:** Added defensive check in update_global_config() that validates log_target even if model validation is bypassed 3. **New validation helper:** Added validate_log_target() utility that accepts special values (STDOUT, STDERR, SYSLOG) or paths within allowed directories **Changes:** - app/utils/path_utils.py: Added validate_log_target() helper - app/services/config_service.py: Added service-layer validation before sending command to fail2ban - backend/tests: Fixed session_secret length issues in fixtures (min 32 chars) - backend/tests: Added tests for valid special log targets - Docs/Backend-Development.md: Documented log_target security requirements **Test Coverage:** - Model validation rejects /etc/passwd (existing test) - Model validation accepts STDOUT, STDERR, SYSLOG special values - Model validation accepts paths in allowed directories - Service layer validation tested with special values Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
63 lines
1.8 KiB
Python
63 lines
1.8 KiB
Python
"""Path validation utilities."""
|
|
|
|
from pathlib import Path
|
|
|
|
from app.config import get_settings
|
|
|
|
|
|
def validate_log_path(log_path: str) -> str:
|
|
"""Validate that a log path is within allowed directories.
|
|
|
|
Resolves the path to its canonical form (resolving symlinks) and checks
|
|
that it is relative to one of the allowed log directories from settings.
|
|
|
|
Args:
|
|
log_path: The log path to validate.
|
|
|
|
Returns:
|
|
The validated log path (unchanged).
|
|
|
|
Raises:
|
|
ValueError: If the path is outside allowed log directories.
|
|
"""
|
|
settings = get_settings()
|
|
try:
|
|
resolved_path = Path(log_path).resolve()
|
|
except (OSError, RuntimeError) as e:
|
|
raise ValueError(f"Cannot resolve path {log_path!r}: {e}") from e
|
|
|
|
for allowed_dir in settings.allowed_log_dirs:
|
|
allowed_path = Path(allowed_dir).resolve()
|
|
try:
|
|
resolved_path.relative_to(allowed_path)
|
|
return log_path
|
|
except ValueError:
|
|
continue
|
|
|
|
allowed_dirs_str = ", ".join(settings.allowed_log_dirs)
|
|
raise ValueError(
|
|
f"Log path {log_path!r} is outside allowed directories: {allowed_dirs_str}"
|
|
)
|
|
|
|
|
|
def validate_log_target(log_target: str) -> str:
|
|
"""Validate that a log target is either a special value or a valid file path.
|
|
|
|
Accepts special values (STDOUT, STDERR, SYSLOG) and file paths that resolve
|
|
to one of the configured allowed log directories.
|
|
|
|
Args:
|
|
log_target: The log target to validate.
|
|
|
|
Returns:
|
|
The validated log target (unchanged).
|
|
|
|
Raises:
|
|
ValueError: If the target is not a special value and not in allowed directories.
|
|
"""
|
|
if log_target.upper() in ("STDOUT", "STDERR", "SYSLOG"):
|
|
return log_target
|
|
|
|
return validate_log_path(log_target)
|
|
|