TASK-014: Add log path validation to prevent arbitrary file access
Restrict monitored log paths to a configurable allowlist of safe directories to prevent authenticated users from instructing fail2ban to monitor arbitrary files on the system, which could leak contents via fail2ban logging. Changes: - Add 'allowed_log_dirs' setting to Settings (defaults to /var/log, /config/log) - Add @field_validator to AddLogPathRequest to validate log paths at request time - Validator resolves paths to canonical form and checks against allowed prefixes - Use Path.is_relative_to() to prevent prefix bypass attacks like /var/log_evil - Add comprehensive tests for valid/invalid paths and symlink handling - Update Features.md and Backend-Development.md with security documentation Security improvements: - Blocks access to sensitive files (/etc/shadow, /etc/passwd, etc.) - Resolves symlinks before validation to prevent escape routes - Uses proper path comparison instead of string prefix matching - Configurable via BANGUI_ALLOWED_LOG_DIRS environment variable Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -4,9 +4,12 @@ Request, response, and domain models for the config router and service.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
from app.config import get_settings
|
||||
|
||||
DNSMode = Literal["yes", "warn", "no", "raw"]
|
||||
LogEncoding = Literal["auto", "ascii", "utf-8", "UTF-8", "latin-1"]
|
||||
@@ -213,6 +216,43 @@ class AddLogPathRequest(BaseModel):
|
||||
description="If true, monitor from current end of file (tail). If false, read from the beginning.",
|
||||
)
|
||||
|
||||
@field_validator("log_path", mode="after")
|
||||
@classmethod
|
||||
def validate_log_path(cls, value: str) -> str:
|
||||
"""Validate that the 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:
|
||||
value: The log path to validate.
|
||||
|
||||
Returns:
|
||||
The validated log path.
|
||||
|
||||
Raises:
|
||||
ValueError: If the path is outside allowed log directories.
|
||||
"""
|
||||
settings = get_settings()
|
||||
try:
|
||||
resolved_path = Path(value).resolve()
|
||||
except (OSError, RuntimeError) as e:
|
||||
raise ValueError(f"Cannot resolve path {value!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 value
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
allowed_dirs_str = ", ".join(settings.allowed_log_dirs)
|
||||
raise ValueError(
|
||||
f"Log path {value!r} is outside allowed directories: {allowed_dirs_str}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
class LogPreviewRequest(BaseModel):
|
||||
"""Payload for ``POST /api/config/preview-log``."""
|
||||
|
||||
Reference in New Issue
Block a user