# Sicherheitshärtung — Checkliste mit Verifikation

Jeder Punkt der Härtung ist durch genau einen Test oder Befehl belegbar
(Definition of Done #8). Befehle, die einen laufenden Server oder eine
erreichbare Datenbank benötigen, sind als **(server/db)** markiert und in der
Sandbox **deferred**; ihre statisch prüfbare Grundlage ist jeweils zusätzlich
durch einen Offline-Unit-Test abgesichert.

| # | Härtungspunkt | Verifikation (Test / Befehl) | Sandbox |
|---|---|---|---|
| 1 | **Auth-Gating (oberstes Prinzip).** Jede Seite → Redirect `/login` mit `callbackUrl`; jede API → `401` ohne Daten-Leak. | `npm run test:e2e:gating` (genau ein Fall je `ROUTES`-Eintrag). | deferred (server/db) |
| 2 | **Driftschutz Routen.** Keine ungetestete neue Route unter `src/app/**`. | `npx vitest run tests/unit/routes-manifest.test.ts` (offline). Negativ-Probe: `src/app/(app)/leak/page.tsx` → rot. | offline |
| 3 | **Default-Deny Server Actions.** Jede `"use server"`-Funktion ruft als erste Anweisung einen Guard. | `npx vitest run tests/unit/server-actions-guard.test.ts` (offline). Negativ-Probe: einen Guard entfernen → rot. | offline |
| 4 | **Rollen-/Wehr-Scoping.** `wehr_read` schreibt nicht (403); `wehr_admin` A ändert Wehr B nicht (403/404); eigene Ressource (200). | `npm run test:e2e -- rbac-scoping.spec.ts`. | deferred (server/db) |
| 5 | **Security-Header.** `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, CSP `frame-ancestors 'none'` + `form-action 'self'`, HSTS. | Offline: `npx vitest run src/lib/security/headers.test.ts`. Live: `curl -sI https://<host>/login \| grep -i x-frame-options` → `DENY`; bzw. `npm run test:e2e -- security-headers.spec.ts`. | offline + deferred |
| 6 | **CSP in der App verdrahtet.** `SECURITY_HEADERS` ist in `next.config.ts` (`headers()`) eingehängt. | `npm run build` (Header-Konfiguration wird validiert); Live-Beleg via security-headers.spec.ts. | offline (build) |
| 7 | **Session-Cookie-Flags.** `httpOnly`, `sameSite=lax`; `secure` + `__Secure-`-Präfix nur unter `https://` (Querschnittsstandard 9). | `npm run test:e2e -- security-headers.spec.ts` (Cookie-Assertion). | deferred (server/db) |
| 8 | **argon2id OWASP-Minima.** `type=argon2id` (2), `memoryCost ≥ 19456`, `timeCost ≥ 2`, `parallelism ≥ 1`; Hash beginnt mit `$argon2id$`; Roundtrip. | `npx vitest run src/lib/auth/__tests__/password.test.ts` (offline). | offline |
| 9 | **Login-Rate-Limit im `authorize`-Pfad.** 5 Fehlversuche / 15 min pro Key; Drosselung ab Versuch 6. | Offline-Policy: `npx vitest run src/lib/auth/__tests__/rate-limit.test.ts`. Live: `npm run test:e2e -- login-ratelimit.spec.ts`. | offline + deferred |
| 10 | **CSRF.** State-Changing `POST` ohne gültiges CSRF-Token erzeugt keine Session (Auth.js-CSRF im Credentials-Flow). | Live: `POST /api/auth/callback/credentials` ohne `csrfToken` → keine Session; `GET /api/auth/session` liefert leeres Objekt. | deferred (server) |
| 11 | **Audit-Logging.** Schreib-Aktionen schreiben `audit_log` (eine `writeAudit`-Signatur, optionaler `tx`). Nach `merkmal.promote` existiert eine Zeile. | Live: `select aktion, ziel_typ from audit_log order by zeitpunkt desc limit 1` → `merkmal.promote \| merkmal`. Offline: Server-Action-Unit-Tests prüfen `writeAudit`-Aufruf. | deferred (db) + offline |
| 12 | **API gibt 401/403, kein HTML-Redirect; kein Daten-Leak.** | `npm run test:e2e:gating` (API-Fälle: `expect(status).toBe(401)`, Body ohne Fachbegriffe). | deferred (server) |
| 13 | **`/api/health` anonym 200 (Allowlist).** | Live: `curl -s https://<host>/api/health` → `{"status":"ok"}`. Offline: `tests/unit/routes-manifest.test.ts` belegt `/api/health` in `PUBLIC_ALLOWLIST`. | offline + deferred |
| 14 | **argon2 nicht im Edge-/Middleware-Pfad.** `@node-rs/argon2` wird nur server-seitig importiert. | `npm run build` (Edge-Bundle bricht sonst); Code-Review von `middleware.ts`. | offline (build) |

## Negativ-Proben (Beweis, dass die Tests greifen)

- **Layout-Guard entfernen** (z. B. `await requireSession()` aus
  `src/app/(app)/layout.tsx`): `test:e2e:gating` wird rot (Seiten erreichbar).
- **Manifest-Route entfernen**: Driftschutz `routes-manifest.test.ts` wird rot.
- **Server-Action-Guard entfernen**: `server-actions-guard.test.ts` wird rot.
- **Route ohne Manifest-Eintrag anlegen** (`src/app/(app)/leak/page.tsx`):
  Driftschutz rot; nach Entfernen wieder grün.

## Offline vs. deferred (Sandbox-Hinweis)

In dieser Umgebung gibt es **kein** Postgres und **keinen** laufenden Server.
Verifiziert wurden daher ausschließlich die Offline-Belege:

- `npx tsc --noEmit` (Typprüfung inkl. aller Tests).
- `npx vitest run` (alle reinen Unit-Tests; DB-Roundtrips werden bewusst
  übersprungen).
- `npm run build` (Next.js-Standalone-Build inkl. Header-Verdrahtung).

Die mit **(server/db)** markierten E2E-Punkte werden im CI bzw. lokal gegen
einen geseedeten Server über `npm run test:e2e` / `npm run test:e2e:gating`
ausgeführt.
