Files
vulncheck/app/rate_limiter.py
T
vulncheck 03eef00f31 security: harden auth, secrets, headers and email rendering
Closes 10 findings from the automated security scan (1 critical, 4 high,
5 medium). Operator action required before redeploy — see deploy notes
in chat or README.DEV.md.

Critical:
- TOTP/LDAP Fernet key (AUTH_PROVIDER_CRYPTO_KEY) is now env-only.
  Removed the DB fallback that co-located the key with the ciphertext
  it protects.

High:
- Rate limiter no longer trusts X-Forwarded-For from arbitrary peers.
  TRUSTED_PROXY_CIDRS gates which direct peers may rewrite the client
  IP, and ProxyHeadersMiddleware trusted_hosts is narrowed from "*"
  to FORWARDED_ALLOW_IPS.
- TOTP codes are single-use within their 90s validation window.
  In-memory replay cache keyed on (user_id, code).
- JWTs carry a jti claim; logout revokes both access and refresh JTIs,
  refresh rotates (revokes the presented token), and get_current_user
  rejects any revoked JTI. In-memory store with TTL = token exp.
- Sensitive setting values (wazuh_config, smtp_config, nessus_config)
  are encrypted at rest with an enc:v1: prefix. All read sites go
  through read_setting_value(); legacy plaintext rows still readable
  until next write. GET responses redact secret subfields so admins
  cannot accidentally exfiltrate stored credentials.

Medium:
- Email template rendering HTML-escapes all dynamic values. The "rows"
  variable is whitelisted as pre-escaped HTML. Severity CSS class is
  whitelisted to prevent attribute breakout via crafted package data.
- Request logging redacts sensitive query parameters (token, password,
  code, mfa_token, ...). Validation-error handler no longer logs or
  returns the offending request body.
- /health returns only {"status":"healthy"} — environment and version
  no longer leak to unauthenticated callers.
- SETUP_ADMIN_TOKEN comparison uses hmac.compare_digest.
- Settings PUT denylists auth_provider_crypto_key (env-only) and
  refuses to store the "***set***" redaction placeholder back into
  protected configs.
2026-05-16 09:25:22 +02:00

61 lines
2.2 KiB
Python

import ipaddress
import logging
import os
from fastapi import Request
from slowapi import Limiter
logger = logging.getLogger(__name__)
TRUST_PROXY_HEADERS = os.getenv("TRUST_PROXY_HEADERS", "false").lower() == "true"
# Comma-separated list of CIDR ranges. Only requests whose direct peer is
# inside one of these ranges are allowed to override the client IP via
# X-Forwarded-For / X-Real-IP. Defaults to RFC1918 + loopback for safety —
# operators behind public-internet load balancers must set this explicitly.
_DEFAULT_TRUSTED_PROXIES = "127.0.0.1/32,::1/128,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
_trusted_proxy_cidrs: list = []
for raw in os.getenv("TRUSTED_PROXY_CIDRS", _DEFAULT_TRUSTED_PROXIES).split(","):
raw = raw.strip()
if not raw:
continue
try:
_trusted_proxy_cidrs.append(ipaddress.ip_network(raw, strict=False))
except ValueError:
logger.warning("Ignoring invalid TRUSTED_PROXY_CIDRS entry: %s", raw)
def _peer_is_trusted_proxy(peer_ip: str) -> bool:
try:
addr = ipaddress.ip_address(peer_ip)
except ValueError:
return False
return any(addr in net for net in _trusted_proxy_cidrs)
def get_client_ip(request: Request) -> str:
"""
Resolve client IP. Honour reverse-proxy headers only when the direct
peer is itself a trusted proxy — otherwise the client could spoof
X-Forwarded-For to bypass rate limiting on /auth/login etc.
"""
peer = request.client.host if request.client else None
if TRUST_PROXY_HEADERS and peer and _peer_is_trusted_proxy(peer):
xff = request.headers.get("x-forwarded-for")
if xff:
# Rightmost untrusted hop = leftmost entry the trusted proxy
# received. Take the first non-trusted IP walking from the right.
for candidate in reversed([h.strip() for h in xff.split(",") if h.strip()]):
if not _peer_is_trusted_proxy(candidate):
return candidate
# All hops were trusted proxies — fall back to leftmost.
return xff.split(",")[0].strip()
xri = request.headers.get("x-real-ip")
if xri:
return xri.strip()
return peer or "unknown"
limiter = Limiter(key_func=get_client_ip)