#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>
9.4 KiB
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 inapps/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/qrcodezuapps/web/package.jsonhinzufügen,pnpm installausführen. -
Task 4:
MfaSetup.tsxumschreiben:<img src="https://api.qrserver.com/...">durch lokale QR-Generierung ersetzenqrcode.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>mitqrserver.comoderchart.googleapis.comgerendert wird - → Datei:
apps/web/src/components/security/MfaSetup.test.tsx(neu oder bestehend ergänzen)
- Unit-Test oder Playwright-Test der prüft, dass kein
Issue #20 — Webhook SSRF-Schutz
-
Task 6:
ssrf-guard.tserstellen mit einerassertWebhookUrlAllowed(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(undhttpnur wenn expliziter Dev-Override gesetzt) - Wirft
TRPCError({ code: "BAD_REQUEST" })mit allgemeiner Fehlermeldung (ohne IP preiszugeben) - → Datei:
packages/api/src/lib/ssrf-guard.ts
- Parst die URL, löst Hostname auf (DNS-Check via Node
-
Task 7:
ssrf-guardinwebhook-support.tsundwebhook-dispatcher.tsintegrieren:- Vor Speicherung + vor Dispatch
assertWebhookUrlAllowed(url)aufrufen - → Dateien:
packages/api/src/router/webhook-support.ts,packages/api/src/lib/webhook-dispatcher.ts
- Vor Speicherung + vor Dispatch
-
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)
- Erlaubt:
Issue #22 — CSP härten
-
Task 9: CSP in
apps/web/next.config.tsüberarbeiten:unsafe-evalentfernen oder nur fürNODE_ENV === "development"erlaubenunsafe-inlineausscript-srcentfernen- 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 demauth()-Call diejtiaus dem Session-Token lesen und gegenprisma.activeSessionvalidieren: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.devprüfen und absichern:pnpm installmuss im Container ablaufen (kein Volume-Mount der Host-node_modules)prisma generatemuss Teil des Starts sein (nicht als Host-Voraussetzung)- Fehlende Systempakete (z. B. OpenSSL für Prisma) explizit installieren
- → Datei:
Dockerfile.dev
-
Task 14:
Dockerfile.prodprü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.ymlabsichern:node_modules-Volume-Override korrekt gesetzt damit Host-Modules nicht reinmappenapp-dev-start.shausfü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:
git clone … && cd capakraken docker compose up --build # → App erreichbar, Login funktioniert, keine Host-Abhängigkeiten→ Schritt in
docs/oderREADME.mdfesthalten
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:unitläuft grünpnpm --filter @capakraken/web exec tsc --noEmit— keine neuen Errorspnpm lint— sauber- #21:
/api/perfohneCRON_SECRETgibt 401/403, Query-Token wird nicht mehr akzeptiert - #19:
MfaSetupmacht keinen Request anqrserver.comoderchart.googleapis.com - #20: Webhook-Save und Dispatch für
http://127.0.0.1/…gibtBAD_REQUEST - #22: Browser-DevTools zeigt keine CSP-Violations für Haupt-User-Flows in Production-Mode
- #23: Request mit gelöschter
ActiveSession-jtigibt 401 - #24:
docker compose up --buildauf 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 |