Files
CapaKraken/plan.md
T
Hartmut 0e119cfe73 security: close audit findings #19–#23 and harden Docker setup (#24)
#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>
2026-04-01 18:19:21 +02:00

195 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.1631.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 1316 unabhängig von allen anderen (Docker-only)
Parallel möglich (nach Task-Gruppe):
- #21 (Tasks 12) || #19 (Tasks 35) || #24 (Tasks 1316)
- #20 (Tasks 68) || #22 (Tasks 910) — erst nachdem #21/#19 fertig sind (separater Branch)
- #23 (Tasks 1112) — 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 |