Workstream 11: Tests & Sicherheitshärtung (Phase 7)
Beweist die Auth-Gating-Garantie und härtet das System ab (Definition of Done #1, #2, #3, #7, #8): - Routen-Manifest (tests/e2e/routes.manifest.ts) als einzige Quelle der Wahrheit; anonyme Seite -> Redirect /login, anonyme API -> 401. - Kritische auth-gating.spec.ts: genau ein Fall je Manifest-Eintrag, ohne Daten-Leak. - Driftschutz (routes.manifest.spec.ts + tests/unit/routes-manifest.test.ts): keine ungetestete neue Route unter src/app/**. - Default-Deny-Beweis für Server Actions (server-actions-guard.spec.ts + tests/unit/server-actions-guard.test.ts): jede "use server"-Funktion ruft als erste Anweisung einen Guard; Login-Actions per Allowlist ausgenommen. - Wiederverwendbare reine Scanner unter tests/support (route-scan, guard-scan) — offline lauffähig, in Vitest und Playwright geteilt. - rbac-scoping, search-eta, login-ratelimit, security-headers Specs (gegen geseedeten Server; in der Sandbox deferred, per test.skip abgesichert). - global-setup (Migration + Seed) und auth.setup (Login je Konto -> storageState); Playwright-Projekte setup -> chromium verdrahtet. - src/lib/security/headers.test.ts: statischer Beleg für CSP, HSTS, X-Frame-Options DENY, nosniff, Permissions-Policy. - vitest.config.ts: Coverage-Schwellen (>=90 %) für src/lib/search + src/lib/geo. - package.json: Scripts test:unit, test:coverage, test:e2e, test:e2e:gating. - docs/reference/sicherheitshaertung-checkliste.md: jeder Härtungspunkt mit Test/Befehl und Negativ-Probe. Offline verifiziert: tsc --noEmit (0), vitest run (229 passed / 7 db-skipped), drizzle-kit check (ok), next build (exit 0), next lint (0 Fehler), playwright --list (98 Tests, 15 Dateien). DB-/Server-/Browser-abhängige E2E-Läufe sind deferred (kein Postgres/Server in der Sandbox). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
47
docs/reference/sicherheitshaertung-checkliste.md
Normal file
47
docs/reference/sicherheitshaertung-checkliste.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user