Phase 2: Fix high-priority security and stability issues (H1-H7)

H1: Sanitize error endpoint context (strip tokens/passwords, limit depth/size)
H2: Rate limiter IP spoofing fix (trusted proxy CIDR check for X-Forwarded-For)
H3: CSRF middleware uses Redis singleton instead of per-request connection
H4: WebSocket origin verification added to both kommunikation and ai_ui_control
H5: Storage path traversal protection, get_url() returns relative URL not filesystem path
H6: Security headers middleware (HSTS, X-Content-Type-Options, X-Frame-Options, CSP, Referrer-Policy)
H7: Forward-repair migration 0045 for databases that ran original 0021/0027

Also: add trusted_proxy_cidrs to config, add verify_ws_origin to auth
This commit is contained in:
Agent Zero
2026-07-26 20:49:15 +02:00
parent 5ec1fc9b05
commit 604a2b7648
10 changed files with 360 additions and 38 deletions
+37 -4
View File
@@ -2,12 +2,14 @@
No auth required so errors can be logged even during logout.
Rate-limited to 10 requests per minute per IP (simple in-memory implementation).
Context data is sanitized to prevent leaking sensitive information.
"""
from __future__ import annotations
import time
import logging
import re
from collections import defaultdict, deque
from typing import Any
@@ -23,6 +25,13 @@ RATE_LIMIT = 10 # max requests
RATE_WINDOW = 60 # seconds
_ip_requests: dict[str, deque[float]] = defaultdict(deque)
# -- Sensitive key patterns to strip from context --
_SENSITIVE_PATTERNS = re.compile(
r"(?i)(token|password|secret|authorization|cookie|session|api[_-]?key|"
r"access[_-]?token|refresh[_-]?token|csrf|bearer|private[_-]?key|"
r"client[_-]?secret|x[_-]?auth|x[_-]?api[_-]?key)",
)
def _is_rate_limited(client_ip: str) -> bool:
"""Return True if the IP has exceeded the rate limit."""
@@ -40,6 +49,25 @@ def _is_rate_limited(client_ip: str) -> bool:
return False
def _sanitize_context(context: Any, max_depth: int = 3, _depth: int = 0) -> Any:
"""Recursively remove sensitive keys and limit depth/size of context data."""
if _depth > max_depth:
return "[truncated]"
if isinstance(context, dict):
sanitized = {}
for key, value in context.items():
if _SENSITIVE_PATTERNS.search(str(key)):
sanitized[key] = "[redacted]"
else:
sanitized[key] = _sanitize_context(value, max_depth, _depth + 1)
return sanitized
if isinstance(context, list):
return [_sanitize_context(item, max_depth, _depth + 1) for item in context[:20]]
if isinstance(context, str) and len(context) > 500:
return context[:500] + "[truncated]"
return context
# -- Request schema --
class ErrorReport(BaseModel):
@@ -54,11 +82,16 @@ class ErrorReport(BaseModel):
@router.post("", status_code=status.HTTP_204_NO_CONTENT)
async def report_error(error: ErrorReport, request: Request) -> Response:
"""Log a frontend error. No auth required. Rate-limited per IP."""
client_ip = request.client.host if request.client else "unknown"
from app.core.rate_limit import get_client_ip
client_ip = get_client_ip(request)
if _is_rate_limited(client_ip):
return Response(status_code=status.HTTP_429_TOO_MANY_REQUESTS)
# Sanitize context to prevent leaking sensitive data
sanitized_context = _sanitize_context(error.context) if error.context else None
# Log with structured info
logger.error(
"Frontend error reported: %s",
@@ -67,14 +100,14 @@ async def report_error(error: ErrorReport, request: Request) -> Response:
"error_timestamp": error.timestamp,
"error_message": error.message,
"error_stack": error.stack,
"error_context": error.context,
"error_context": sanitized_context,
"error_url": error.url,
"error_user_agent": error.userAgent,
"client_ip": client_ip,
},
)
# If forgejo_error_reporter plugin is active, forward error
# If forgejo_error_reporter plugin is active, forward sanitized error
try:
from app.plugins.builtins.forgejo_error_reporter.service import report_error_to_forgejo
entry = {
@@ -83,7 +116,7 @@ async def report_error(error: ErrorReport, request: Request) -> Response:
"url": error.url,
"userAgent": error.userAgent,
"timestamp": error.timestamp,
"context": error.context,
"context": sanitized_context,
}
await report_error_to_forgejo(entry)
except Exception: