fix: ADR-080 P0 security + CI remediation from QE analysis

Address all 5 P0 issues from QE analysis (55/100 score):

- P0-1: Rate limiter bypass — validate X-Forwarded-For against trusted proxy list
- P0-2: Exception detail leak — generic 500 messages, exception_type gated by dev mode
- P0-3: WebSocket JWT in URL (CWE-598) — first-message auth pattern replaces query param
- P0-4: Rust tests not in CI — add rust-tests job gating docker-build and notify
- P0-5: WebSocket path mismatch — use WS_PATH constant instead of hardcoded /ws/sensing

Includes ADR-080 remediation plan and 9 QE reports (4,914 lines).
Firmware validated on ESP32-S3 (COM8): CSI collecting, calibration OK.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv
2026-04-06 16:12:13 -04:00
parent b5e924cd72
commit 924c32547e
17 changed files with 5169 additions and 68 deletions
+1 -7
View File
@@ -237,13 +237,7 @@ class AuthenticationMiddleware:
"""Authenticate the request and return user info."""
# Try to get token from Authorization header
authorization = request.headers.get("Authorization")
if not authorization:
# For WebSocket connections, try to get token from query parameters
if request.url.path.startswith("/ws"):
token = request.query_params.get("token")
if token:
authorization = f"Bearer {token}"
if not authorization:
if self._requires_auth(request):
raise AuthenticationError("Missing authorization header")
+3 -4
View File
@@ -202,11 +202,10 @@ class ErrorHandler:
)
# Determine error details
details = {
"exception_type": type(exc).__name__,
}
details = {}
if self.include_traceback:
details["exception_type"] = type(exc).__name__
details["traceback"] = traceback.format_exception(
type(exc), exc, exc.__traceback__
)
+25 -13
View File
@@ -5,7 +5,7 @@ Rate limiting middleware for WiFi-DensePose API
import asyncio
import logging
import time
from typing import Dict, Any, Optional, Callable, Tuple
from typing import Dict, Any, Optional, Callable, Set, Tuple
from datetime import datetime, timedelta
from collections import defaultdict, deque
from dataclasses import dataclass
@@ -128,6 +128,11 @@ class RateLimiter:
self.authenticated_limit = settings.rate_limit_authenticated_requests
self.window_size = settings.rate_limit_window
# Trusted proxy IPs — only trust X-Forwarded-For/X-Real-IP from these
self.trusted_proxies: Set[str] = set(
getattr(settings, "trusted_proxies", [])
)
# Storage for rate limit data
self._sliding_windows: Dict[str, SlidingWindowCounter] = {}
self._token_buckets: Dict[str, TokenBucket] = {}
@@ -196,18 +201,25 @@ class RateLimiter:
return f"ip:{client_ip}"
def _get_client_ip(self, request: Request) -> str:
"""Get client IP address."""
# Check for forwarded headers
forwarded_for = request.headers.get("X-Forwarded-For")
if forwarded_for:
return forwarded_for.split(",")[0].strip()
real_ip = request.headers.get("X-Real-IP")
if real_ip:
return real_ip
# Fall back to direct connection
return request.client.host if request.client else "unknown"
"""Get client IP address.
Only trusts X-Forwarded-For / X-Real-IP when the direct connection
originates from a known trusted proxy. This prevents clients from
spoofing forwarded headers to bypass rate limiting.
"""
connection_ip = request.client.host if request.client else "unknown"
# Only honour forwarded headers from trusted proxies
if connection_ip in self.trusted_proxies:
forwarded_for = request.headers.get("X-Forwarded-For")
if forwarded_for:
return forwarded_for.split(",")[0].strip()
real_ip = request.headers.get("X-Real-IP")
if real_ip:
return real_ip
return connection_ip
def _get_rate_limit(self, request: Request) -> int:
"""Get rate limit for request."""