From 161d5203294a5ab116446e95ad009bfcf7538075 Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 7 May 2026 11:09:06 +0200 Subject: [PATCH] feat: implement CSP and security headers for API responses - Added Content-Security-Policy header to nginx configuration for SPA, enhancing security against XSS attacks. - Introduced middleware in FastAPI to set X-Content-Type-Options header, preventing MIME-sniffing vulnerabilities. - Updated production readiness audit and access layer endpoint audit to reflect security enhancements and ongoing governance practices. - Added tests to verify the presence of security headers in API responses, ensuring compliance with security standards. --- .../working/ACCESS_LAYER_ENDPOINT_AUDIT.md | 6 ++++-- .../PRODUCTION_READINESS_AUDIT_2026-05.md | 18 ++++++++++++------ backend/main.py | 10 ++++++++-- backend/tests/test_security_release.py | 10 ++++++++++ frontend/nginx.conf | 4 +++- 5 files changed, 37 insertions(+), 11 deletions(-) diff --git a/.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md b/.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md index 0f8026d..08b09ea 100644 --- a/.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md +++ b/.claude/docs/working/ACCESS_LAYER_ENDPOINT_AUDIT.md @@ -20,12 +20,14 @@ Fortlaufend gemäß `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` Stufe A–C. | auth | `/api/auth/*` | nein | — | Login/Session | EXEMPT | | catalogs | Katalog-CRUD | nein (global) | `require_auth` | Admin/Trainer je Endpoint | EXEMPT; bei späterem `club_id` nachziehen | | skills | `/api/skills*` | nein (global) | `require_auth` | je Endpoint | EXEMPT | -| maturity_models | Admin-Matrix | nein (global) | `require_auth` | Admin | EXEMPT | +| maturity_models | Admin-Matrix | nein (global) | `require_auth` | Admin für Schreiben; `GET …/{id}` nur Portal-Admin | EXEMPT | | matrix_stack_bundle | Export/Import Bundles | Plattform/Test | `require_auth` | Admin | EXEMPT | | import_wiki / import_wiki_admin | Wiki-Import | Werkzeug | `require_auth`/Admin | Admin | EXEMPT | **Legende:** Router auf der EXEMPT-Liste des Scripts sind globale oder Auth-only-Pfade; sobald ein Router Vereinsdaten oder Bibliotheks-Sichtbarkeit erhält, EXEMPT entfernen und `get_tenant_context` einführen. +**Pflege / Drift:** Änderungen an Mandanten, Governance (`visibility`/`club_id`) oder neuen inhaltsbezogenen Endpoints → eine Zeile in dieser Tabelle anpassen und `PRODUCTION_READINESS_AUDIT_2026-05.md` prüfen. + Letzte Änderung: 2026-05-06 — MULTI_TENANCY §3 Gap-Analyse aktualisiert; Audit um geschützte Profil-Endpunkte ergänzt. --- @@ -33,7 +35,7 @@ Letzte Änderung: 2026-05-06 — MULTI_TENANCY §3 Gap-Analyse aktualisiert; Aud ### Changelog (Fortführung) - **2026-05-07:** Legacy `GET/PUT /api/profile` auf Session-Profil gehärtet; OpenAPI/Health-Ready Produktionsdefaults; Security-Release-Tests + CI-Schritt `security_release_checks.py` — siehe `PRODUCTION_READINESS_AUDIT_2026-05.md`. -- **2026-05-07 (Phase 2):** Geschützte Übungs-Mediendatei-API; `/media`-Static optional; Mitgliederverzeichnis E-Mail eingeschränkt; GET maturity-by-id nur Admin; Postgres `127.0.0.1` im Prod-Compose. +- **2026-05-07 (Phase 3):** CSP SPA (nginx); API `nosniff`-Middleware — siehe `PRODUCTION_READINESS_AUDIT_2026-05.md`. --- diff --git a/.claude/docs/working/PRODUCTION_READINESS_AUDIT_2026-05.md b/.claude/docs/working/PRODUCTION_READINESS_AUDIT_2026-05.md index 79f4491..58ae9de 100644 --- a/.claude/docs/working/PRODUCTION_READINESS_AUDIT_2026-05.md +++ b/.claude/docs/working/PRODUCTION_READINESS_AUDIT_2026-05.md @@ -1,4 +1,4 @@ -# Produktionsreife: Audit-Ergebnis & Umsetzungsplan (Stand 2026-05-07) +# Produktionsreife: Audit-Ergebnis & Umsetzungsplan (Stand 2026-05-07, Phase-3-Update) **Zweck:** Einheitliche Referenz gegen **Drift** zwischen Sicherheits-/Betriebsanforderungen und Code. **Bezug:** Zugriffsschicht `.claude/docs/technical/ACCESS_LAYER_AND_GOVERNANCE_PLAN.md`, Cursor-Regel `.cursor/rules/access-layer.mdc`. @@ -30,8 +30,8 @@ | ID | Befund | Status | |----|--------|--------| -| CON-03 | Sparten (`division`) vs. Zielbild ACCESS_LAYER | **Offen** (Roadmap Stufe D) | -| OPS-02 | CSP / restliche Browser-Härtung | **Teilweise** (Basis-Header nginx; CSP offen) | +| CON-03 | Sparten (`division`) vs. Zielbild ACCESS_LAYER | **Roadmap** — siehe Phase 3 unten (Stufe D); nicht in diesem Schritt implementiert | +| OPS-02 | CSP / restliche Browser-Härtung | **Behoben (Basis):** CSP auf SPA-Dokumente (nginx `location /`); API `X-Content-Type-Options: nosniff` (FastAPI-Middleware) | | MISC-01 | `auth.py` Debug-`print` beim Import | **Behoben** (entfernt) | | MISC-02 | `delete_profile`: DELETE auf Mitai-Tabellen schlägt fehl, wenn Tabelle fehlt | **Behoben:** nur löschen, wenn Tabelle existiert | @@ -57,9 +57,13 @@ ### Phase 3 — Mittelfristig -1. CSP und restliche Header fein abstimmen (PWA, eingebettete Medien). -2. `ACCESS_LAYER_ENDPOINT_AUDIT.md` bei Änderungen an Tenant/Governance aktualisieren (Arbeitsdisziplin). -3. Division-/Sparten-Durchsetzung laut ARCHITEKTUR-Roadmap. +1. **CSP & Header:** Content-Security-Policy für die SPA (nginx, `location /`); `X-Content-Type-Options: nosniff` auf allen FastAPI-Responses — ✅ umgesetzt. +2. **Audit-Pflege:** bei Tenant/Governance-Änderungen `ACCESS_LAYER_ENDPOINT_AUDIT.md` und dieses Dokument anpassen — fortlaufend (Hinweis im Endpoint-Audit). +3. **Division / Sparten (CON-03, Stufe D):** noch **nicht** code-seitig durchgesetzt. Nächste technische Schritte (für eigenes Epic): + - Ist-Zustand: `division_id` auf Objekten & `division_lead` in `can_plan_in_club` ohne strikte Objekt-Filter. + - Ziel: Lesen/Schreiben nur mit passender Sparten-Mitgliedschaft bzw. Rolle; einheitliche Filter in Listen (`training_groups`, Übungen, Planung). + - Tests: Zwei Nutzer, zwei Sparten, kein Cross-Lesen; Superadmin-Pfade getrennt. + - Spec: `ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` Stufe D. --- @@ -79,6 +83,7 @@ | `PUBLIC_OPENAPI` | `1` / `true` → OpenAPI trotz Prod einschalten (nur Debugging) | | `HEALTH_READY_PUBLIC_DETAIL` | `1` / `true` → volle Ready-JSON inkl. Tabellenliste in Prod | | `ALLOW_PUBLIC_MEDIA_STATIC` | `1` / `true` → öffentliches Mount von `/media/` (Notfall/Legacy; Standard: aus) | +| `CSP` (nginx) | Kein Env: Policy fest in `frontend/nginx.conf` (`location /`). API auf **anderer Host-URL** als SPA: `connect-src` in der Policy erweitern oder Build mit envsubst. | --- @@ -87,3 +92,4 @@ ### Changelog - **2026-05-07:** Phase 2 Medien, Mitgliederverzeichnis E-Mail, maturity GET admin-only, Postgres localhost bind, Tests `test_exercise_media_download.py`. +- **Phase 3:** CSP (nginx SPA), API `X-Content-Type-Options` (FastAPI), Test `test_api_attachments_x_content_type_options_nosniff`; Division-Durchsetzung nur als Roadmap (CON-03). diff --git a/backend/main.py b/backend/main.py index 64e3652..629db6f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -6,7 +6,7 @@ Trainer- und Vereinsplattform für Kampfsport-Trainingsplanung from pathlib import Path from typing import Optional -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles @@ -85,7 +85,13 @@ app.add_middleware( allow_headers=["*"], ) -# TODO: Initialize Database with migrations + +@app.middleware("http") +async def add_api_security_headers(request: Request, call_next): + """Konsistente Basis-Header auch für rein JSON-Responses (MIME-Sniffing).""" + response = await call_next(request) + response.headers.setdefault("X-Content-Type-Options", "nosniff") + return response # Version Endpoint (public, no auth) @app.get("/api/version") diff --git a/backend/tests/test_security_release.py b/backend/tests/test_security_release.py index 88eccda..1d50482 100644 --- a/backend/tests/test_security_release.py +++ b/backend/tests/test_security_release.py @@ -114,3 +114,13 @@ assert "schema_migrations_count" in j {"ENVIRONMENT": "production", "HEALTH_READY_PUBLIC_DETAIL": "1"}, ) assert proc.returncode == 0, proc.stderr + proc.stdout + + +def test_api_attachments_x_content_type_options_nosniff(client: TestClient) -> None: + """Globales Middleware: keine MIME-Sniffing-Heuristik für API/Health.""" + r = client.get("/health") + assert r.status_code == 200 + assert r.headers.get("x-content-type-options") == "nosniff" + r2 = client.get("/api/version") + assert r2.status_code == 200 + assert r2.headers.get("x-content-type-options") == "nosniff" diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 0b3a178..4cf0529 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -49,8 +49,10 @@ server { proxy_set_header Host $host; } - # SPA routing - serve index.html for all routes location / { + # Document-CSP für SPA/PWA — React nutzt häufig inline-styles; Mediendateien & API sind same-origin (Proxy). + # Bei separater API-Origin: connect-src hier erweitern oder nginx-Envsubst nutzen. + add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; object-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self'; media-src 'self' blob: data:; worker-src 'self' blob:; manifest-src 'self';" always; try_files $uri $uri/ /index.html; }