Files
BanGUI/backend/app/utils/path_utils.py
Lukas d476e9d611 TASK-020: Fix log_target security vulnerability (defense in depth)
**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>
2026-04-26 14:23:56 +02:00

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)