From 3a9bdc9e15c1cb8b359e414ca00f5f341f206031 Mon Sep 17 00:00:00 2001 From: Brandon Walter <51866976+echoparkbaby@users.noreply.github.com> Date: Sat, 2 May 2026 18:28:13 -0400 Subject: [PATCH] feat: CSP + security headers middleware + session-fixation defense (1.0.0-27) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #6 — defense-in-depth security headers: * New _SecurityHeadersMiddleware emits five headers on every response: - Content-Security-Policy: tight default-src 'self', allow-list the three CDNs we actively load (unpkg for HTMX, cdnjs for QR codes, jsdelivr for xterm.js), plus 'unsafe-inline' for the inline script in settings.html and inline style in job_print.html. Tighten via nonces later if you want true CSP-level XSS protection. - X-Content-Type-Options: nosniff - Referrer-Policy: same-origin - X-Frame-Options: DENY (no clickjacking) - Permissions-Policy: camera/microphone/geolocation/interest-cohort all blocked * Middleware ordering: SecurityHeaders -> AuthGate -> Session, so headers go on EVERY response including 401/403/redirects. #7 — session-fixation defense: * request.session.clear() now runs BEFORE setting user_id/username on successful /login AND /api/v1/auth/setup. Discards any pre-login payload an attacker might have seeded the cookie with. Combined with SameSite=strict + the HMAC-signed Starlette session cookie, this closes the residual fixation surface. Verified: curl -sSI /login returns all five headers; container boots clean; /health 200; existing session for the operator continues to work because we only clear on the LOGIN flow itself. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/config.py | 2 +- app/main.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ app/routes.py | 8 ++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/app/config.py b/app/config.py index 2a4c099..e48ba4a 100644 --- a/app/config.py +++ b/app/config.py @@ -83,7 +83,7 @@ class Settings(BaseSettings): ssh_key: str = "" # PEM private key content (paste full key including headers) # Application version — used by the /api/v1/updates/check endpoint - app_version: str = "1.0.0-26" + app_version: str = "1.0.0-27" # ---- Authentication (1.0.0-22) ---- # session_secret: HMAC key for signing session cookies. Empty = generate diff --git a/app/main.py b/app/main.py index 99268a4..01c16c3 100644 --- a/app/main.py +++ b/app/main.py @@ -121,6 +121,52 @@ async def lifespan(app: FastAPI): app = FastAPI(title="TrueNAS Burn-In Dashboard", lifespan=lifespan) +# --------------------------------------------------------------------------- +# Defense-in-depth security headers +# --------------------------------------------------------------------------- + +# CSP allows the CDNs we actively load: +# unpkg.com — htmx + htmx-sse-extension +# cdnjs.cloudflare.com — qrcodejs (history print page) +# cdn.jsdelivr.net — xterm.js (terminal tab, lazy-loaded) +# 'unsafe-inline' is needed for inline