# Sicherheit Was in dieser Anwendung gegen welche Bedrohung steht — und was ausdrücklich **nicht**. Der zweite Teil ist der wichtigere: Eine Zusage, die nicht gilt, ist gefährlicher als eine fehlende, weil man sich auf sie verlässt. Stand: v1.11.0. Die Fassung entstand beim Abarbeiten von Issue #38 (Sicherheitsprüfung durch ein zweites Modell) und dem eigenen Durchgang entlang der OWASP Top 10 (2021). --- ## Das Bedrohungsmodell Eine ehrenamtliche rechtliche Betreuung. Ein Benutzer, eine betreute Person, ein kleiner Server hinter einem Reverse Proxy. In der Datenbank liegen Gesundheitsdaten (Art. 9 DSGVO), Kontobewegungen und Vermögensverzeichnisse. Nach Wahrscheinlichkeit geordnet: | Was | Wie wahrscheinlich | Was dagegen steht | |---|---|---| | Offene Sitzung an einem unbeaufsichtigten Rechner | hoch | Abmeldung nach 30 min Leerlauf; erneute Bestätigung vor Export, Löschen, Benutzer- und Passkey-Verwaltung | | Verlorenes oder gestohlenes Gerät | mittel | Zweiter Faktor ist Pflicht; Passkeys verlangen Nutzerverifikation | | Kopiertes Backup, falsch gerechtetes Verzeichnis | mittel | Feldverschlüsselung (AES-256-GCM), Dateien im Umschlag, Schlüssel nur in der Umgebung | | Zugangsdaten zum Objektspeicher abhandengekommen | mittel | Objekte liegen verschlüsselt, an ihren Ablageort gebunden | | Raten von Passwörtern über das Formular | mittel | Anmeldesperre nach E-Mail **und** Anschluss, Argon2id | | Einbruch in den Server selbst | gering | Kaum etwas — wer dort Wurzelrechte hat, hat die Schlüssel | Der letzte Punkt ist keine Nachlässigkeit, sondern eine Grenze: Die Anwendung braucht die Schlüssel zur Laufzeit. Wer den Prozess kontrolliert, kommt an sie heran. Dagegen hilft Betriebssicherheit, kein Anwendungscode. --- ## Was ausdrücklich nicht geschützt ist - **Der Server selbst.** Siehe oben. - **Metadaten in der Datenbank.** Verschlüsselt sind einzelne Felder — Diagnosen, IBANs, Verwendungszwecke, Notizen. Dass es einen Vorgang „Betreuungsgericht" gibt und wann er angelegt wurde, steht im Klartext. Sonst ließe sich nichts mehr sortieren oder filtern. - **Symlinks im Ablageverzeichnis.** `resolve()` folgt keinen Links. Wer dort einen anlegen kann, hat bereits Schreibrechte im Datenverzeichnis — dann ist der Umweg über einen Link die geringste Sorge. Bis v1.10.2 stand im Code das Gegenteil; der Satz ist entfernt. - **Die Middleware prüft keine Anmeldung.** Sie setzt Kopfzeilen, mehr nicht. Der Zugriffsschutz sitzt in `src/lib/auth/sitzung.ts`, und nur dort. Bis v1.10.2 behauptete ein Kommentar das Gegenteil. - **Verfügbarkeit.** Es gibt keinen Schutz gegen jemanden, der den Server mit Anfragen überzieht. Bei einem Nutzer hinter einem Proxy ist das die Aufgabe des Proxys. --- ## Der Durchgang entlang der OWASP Top 10 (2021) ### A01 Broken Access Control Jede Server Action und jede Seite geht durch `erfordereZugang()`, das zeilenweise gegen die `betreuteId` prüft — nie nur im UI ausgeblendet. Fremdschlüssel aus Formularen werden über `src/lib/auth/verweise.ts` geprüft; ein verändertes Auswahlfeld kann keine fremde Akte verknüpfen (e2e-Test `fremdakte.spec.ts`). Weiterleitungsziele laufen durch `sicheresZiel()` — kein offener Redirect. **Behoben in dieser Runde:** Die MFA-Pflicht galt nur für Seiten, nicht für Server Actions und Route Handler. Sie steckt jetzt in `erfordereBenutzer()` selbst. `tests/mfa-grenze.test.ts` liest den Quelltext und schlägt an, sobald eine weitere Umgehung dazukommt. ### A02 Cryptographic Failures - Felder: AES-256-GCM, Umschlag `bc1....`, Feldname als Zusatzdaten — ein Geheimtext lässt sich nicht von einem Feld in ein anderes kopieren. - Dateien: AES-256-GCM, Umschlag `BCD1`, seit v1.11.0 **beide** Treiber, mit dem Ablageschlüssel als Zusatzdaten. - Passwörter: Argon2id, Parameter im Hash, Nachziehen beim Anmelden. - Schlüsselrotation über `ENC_KEY_V` / `ENC_KEY_AKTIV`; alte Schlüssel bleiben zum Entschlüsseln stehen. - Alle Geheimnisvergleiche laufen über `timingSafeEqual` — geprüft für Reauth-Cookie, Zugriffstoken, ICS-Signatur, Wartungsschlüssel und das Einrichtungsgeheimnis. **Behoben in dieser Runde:** Der Objektspeicher verließ sich auf `ServerSideEncryption: 'AES256'` — den Schlüssel des Anbieters. ### A03 Injection Prisma parametrisiert. Die fünf Rohabfragen benutzen ausnahmslos `Prisma.sql`-Templates; `Prisma.join()` in `suche.ts` fügt nur bereits parametrisierte Fragmente zusammen und ist damit unbedenklich. Zu XSS: `ts_headline` liefert ``-Marker im Suchausschnitt, und die Quelle ist der OCR-Text hochgeladener Dokumente. Gerendert wird er in `dokument-liste.tsx` aber nicht als HTML, sondern zerlegt und als React- Kinder ausgegeben — also escaped. Das einzige `dangerouslySetInnerHTML` der Anwendung setzt eine feste Konstante für die Themenumschaltung. **Kein Fund.** ### A04 Insecure Design Erneute Bestätigung vor den folgenschweren Handlungen. Anmeldesperre nach zwei Dimensionen. Löschen ist ein Papierkorb mit 30 Tagen Frist. **Behoben in dieser Runde:** Passkeys ließen sich ohne erneute Bestätigung anlegen und entfernen — ein Passkey ist ein vollwertiger Zugang, der einen Passwortwechsel überlebt. ### A05 Security Misconfiguration CSP mit Nonce pro Anfrage und `strict-dynamic` im Produktivbetrieb; dazu `X-Content-Type-Options`, `X-Frame-Options: DENY`, `Referrer-Policy: no-referrer`, `Permissions-Policy`, HSTS mit zwei Jahren. `poweredByHeader` aus. **Behoben in dieser Runde:** `trustHost` stand fest auf `true`, obwohl `AUTH_URL` gesetzt ist. `X-Forwarded-For` wurde ohne Proxy geglaubt und aus dem falschen Ende gelesen. ### A06 Vulnerable and Outdated Components `npm audit`: 0 Schwachstellen (vorher 32, davon 5 kritisch). ### A07 Identification and Authentication Failures Passwort + TOTP in einem Schritt, kein Zwischenzustand. Passkeys mit erzwungener Nutzerverifikation zählen als zwei Faktoren. `sitzungsGeneration` macht alle Sitzungen auf einen Schlag ungültig. Antwortzeiten sind angeglichen, damit sich keine E-Mail-Adressen abfragen lassen. **Behoben in dieser Runde:** TOTP-Codes galten mehrfach; die Anmeldesperre verlängerte sich selbst; die Ersteinrichtung stand offen und war nicht atomar. ### A08 Software and Data Integrity Failures Zu jedem abgelegten Dokument steht eine SHA-256-Prüfsumme über den Klartext in der Datenbank — der Nachweis der Unverändertheit gegenüber dem Gericht. Der Umschlag ist authentifiziert (GCM), Veränderungen fallen beim Lesen auf. ### A09 Security Logging and Monitoring Failures `AuditLog` hält Anmeldungen, Fehlversuche, Exporte, Löschungen und Passkey-Änderungen fest. Anmeldeversuche stehen zusätzlich in `login_versuch` und werden nach 30 Tagen aufgeräumt. **Behoben in dieser Runde:** Während einer Sperre entstand pro Anfrage eine Zeile — die Tabelle ließ sich vollschreiben. Jetzt eine je Sperre. ### A10 Server-Side Request Forgery Die Anwendung ruft nichts von außen ab: kein `fetch`, keine HTTP-Bibliothek, keine Webhooks, keine URL-Vorschauen. Ausgehend geht nur SMTP an den konfigurierten Server. **Keine Angriffsfläche.** --- ## Wenn etwas gefunden wird Kein öffentliches Issue. Der Weg führt über das Dashboard; dort wird entschieden, ob und wie es hier landet.