## Audit-Ergebnis **Status: Integrationsfähig nach vorgelagerter Auth-/API-Härtung.** Die bestehenden Muster liefern gute Bausteine für Host-Prüfung, CSP, `no-store`, Pfad-Allowlisting, read-only SQLite, Datenverträge und synthetische Tests. **Eine echte Authentisierung und ein strikter GET-Queryparser existieren noch nicht** und dürfen nicht durch CSRF oder Loopback-Binding ersetzt werden. ### Wesentliche Risiken 1. **Important – keine Authentisierung** - `scripts/health/health_dashboard_server.py:345-355` prüft bei `HEAD`/`GET` nur den Host. - `scripts/health/health_dashboard_server.py:38-44,148-150` enthält eine Host-Allowlist, aber keine Benutzer-/Session-Authentisierung. - CSRF-Cookie und Token in `scripts/health/health_dashboard_server.py:180-202,531-535` schützen nur die Schreibaktion; sie authentisieren keine Lesezugriffe. - Das Zielbild verlangt Auth explizit: `docs/dashboard_v5_architecture_target.md:46-58`. - **Folge:** Die sechs APIs dürfen nicht freigeschaltet werden, bevor Dashboard und API denselben Auth-Gate verwenden. Loopback reduziert Exposition, schützt aber nicht gegen andere lokale Prozesse. 2. **Important – aktuelle CSP blockiert API-Fetch** - `scripts/health/health_dashboard_server.py:523-530` setzt `connect-src 'none'`. - Dies wird in `tests/browser/dashboard_v5_echarts_prototype.spec.js:171-177` explizit erwartet. - **Folge:** Ein V5-Frontend kann die neuen APIs nicht abrufen. Nur die API-fähige V5-HTML-Route sollte auf `connect-src 'self'` wechseln; V4/Fallback und nicht API-fähige Oberflächen können bei `'none'` bleiben. 3. **Important – aktuelle Provider lesen teilweise ganze Tabellen** - Symptome: `dashboard_v5/data_provider.py:97-118`. - Apple-Reihen: `dashboard_v5/data_provider.py:121-130`. - Medikationsereignisse: `dashboard_v5/data_provider.py:133-194`. - Labore: `dashboard_v5/data_provider.py:197-237`; `limit` wird erst nach vollständigem Lesen/Filtern angewendet. - **Folge:** Query-Zeitfenster und Limits müssen in SQL beziehungsweise unmittelbar am Cursor durchgesetzt werden, nicht erst nach vollständigem Laden. 4. **Important – kein wiederverwendbarer strikter GET-Queryparser** - Der vorhandene Dashboard-Parameter wird tolerant geparst und unbekannte/mehrfache Parameter werden nicht generell abgewiesen: `health_dashboard_server.py:420-440`. - Das sichere POST-Muster ist dagegen gut: `parse_qs(..., strict_parsing=True)` bei `health_dashboard_server.py:381-385`, danach exakte Schlüsselmenge und Einzelwertprüfung bei `health_dashboard_server.py:275-289`. - **Folge:** Für APIs einen eigenen Parser bauen; den aktuellen `queued`-GET-Code nicht kopieren. 5. **Moderate – Fehlerantworten umgehen die gemeinsamen Datenschutzheader** - Erfolgreiche Antworten erhalten `no-store` und Security-Header zentral: `health_dashboard_server.py:502-536`. - Viele Fehler laufen direkt über `send_error`, z. B. `health_dashboard_server.py:346-354,361-408,445-485`. - **Folge:** JSON-API-Antworten einschließlich `401/403/404/413/500` benötigen einen zentralen Sender mit `Cache-Control: no-store`, `nosniff` und generischer Fehlermeldung. 6. **Moderate – API-Datenbankziel ist im Server nicht fail-closed konfiguriert** - Server verwendet den festen Default `DB = BASE / "health_data.db"`: `health_dashboard_server.py:21-24`. - Die Server-Unit übergibt aktuell keinen expliziten DB-Pfad: `deploy/systemd/health-dashboard.service:10-18`. - Der sichere read-only Connector existiert: `dashboard_v5/data_provider.py:48-51`. - **Folge:** API-Start sollte einen expliziten `HEALTH_DASHBOARD_DB`-Pfad verlangen und ausschließlich `mode=ro` verwenden. 7. **Moderate – generischer Textfilter reicht nicht für Suchsnippets** - Pfad-/URL-Prüfung: `dashboard_v5/contracts.py:30,47-53` und `dashboard_v5/data_provider.py:39-45`. - Eingebettete Pfade wie freier Text plus `/home/...` werden nicht in allen Formen erkannt. - **Folge:** Suche primär über feste IDs, kanonische Labels und eigens freigegebene Metadaten; keine direkte Ausgabe beliebiger DB-Freitexte oder Dokumentvolltexte. ## Sichere Wiederverwendungspunkte - **Host-Gate:** `health_dashboard_server.py:136-150,345-359`. - **Exakter aktueller HTTP-Origin-Check:** `health_dashboard_server.py:153-177`; Tests in `tests/test_dashboard_v5_capture_sprint5c.py:156-185`. - Nur für das aktuelle direkte HTTP-Loopback-Modell wiederverwenden; bei HTTPS/Proxy muss das erwartete Scheme explizit konfiguriert werden. - **Konstante Vergleiche:** `secrets.compare_digest` bei `health_dashboard_server.py:389-397`. - **`no-store` und Basissicherheitsheader:** `health_dashboard_server.py:502-536`. - **Exakte Asset-Allowlist:** `health_dashboard_server.py:47-53,457-465`; Negativtest `tests/test_health_dashboard_v5.py:402-436`. - **Dokument-/Report-Pfadcontainment:** `health_dashboard_server.py:83-133,466-483`; Tests `tests/test_health_dashboard_v4.py:287-309`. - **Kanonischer Metrikkatalog:** `dashboard_v5/metric_registry.py:7-32,74-82`. - **Metrik-/Explorer-Vertrag:** `dashboard_v5/contracts.py:71-176`. - **Bestehende Obergrenzen:** `dashboard_v5/contracts.py:20-29`; serialisiertes Gesamtbudget bei `:334-339`. - **Labor-Allowlist und ASCII-Zahlenvertrag:** `dashboard_v5/lab_registry.py:6-20`; Provenienzprüfung `dashboard_v5/data_provider.py:197-237` und Contract `dashboard_v5/contracts.py:204-223`. - **Missingness/Null/Datumssemantik:** `dashboard_v5/contracts.py:143-176`; Provider `dashboard_v5/data_provider.py:322-363`. - **Read-only SQLite:** `dashboard_v5/data_provider.py:48-51`. - **Synthetische Fixture:** fail-closed Erstellung und Marker `tests/fixtures/dashboard_v5_fixture.py:96-131`, Validierung `:287-321`. - **Synthetischer Server-Sentinel:** `health_dashboard_server.py:61-66,520-521`; Browser-Gate `tests/browser/dashboard_v5.spec.js:15-25`. - **Ephemerer Port-0-Server:** `tests/test_health_dashboard_v5.py:402-436` und `tests/test_dashboard_v5_capture_sprint5c.py:49-153`. ## Minimale Integrationsarchitektur 1. **Ein zentraler Request-Gate vor dem Routing** - Reihenfolge: gültiger `Host` → erlaubte Methode → Authentisierung → Origin/Fetch-Metadata soweit vorhanden → Route. - Auth-Geheimnis nur über die bereits vorgesehene `EnvironmentFile`: `deploy/systemd/health-dashboard.service:16`. - Für die kleinste Browserintegration: HTTP-Auth oder vorgelagerter authentisierender Proxy für **HTML, Assets und API gemeinsam**; Browser-`fetch` erbt die Same-Origin-Credentials. - HTTP-Auth nur bei Loopback; bei nicht lokalen Bindings zwingend TLS. - Kein Token in Querystring, HTML-Bundle oder JavaScript. - Keine CORS-Freigabe; fremde `Origin`-Header ablehnen. Host immer prüfen, auch bei `401`. 2. **Separater `parse_api_query()`** - `parse_qs(..., keep_blank_values=True, strict_parsing=True, max_num_fields=N)`. - Exakte Schlüsselmenge, jeder Parameter höchstens einmal. - ISO-Daten via `date.fromisoformat`, normalisierte Rückgabe. - Enums/IDs ausschließlich aus festen Allowlists. - Gebundene Länge für `q`, `types`, Zeitspanne und Trefferzahl. - Unbekannt, leer, mehrfach, falsch typisiert oder außerhalb des Bereichs → generisches `400`. 3. **Exakte Routen-Allowlist** - Feste Tabelle für: - `/api/v1/metric-catalog` - `/api/v1/series` - `/api/v1/events` - `/api/v1/labs` - `/api/v1/search` - `/api/v1/day/YYYY-MM-DD` nur über vollständigen Regex plus ISO-Validierung. - Keine generische Dateisystemabbildung und keine vom Client gelieferten Tabellen-/Spaltennamen. 4. **Endpoint-Adapter** - **metric-catalog:** ausschließlich `public_registry()`/`public_explorer()`; `q` filtert nur kanonische Labels/Aliasse. - **series:** Metrik-ID über `BY_ID`; SQL mit `from/to`, deterministischer Aggregation und `LIMIT`; bestehender Point-Contract bleibt maßgeblich. - **day:** gezielte Ein-Tages-Abfragen; keine Erzeugung des vollständigen Bundles. - **events:** bestehende Medikationsstatus-Semantik wiederverwenden, aber `types` als feste Enum und Zeitraum bereits in SQL begrenzen. - **labs:** bestehende Parameter-/Einheiten-/Original-Provenienz-Allowlist unverändert wiederverwenden; Zeitraum und Limit früh anwenden. - **search:** registrierte Adapter pro Ergebnisgruppe; nur feste Typen, IDs, sichere Labels, Datum und Navigationstarget. Kein SQL-/Spaltenparameter, Dokumentvolltext, lokaler Pfad oder Rohsnippet. 5. **Antwortschicht** - Einheitlicher JSON-Sender für Erfolg und Fehler. - Immer `application/json; charset=utf-8`, `Cache-Control: no-store`, `X-Content-Type-Options: nosniff`. - `HEAD` liefert dieselben Status/Header und keinen Body. - Bestehende Contract-Maxima als absolute Obergrenzen übernehmen: - Katalog 32, - Serienpunkte 3.660, - Events 1.000, - Labore 100, - Gesamtantwort höchstens 512.000 Bytes. - Für Suche zusätzlich ein deutlich kleineres eigenes Trefferlimit definieren. - Überschreitung fail-closed, nicht still abschneiden, außer der Vertrag dokumentiert Pagination/Cursor. 6. **CSP-Profil** - API-fähige V5-Seite: `connect-src 'self'`. - V4 und sonstige statische Seiten möglichst weiter `connect-src 'none'`. - Keine Remote-URLs/CDNs; Browsertests müssen weiterhin `externalRequests == []` nachweisen. ## Erforderliche synthetische API-Tests - Fehlendes/falsches Auth → `401`; gültiges Auth → Erfolg. - Böser Host wird vor Auth abgewiesen. - Fremder Origin, falsches Scheme/Port und Cross-Site Fetch Metadata werden abgewiesen. - `OPTIONS/PUT/DELETE/PATCH/TRACE/CONNECT` bleiben gesperrt. - Erfolg **und jeder Fehler** tragen `no-store`. - Unbekannte, doppelte, leere und überlange Queryparameter → `400`. - Nicht allowlistete Metrik/Eventart/Labor-ID → `400/404`. - Zeitspanne, Zeilenzahl und Bytebudget werden erzwungen. - Zukunftsdaten, Missingness und echte Null bleiben korrekt. - Day-Route akzeptiert nur exaktes `YYYY-MM-DD`. - Keine Pfade, URLs, Dateinamen oder Rohdokumenttexte in Antworten. - Testserver ausschließlich Port `0`, temporäre markierte DB, read-only Connection und `X-Health-Synthetic-Instance`. ## Verifikation / Tree-Status - Gezielter synthetischer Lauf: **7 Tests bestanden** (`4.68s`) für Host/CSP, Pfadcontainment, exakten Origin, Asset-Allowlist, Bundlelimits, Fixture und Architekturvertrag. - Keine Produktionsdatenbank, Reports, Dokumente, Logs oder Secrets geöffnet. - Keine Dateien erstellt oder verändert. - Abschließendes `git diff --check`: sauber; `git diff --name-only`: leer. - Working Tree unverändert sauber; Branch war bereits `main...origin/main [voraus 1]`.