# Sicherheitsprüfung ## Der Stand vom Juli 2026 ist überholt Die damalige Prüfung untersuchte einen Dienstschlüssel, der den Zeilenschutz aushebelte, einen Cookie-Adapter der abgelösten Plattform und ein Anmeldemodell über `auth.users`. **Alle drei Bestandteile gibt es nicht mehr.** Ihre Bewertungen stehen deshalb nicht mehr hier — ein Befund über eine Datei, die entfernt wurde, sagt nichts über den heutigen Zustand, verleitet aber dazu, ihn für geprüft zu halten. Was nachweislich weg ist: | Damals geprüft | Heute | |---|---| | `SUPABASE_SERVICE_ROLE_KEY` | Ersatzlos entfallen. Es gibt keinen Zugang, der den Zeilenschutz umgeht. | | Postgres-Rolle `service_role` | Nur noch eine Platzhalterrolle ohne Anmelderecht, damit alte Migrationen abspielbar bleiben. | | Cookie-Adapter der Plattform | Ersetzt durch Auth.js gegen Entra ID. | | `auth.users` | Ersetzt durch `app_users`; kein Fremdschlüssel zeigt mehr nach `auth`. | | Browser-Anbindung an die Datenbank | Entfallen. Der Browser spricht ausschließlich mit dieser Anwendung. | **Eine neue Prüfung des heutigen Aufbaus steht aus.** Bis dahin gilt: geprüft ist, was unten steht, weil Tests es abdecken — nicht das Übrige. ## Was strukturell gilt und durch Tests abgedeckt ist **Der Zeilenschutz ist die Schranke, nicht die Oberfläche.** Jede Tabelle hat ihn aktiv, dazu 67 Regeln. Ein Sicherheitsnetz in der Datenbank schaltet ihn für neu angelegte Tabellen selbsttätig ein, damit eine vergessene Tabelle nicht offen steht. **Es gibt genau einen Weg an die Datenbank.** `withUser()` setzt den Sitzungskontext in derselben Transaktion wie die Abfrage. Die Verbindung wird nicht ausgeleitet, und eine Linter-Regel verbietet den direkten Import des Treibers ausserhalb von `lib/db/`. Der Nachweis dazu ist `tests/integration/session-context.test.ts`: ohne Kontext liefert die Berechtigungsfunktion nie wahr, und eine Kennung leckt nicht über den Verbindungspool in die nächste Anfrage. **Der Nachweis entsteht in der Datenbank, nicht im Anwendungscode.** Jede ändernde SQL-Funktion schreibt ihren Protokolleintrag in derselben Transaktion wie die Änderung. Ein fehlgeschlagener Eintrag lässt den ganzen Vorgang scheitern. ### Warum es keinen `lib/audit/audit-log.ts` gibt Ein anwendungsseitiger Protokollschreiber wäre eine **zweite** Quelle neben der bestehenden — und die einzige, die sich umgehen liesse, indem jemand die Datenbankfunktion direkt aufruft. Die Aufgabenstellung nennt die Tabellen `audit_logs` und `planned_changes`; im Schema heissen sie `audit_log` (Einzahl) und `pending_org_changes`. Die vollständige Zuordnung steht im [Datenkatalog](datenkatalog.md). ### Der nächtliche Lauf Die Route prüft `Authorization` über `request.headers.get("authorization")`. HTTP-Kopfzeilen sind unabhängig von Gross- und Kleinschreibung, und `Headers.get()` behandelt sie entsprechend. Drei Tests decken das ab: fehlende Kopfzeile, falscher Wert, nicht gesetztes `CRON_SECRET`. Die Antwort ist jeweils `401`, nicht `500`. ## Offen - **Neue Prüfung des heutigen Aufbaus** — Entra ID, `app_users`, `withUser()`, der nächtliche Lauf ohne Sonderrechte. - **Inhaltsrichtlinie scharf schalten** — sie läuft im Nur-Bericht-Modus, bis die Meldungen sauber sind. - **Zugangsdaten rotieren** — siehe README.