0e119cfe73
#19 MFA QR code: render locally via qrcode package, remove external qrserver.com request #20 Webhook SSRF: add ssrf-guard.ts with DNS-verified IP blocklist; enforce on create/update/test/dispatch #21 /api/perf: fail-closed when CRON_SECRET missing; remove query-string token auth #22 CSP: remove unsafe-eval and unsafe-inline from script-src in production builds #23 Active session registry: forward jti into session object; validate against ActiveSession on every tRPC request #24 Docker: add missing packages/application to Dockerfile.dev; fix pnpm-lock.yaml glob; run db:migrate:deploy on container start so a fresh checkout boots without manual steps Also: fix pre-existing TS error in e2e/allocations.spec.ts (args.length literal type overlap) Co-Authored-By: claude-flow <ruv@ruv.net>
195 lines
9.4 KiB
Markdown
195 lines
9.4 KiB
Markdown
# CapaKraken — Umsetzungsplan: Security + Platform Issues
|
||
|
||
Gitea-Repo: `https://gitea.hartmut-noerenberg.com/Hartmut/plANARCHY`
|
||
Stand: 2026-04-01 | Issues: #19, #20, #21, #22, #23, #24
|
||
|
||
---
|
||
|
||
## Anforderungsanalyse
|
||
|
||
Alle 6 Issues stammen aus einem Security-Audit und einem Plattform-Review.
|
||
5 davon sind Security-Findings (OWASP A02/A05/A07/A09), 1 ist ein Plattform-Thema (Docker-Reproduzierbarkeit).
|
||
Die Security-Issues sind weitgehend unabhängig voneinander; lediglich #23 greift in den tRPC-Request-Pfad ein und sollte zuletzt umgesetzt werden, da es den Haupt-Auth-Pfad berührt.
|
||
|
||
---
|
||
|
||
## Betroffene Pakete & Dateien
|
||
|
||
| Issue | Paket/Pfad | Datei | Art |
|
||
|-------|-----------|-------|-----|
|
||
| #21 | `apps/web` | `src/app/api/perf/route.ts` | edit |
|
||
| #19 | `apps/web` | `src/components/security/MfaSetup.tsx` | edit |
|
||
| #19 | `apps/web` | `package.json` | edit (neue Dep: `qrcode` + `@types/qrcode`) |
|
||
| #20 | `packages/api` | `src/lib/webhook-dispatcher.ts` | edit |
|
||
| #20 | `packages/api` | `src/router/webhook-support.ts` | edit |
|
||
| #20 | `packages/api` | `src/lib/ssrf-guard.ts` | create |
|
||
| #22 | `apps/web` | `next.config.ts` | edit |
|
||
| #23 | `apps/web` | `src/app/api/trpc/[trpc]/route.ts` | edit |
|
||
| #23 | `apps/web` | `src/server/auth.ts` | edit (Doku/Cleanup) |
|
||
| #24 | root | `Dockerfile.dev`, `Dockerfile.prod`, `docker-compose.yml`, `docker-compose.prod.yml`, `tooling/docker/app-dev-start.sh` | edit |
|
||
|
||
---
|
||
|
||
## Task-Liste (in empfohlener Reihenfolge)
|
||
|
||
### Issue #21 — /api/perf fail-closed + Query-Token entfernen
|
||
|
||
- [ ] **Task 1:** `GET`-Handler in `apps/web/src/app/api/perf/route.ts` ändern:
|
||
- `if (cronSecret)` → `if (!cronSecret) return 401/403` (fail-closed)
|
||
- `queryToken`-Zweig vollständig entfernen
|
||
- Nur noch `Authorization: Bearer <secret>` prüfen
|
||
- → Datei: `apps/web/src/app/api/perf/route.ts`
|
||
|
||
- [ ] **Task 2:** Unit-Tests für `/api/perf`:
|
||
- Test: autorisiert per Header → 200
|
||
- Test: kein Secret → 401
|
||
- Test: Query-Param-Token → 401 (nicht mehr akzeptiert)
|
||
- Test: fehlende `CRON_SECRET`-Env → fail-closed (kein Metrics-Leak)
|
||
- → Datei: `apps/web/src/app/api/perf/route.test.ts` (neu)
|
||
|
||
---
|
||
|
||
### Issue #19 — MFA QR lokal rendern
|
||
|
||
- [ ] **Task 3:** `qrcode`-Paket und `@types/qrcode` zu `apps/web/package.json` hinzufügen, `pnpm install` ausführen.
|
||
|
||
- [ ] **Task 4:** `MfaSetup.tsx` umschreiben:
|
||
- `<img src="https://api.qrserver.com/...">` durch lokale QR-Generierung ersetzen
|
||
- `qrcode.toDataURL(uri)` im Client-Effekt aufrufen und als `<img src={dataUrl}>` rendern
|
||
- Sicherstellen: der `otpauth://`-URI verlässt den Browser nicht mehr
|
||
- → Datei: `apps/web/src/components/security/MfaSetup.tsx`
|
||
|
||
- [ ] **Task 5:** Test sicherstellen, dass kein Rendering-Request an externe QR-URL geht:
|
||
- Unit-Test oder Playwright-Test der prüft, dass kein `<img src>` mit `qrserver.com` oder `chart.googleapis.com` gerendert wird
|
||
- → Datei: `apps/web/src/components/security/MfaSetup.test.tsx` (neu oder bestehend ergänzen)
|
||
|
||
---
|
||
|
||
### Issue #20 — Webhook SSRF-Schutz
|
||
|
||
- [ ] **Task 6:** `ssrf-guard.ts` erstellen mit einer `assertWebhookUrlAllowed(url: string): void`-Funktion:
|
||
- Parst die URL, löst Hostname auf (DNS-Check via Node `dns.lookup`)
|
||
- Blockt: Loopback (`127.0.0.0/8`, `::1`), RFC1918 (`10.x`, `172.16–31.x`, `192.168.x`), Link-Local (`169.254.x`), Cloud-Metadata (`169.254.169.254`)
|
||
- Blockt: alle Schemes außer `https` (und `http` nur wenn expliziter Dev-Override gesetzt)
|
||
- Wirft `TRPCError({ code: "BAD_REQUEST" })` mit allgemeiner Fehlermeldung (ohne IP preiszugeben)
|
||
- → Datei: `packages/api/src/lib/ssrf-guard.ts`
|
||
|
||
- [ ] **Task 7:** `ssrf-guard` in `webhook-support.ts` und `webhook-dispatcher.ts` integrieren:
|
||
- Vor Speicherung + vor Dispatch `assertWebhookUrlAllowed(url)` aufrufen
|
||
- → Dateien: `packages/api/src/router/webhook-support.ts`, `packages/api/src/lib/webhook-dispatcher.ts`
|
||
|
||
- [ ] **Task 8:** Unit-Tests für `ssrf-guard.ts`:
|
||
- Erlaubt: `https://example.com/hook`
|
||
- Blockt: `http://localhost/…`, `http://127.0.0.1/…`, `http://10.0.0.1/…`, `http://192.168.1.1/…`, `http://169.254.169.254/…`, `ftp://…`
|
||
- → Datei: `packages/api/src/__tests__/ssrf-guard.test.ts` (neu)
|
||
|
||
---
|
||
|
||
### Issue #22 — CSP härten
|
||
|
||
- [ ] **Task 9:** CSP in `apps/web/next.config.ts` überarbeiten:
|
||
- `unsafe-eval` entfernen oder nur für `NODE_ENV === "development"` erlauben
|
||
- `unsafe-inline` aus `script-src` entfernen
|
||
- Nonce-basierte Inline-Scripts prüfen: Next.js 15 unterstützt CSP-Nonces via `nonce`-Prop auf `<Script>` — recherchieren ob tatsächliche Inline-Scripts existieren, die Nonces brauchen
|
||
- Unnötige `connect-src`-Origins bereinigen
|
||
- → Datei: `apps/web/next.config.ts`
|
||
|
||
- [ ] **Task 10:** Smoke-Test: App unter gehärteter CSP starten, Browser-DevTools auf CSP-Violations prüfen (mindestens Login → Dashboard → Timeline → Allocations).
|
||
Gefundene Violations entweder beheben (Move to external file / Nonce) oder als Known Exception dokumentieren.
|
||
|
||
---
|
||
|
||
### Issue #23 — Active-Session-Registry bei jedem Request prüfen
|
||
|
||
- [ ] **Task 11:** Im tRPC-Route-Handler (`apps/web/src/app/api/trpc/[trpc]/route.ts`) nach dem `auth()`-Call die `jti` aus dem Session-Token lesen und gegen `prisma.activeSession` validieren:
|
||
```ts
|
||
const jti = session?.user?.jti as string | undefined;
|
||
if (jti) {
|
||
const active = await prisma.activeSession.findUnique({ where: { jti } });
|
||
if (!active) {
|
||
return NextResponse.json({ error: "Session revoked" }, { status: 401 });
|
||
}
|
||
}
|
||
```
|
||
- Gilt für alle authentisierten tRPC-Requests
|
||
- Gilt auch für nicht-tRPC Auth-Pfade wenn vorhanden (Route-Handler prüfen)
|
||
- → Datei: `apps/web/src/app/api/trpc/[trpc]/route.ts`
|
||
|
||
- [ ] **Task 12:** Unit-/Integrations-Tests:
|
||
- gültige aktive Session → Request durch
|
||
- ausgeloggte (gelöschte) Session → 401
|
||
- durch Concurrent-Session-Limit verdrängte Session → 401
|
||
- → Datei: `apps/web/src/app/api/trpc/[trpc]/route.test.ts` (neu oder ergänzen)
|
||
|
||
---
|
||
|
||
### Issue #24 — Docker-Setup host-unabhängig
|
||
|
||
- [ ] **Task 13:** `Dockerfile.dev` prüfen und absichern:
|
||
- `pnpm install` muss im Container ablaufen (kein Volume-Mount der Host-`node_modules`)
|
||
- `prisma generate` muss Teil des Starts sein (nicht als Host-Voraussetzung)
|
||
- Fehlende Systempakete (z. B. OpenSSL für Prisma) explizit installieren
|
||
- → Datei: `Dockerfile.dev`
|
||
|
||
- [ ] **Task 14:** `Dockerfile.prod` prüfen:
|
||
- Multi-Stage-Build: Build-Stage hat pnpm + alle Dev-Deps; Runtime-Stage nur Prod-Artefakte
|
||
- Generierte Prisma-Artefakte (`node_modules/.prisma`) korrekt aus Build-Stage kopiert
|
||
- → Datei: `Dockerfile.prod`
|
||
|
||
- [ ] **Task 15:** `docker-compose.yml` absichern:
|
||
- `node_modules`-Volume-Override korrekt gesetzt damit Host-Modules nicht reinmappen
|
||
- `app-dev-start.sh` ausführbar und alle Schritte (generate, migrate, start) enthalten
|
||
- → Dateien: `docker-compose.yml`, `tooling/docker/app-dev-start.sh`
|
||
|
||
- [ ] **Task 16:** Frischer-Checkout-Smoke-Test dokumentieren:
|
||
```bash
|
||
git clone … && cd capakraken
|
||
docker compose up --build
|
||
# → App erreichbar, Login funktioniert, keine Host-Abhängigkeiten
|
||
```
|
||
→ Schritt in `docs/` oder `README.md` festhalten
|
||
|
||
---
|
||
|
||
## Abhängigkeiten
|
||
|
||
```
|
||
Task 1 → Task 2 (Tests setzen fertige Impl voraus)
|
||
Task 3 → Task 4 → Task 5 (Dep-Install → Impl → Test)
|
||
Task 6 → Task 7 → Task 8 (Guard-Lib → Integration → Tests)
|
||
Task 9 → Task 10 (CSP-Änderung → Smoke-Test)
|
||
Task 11 → Task 12 (Session-Impl → Tests)
|
||
Task 13–16 unabhängig von allen anderen (Docker-only)
|
||
|
||
Parallel möglich (nach Task-Gruppe):
|
||
- #21 (Tasks 1–2) || #19 (Tasks 3–5) || #24 (Tasks 13–16)
|
||
- #20 (Tasks 6–8) || #22 (Tasks 9–10) — erst nachdem #21/#19 fertig sind (separater Branch)
|
||
- #23 (Tasks 11–12) — zuletzt, da Auth-Pfad berührt
|
||
```
|
||
|
||
---
|
||
|
||
## Akzeptanzkriterien
|
||
|
||
- [ ] `pnpm test:unit` läuft grün
|
||
- [ ] `pnpm --filter @capakraken/web exec tsc --noEmit` — keine neuen Errors
|
||
- [ ] `pnpm lint` — sauber
|
||
- [ ] **#21:** `/api/perf` ohne `CRON_SECRET` gibt 401/403, Query-Token wird nicht mehr akzeptiert
|
||
- [ ] **#19:** `MfaSetup` macht keinen Request an `qrserver.com` oder `chart.googleapis.com`
|
||
- [ ] **#20:** Webhook-Save und Dispatch für `http://127.0.0.1/…` gibt `BAD_REQUEST`
|
||
- [ ] **#22:** Browser-DevTools zeigt keine CSP-Violations für Haupt-User-Flows in Production-Mode
|
||
- [ ] **#23:** Request mit gelöschter `ActiveSession`-`jti` gibt 401
|
||
- [ ] **#24:** `docker compose up --build` auf sauberem Checkout bootet die App ohne Host-Abhängigkeiten
|
||
|
||
---
|
||
|
||
## Risiken & offene Fragen
|
||
|
||
| Risiko | Einschätzung | Maßnahme |
|
||
|--------|-------------|----------|
|
||
| CSP-Nonces in Next.js 15: `<Script>` und Tailwind CSS benötigen ggf. Nonces | Mittel | Vor Task 9 in Next.js-15-Doku recherchieren; ggf. nur `unsafe-eval` entfernen als erster Schritt |
|
||
| Session-Registry-Check (#23) erhöht DB-Load: jeder tRPC-Request = 1 DB-Read | Mittel | Redis-Cache mit kurzer TTL (30s) als Opt-in; erst messen ob nötig |
|
||
| SSRF-Guard DNS-Lookup: async, könnte Race-Condition durch DNS-Rebinding haben | Niedrig | Nach DNS-Lookup Socket-Verbindung ebenfalls gegen IP prüfen (defense-in-depth) |
|
||
| Docker #24: `node_modules`-Volume-Semantik bei pnpm-Workspaces komplex | Mittel | Symlink-Struktur von pnpm in Container testen; ggf. `--shamefully-hoist` Flag |
|
||
| `jti` im Session-Token: Auth.js-Version muss `jti` ins JWT schreiben | Offen | In `auth.ts` prüfen ob `token.jti` tatsächlich im JWT-Callback persistiert wird |
|