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:
+37
-4
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user