# Security Review Ergebnis eines gezielten Greps über das gesamte Repository (ohne `node_modules`) nach neun sicherheitsrelevanten Mustern, mit Bewertung im jeweiligen Kontext. Stand: 2026-07-24. ## `SUPABASE_SERVICE_ROLE_KEY` Referenziert in vier Dateien, alle server-seitig / lokal: - `lib/supabase/admin.ts` — die einzige App-Laufzeit-Verwendung, hinter `import "server-only"`. Jetzt mit expliziter Fehlermeldung bei fehlendem Wert statt eines `!`-Non-null-Assertions (siehe Änderungen). - `supabase/seed.ts`, `tests/integration/helpers.ts` — Node-Skripte außerhalb des Next.js-Bundles (Seeding bzw. Test-Setup), lesen den Wert nur aus `process.env`. Unkritisch. - `.scratch_make_test_hr.mjs` — lokales, nicht eingechecktes Hilfsskript (liest den Key ebenfalls nur aus `process.env`, kein Hardcoding). War bisher weder committet noch von `.gitignore` erfasst; **behoben** — `.gitignore` schließt `.scratch_*` jetzt explizit aus, damit ein künftiges `git add -A` es nicht versehentlich eincheckt. Keine Fundstelle exponiert den Key im Browser-Bundle oder in einer API-Response. ## `service_role` (Postgres-Rolle) 9 Treffer, ausschließlich in `supabase/migrations/*.sql` — Standard-Supabase- Muster: - `20260714120500_default_grants.sql`: explizite `grant ... to anon, authenticated, service_role` (nötig, weil RLS Objekt-Rechte nur einschränkt, nicht ersetzt — ohne dieses Grant schlägt jede Query auch mit korrekter Policy mit "permission denied" fehl). - `20260714120200_effective_dating_rpcs.sql` / `20260714120600_...`: `revoke execute ... from public, anon, authenticated; grant execute ... to service_role` für `apply_due_pending_changes()` — das ist die **korrekte** Absicherung: die Funktion darf nur vom Cron-Job (über den Service-Role-Client) aufgerufen werden, nicht von einer eingeloggten HR-Person, sonst könnte diese noch nicht fällige Änderungen vorzeitig erzwingen. Keine problematische Fundstelle. ## `localStorage`, `sessionStorage`, `document.cookie` Keine Treffer im gesamten App-Code. Sessions laufen ausschließlich über den von `@supabase/ssr` verwalteten Cookie-Adapter (`lib/supabase/server.ts`, `proxy.ts`), nicht über direkten Browser-Storage-Zugriff. Kein Risiko einer Token-Exponierung über clientseitigen Storage. ## `dangerouslySetInnerHTML`, `innerHTML` Keine Treffer. Keine rohe HTML-Injection-Fläche im Code. ## `Authorization` Einzige Fundstelle: `tests/unit/security.test.ts` (prüft den Cron-Route- Guard). Die Route selbst liest den Header über `request.headers.get("authorization")` (Kleinschreibung) — HTTP-Header sind case-insensitiv und `Headers.get()` behandelt sie entsprechend, das ist korrekt und wird bereits durch drei Tests abgedeckt (fehlender Header, falscher Wert, nicht konfiguriertes `CRON_SECRET`). ## `audit_logs` / `planned_changes` (aus der Aufgabenstellung) Keine Treffer unter diesen exakten Namen — das reale Schema heißt `audit_log` (Singular) bzw. `pending_org_changes`. Siehe [`docs/data-model.md`](data-model.md) für die vollständige Zuordnung. **Audit-Log-Abdeckung geprüft:** Jede mutierende Server Action (`actions/ employees.ts`, `actions/positions.ts`, `actions/reorg.ts`) ruft ausschließlich `supabase.rpc(...)` auf — keine einzige schreibt direkt per `.from(...).insert()/.update()/.delete()` auf `employees`, `positions`, `employee_notes`, `employee_dependents` oder `profiles`. Jede der dahinterliegenden SQL-Funktionen (`hire_employee`, `transfer_employee`, `promote_employee`, `start_karenz`, `record_karenz_return`, `change_employee_data`, `apply_reorg`, `undo_reorg`, `staff_position_internally`, `delete_position`, `add_employee_dependent`, `delete_employee_dependent`, `add_employee_note`, `complete_employee_note`) schreibt ihren `audit_log`-Eintrag in derselben Transaktion wie die eigentliche Datenänderung. Für die aktuell existierenden Mutationspfade ist damit lückenlos sichergestellt, dass keine Änderung ohne Audit-Eintrag möglich ist — ein fehlgeschlagener Audit-Insert lässt die gesamte Transaktion fehlschlagen. Ausnahme (bewusst, kein Gap): `apply_due_pending_changes()` (vom Cron-Job aufgerufen) schreibt beim tatsächlichen Anwenden einer fälligen Änderung keinen zusätzlichen `audit_log`-Eintrag — der Audit-Eintrag für diese Änderung wurde bereits zum Zeitpunkt der Anforderung geschrieben (von `transfer_employee`/`start_karenz`/etc.), datiert auf das Wirksamkeitsdatum. Ein zweiter Eintrag beim tatsächlichen Anwenden würde denselben Geschäftsvorfall doppelt loggen. ## Warum hier kein neues `lib/audit/audit-log.ts` entstanden ist Eine App-seitige `writeAuditLog()`-Hilfsfunktion wäre eine zweite, nicht-transaktionale Logging-Quelle neben der bestehenden — sie könnte fehlschlagen, nachdem die eigentliche Mutation bereits committet wurde, und so eine Änderung ohne Audit-Spur hinterlassen. Die bestehende Lösung (Audit-Insert in derselben SQL-Funktion/Transaktion) ist strenger. Ein App-seitiger Helfer wäre daher eine Verschlechterung, kein Fix. ## Offene Punkte (siehe README → "Known TODOs") - Kein Content-Security-Policy-Header (bewusst zurückgestellt, siehe Kommentar in `next.config.ts` — Skript-/Style-/Connect-Quellen noch nicht vollständig inventarisiert). - `.scratch_shots/` enthielt PNG-Screenshots einer Testsitzung mit Beispiel-Notiztext ("vertrauliches Gespräch zur Verifikation" — Testdaten, keine echten Personendaten identifiziert). Ordner ist jetzt über `.gitignore` ausgeschlossen; Inhalt selbst wurde nicht gelöscht (siehe Hinweis unten).