Sicherheitsaudit - Bitte prüfen #38

Closed
opened 2026-08-23 23:00:27 +00:00 by florian.wilde · 4 comments
Owner

Security-Audit: Betreuungs-Cockpit (Stand ece995b)

Ziel

Die folgenden Punkte stammen aus einem statischen Security-/Code-Review sowie einem lokalen Build- und Testlauf des Commits ece995b (v1.10.2). Bitte die Punkte in der angegebenen Reihenfolge beheben und für jede Sicherheitsgrenze einen Regressionstest ergänzen.

Kurzfazit

Die Anwendung besitzt bereits viele gute Schutzmaßnahmen (serverseitige Mandantenzugriffsprüfung, Argon2id, AES-GCM für Felder und lokale Dateien, sichere Download-Header, CSP, Audit-Log). Vor einem produktiven Einsatz mit Gesundheits- und Finanzdaten sollten aber mindestens die Punkte P0 und P1 behoben werden.

Priorität Befund Risiko
P0 Stark veraltete Runtime-Dependencies mit kritischen Advisories Remote-Angriffe auf Framework/Auth/Parser
P0 MFA wird nur im App-Layout, nicht an Server Actions/API-Routen erzwungen Fachzugriff mit Passwort-only-Sitzung
P0 Öffentliche Erstinstallation ist übernehmbar und race-anfällig Unbefugter Administratorzugang
P1 S3-Dokumente sind nicht anwendungsseitig verschlüsselt Klartextzugriff durch Storage-Anbieter/Storage-Kompromittierung
P1 Login-Endpunkt schreibt auch während Sperre unbegrenzt DB-Zeilen Unauthentifizierter Storage-/DB-DoS
P1 Unbegrenzte Dekompression/Verarbeitung fremder PDFs und Bilder Authentifizierter Memory-/CPU-DoS
P1 Passkeys lassen sich ohne frische Bestätigung hinzufügen/entfernen Persistenz nach Sitzungsdiebstahl
P2 TOTP-Codes können im gültigen Zeitfenster wiederverwendet werden Replay eines abgefangenen Codes
P2 Proxy-Header werden ungeprüft als Client-IP vertraut Rate-Limit-Spraying bei Fehlkonfiguration
P3 Sicherheitslogik und Kommentare sind teilweise dupliziert/irreführend Erhöhte Gefahr künftiger Schutzlücken

[P0] Verwundbare Runtime-Dependencies unverzüglich aktualisieren

Fundstellen

  • package.json: next@15.1.6
  • package.json: next-auth@5.0.0-beta.25 / transitiv @auth/core@0.37.2
  • package.json: nodemailer@6.10.0
  • package.json: @aws-sdk/client-s3@3.735.0 und @aws-sdk/s3-request-presigner@3.735.0
  • Außerdem verwundbare Build-/Test-Pakete: vitest@3.0.5, @playwright/test@1.50.1, tsx@4.19.2, postcss@8.5.1, eslint@9.19.0

Evidenz

npm audit meldet 32 Vulnerabilities: 5 critical, 5 high, 20 moderate, 2 low. Besonders relevant:

  • Next.js: mehrere DoS-, SSRF-, Cache-Poisoning-, Middleware-Bypass- und Server-Action-Probleme; Audit-Fix: next@15.5.23.
  • Auth.js/NextAuth: unter anderem ein kritischer Fail-open bei fehlerhaften Auth-Konfigurationen; Audit-Fix: mindestens next-auth@5.0.0-beta.32.
  • Nodemailer: mehrere Header-/SMTP-Injection-, SSRF/File-read- und DoS-Advisories; Audit schlägt 9.0.5 vor (Major-Upgrade).
  • fast-xml-parser transitiv über AWS SDK: kritische Entity-/Regex-Probleme; AWS SDK aktualisieren.
  • Vitest < 3.2.6: kritischer File-read/RCE-Befund im UI-Server (primär Entwicklungsumgebung).

Nicht jedes Advisory ist mit der aktuellen Konfiguration unmittelbar ausnutzbar (zum Beispiel wird kein NextAuth-E-Mail-Provider verwendet). Bei der Sensitivität der Daten ist die verwundbare Basis trotzdem nicht vertretbar.

Behebung

  1. Next.js innerhalb der 15er-Linie mindestens auf die von npm audit genannte gepatchte Version aktualisieren; danach aktuellen Security-Stand erneut prüfen.
  2. NextAuth/Auth.js mindestens auf die gepatchte Beta aktualisieren und Auth-/Passkey-Flows vollständig regressionsprüfen.
  3. Nodemailer auf eine aktuell gepatchte Version migrieren; Transportoptionen und Typen prüfen.
  4. AWS SDK, Vitest, Playwright, tsx, PostCSS und ESLint aktualisieren.
  5. package-lock.json neu erzeugen und npm audit --omit=dev sowie vollständiges npm audit in CI als Gate ausführen.

Akzeptanzkriterien

  • npm audit --omit=dev enthält keine Critical/High-Funde.
  • Das vollständige Audit enthält keine Critical/High-Funde oder dokumentierte, nachweislich nicht anwendbare Ausnahmen mit Ablaufdatum.
  • Login mit Passwort+TOTP, Passkey-Anmeldung, Reauth, Upload, PDF/DOCX/EML und S3-Download sind regressionsgetestet.

[P0] MFA-Schranke an jeder serverseitigen Fachoperation erzwingen

Fundstellen

  • src/auth.ts:144-147: Benutzer ohne eingerichtetes TOTP erhalten bewusst bereits eine gültige Sitzung.
  • src/app/(app)/layout.tsx:22-23: MFA wird nur beim Rendern des App-Layouts erzwungen.
  • src/lib/auth/sitzung.ts:50-116: erfordereBenutzer() und erfordereZugang() prüfen nicht, ob MFA eingerichtet/abgeschlossen wurde.
  • Nahezu alle Fach-Server-Actions verwenden nur erfordereBenutzer() oder erfordereZugang().
  • Beispiel: src/server/actions/betreute.ts:111-157 erlaubt mit einer Passwort-only-Sitzung das Anlegen einer Betreuung.
  • Auch direkte Routen wie src/app/(app)/schreiben/docx/route.ts, eml/route.ts und mehrere Downloads verlassen sich nur auf diese Helfer.

Auswirkung

Ein neu angelegter Benutzer kann sich nur mit E-Mail und Passwort anmelden und erhält vor Abschluss der MFA-Einrichtung bereits ein gültiges JWT. Die UI leitet ihn zwar zur TOTP-Einrichtung um, Server Actions und Route Handler sind aber direkt aufrufbar. Damit ist „MFA Pflicht vor Fachdaten“ keine serverseitige Sicherheitsgrenze.

Bei einem neuen Admin ist das besonders kritisch: Mit der Passwort-only-Sitzung können direkt Fachaktionen angestoßen werden, ohne die TOTP-Einrichtung abzuschließen.

Behebung

  • Einen zentralen Guard einführen, der neben Benutzerstatus/Sitzungsgeneration auch einen serverseitig vertrauenswürdigen MFA-Zustand prüft.
  • erfordereZugang() muss diesen Guard standardmäßig einschließen.
  • Nur eine sehr kleine Allowlist darf ohne abgeschlossene MFA funktionieren: TOTP vorbereiten/bestätigen, Logout und unbedingt benötigte Konto-/Setup-Schritte.
  • Nicht allein session.user.zweiterFaktorAktiv vertrauen: jwt({ trigger: 'update' }) übernimmt dieses Feld aktuell aus clientgesteuerten Session-Updates (src/auth.ts:331-343). Maßgeblich muss der frische DB-Zustand sein oder ein serverseitig ausgestelltes Auth-Level.
  • Route Handler müssen mit 401/403 antworten statt durch Layout-Redirects geschützt zu werden.

Akzeptanzkriterien

  • Integrationstest: Passwort-login eines Users ohne aktiviertes TOTP; direkte Aufrufe repräsentativer Lese-, Schreib-, Export- und Download-Endpunkte werden abgewiesen.
  • Nur TOTP-Setup und Logout bleiben erreichbar.
  • Nach erfolgreicher TOTP-Bestätigung funktionieren Fachoperationen.
  • Ein manipuliertes session.update({ zweiterFaktorAktiv: true }) schaltet keine Fachoperation frei.

[P0] Erstinstallation gegen Übernahme und Race Conditions absichern

Fundstelle

src/server/actions/ersteinrichtung.ts:44-87

Problem

erstenZugangAnlegen() ist unauthentifiziert und verwendet das Muster „user.count(); danach user.create()“. Wird eine frisch gestartete Instanz vor dem Betreiber erreicht, kann jeder den ersten ADMIN anlegen. Außerdem können zwei parallele Requests beide count() === 0 sehen und anschließend zwei Administratoren mit unterschiedlichen E-Mail-Adressen anlegen. Es gibt keine DB-Constraint, die „genau ein erster Benutzer“ atomar erzwingt.

Behebung

Eine der folgenden Varianten atomar umsetzen:

  • Ein zufälliges, einmaliges Setup-Token aus Secret/Datei/Environment verlangen und nach erfolgreicher Nutzung serverseitig verbrauchen; oder
  • ersten Admin ausschließlich per CLI/Seed auf dem Server anlegen; oder
  • Setup nur auf Loopback/Unix-Socket zulassen.

Zusätzlich die Initialisierung in einer serialisierbaren DB-Transaktion bzw. über eine eindeutige Setup-State-Zeile absichern. Ein bloßer zweiter count() reicht nicht.

Akzeptanzkriterien

  • Ohne Setup-Berechtigung kann kein erster Benutzer angelegt werden.
  • Zwei parallele gültige Setup-Requests erzeugen exakt einen Admin.
  • Nach Abschluss ist das Setup-Secret unwiderruflich verbraucht.
  • Der Vorgang wird auditierbar protokolliert, ohne das Setup-Secret zu loggen.

[P1] Dokumente auch im S3-Treiber anwendungsseitig verschlüsseln

Fundstellen

  • src/lib/speicher/datei.ts:63-100: lokaler Dateitreiber verschlüsselt Inhalte korrekt mit AES-256-GCM.
  • src/lib/speicher/s3.ts:45-61: S3-Treiber lädt den Klartext hoch und setzt nur ServerSideEncryption: 'AES256'.
  • docker-compose.yml: Produktion erzwingt SPEICHER_TREIBER=s3.

Auswirkung

Bei SSE-S3 verwaltet der Storage-Anbieter den Schlüssel und sieht technisch den Klartext. Ein kompromittierter S3-Account, Anbieterzugriff oder falsch konfigurierter Bucket umgeht damit die in den Kommentaren formulierte Trennung „Daten und Anwendungsschlüssel werden zusammen benötigt“. Gerade Arztbriefe, Bescheide und Kontoauszüge sind besonders schützenswert.

Behebung

  • Das vorhandene binäre AES-GCM-Umschlagformat treiberunabhängig vor PutObject und nach GetObject anwenden.
  • Objektmetadaten dürfen keine sensitiven Klartexte enthalten.
  • Schlüsselrotation für Dateiobjekte implementieren und dokumentieren.
  • Bestehende Klartextobjekte migrieren; Migration wiederanlaufbar und prüfbar gestalten.
  • SSE-S3 zusätzlich beibehalten (Defense in Depth), aber nicht als Ersatz für Client-side/Application-level Encryption behandeln.

Akzeptanzkriterien

  • Direkt aus dem Bucket geladenes Objekt enthält weder Originaldatei noch erkennbaren Klartext.
  • Manipulierte Ciphertexte werden durch GCM-Authentifizierung abgewiesen.
  • Alte und neue Schlüsselversionen können gelesen, neue Objekte nur mit aktiver Version geschrieben werden.

[P1] Unbegrenztes Schreiben in login_versuch verhindern

Fundstellen

  • src/auth.ts:110-114: Auch bei bereits festgestellter Sperre wird erneut versuchFesthalten() aufgerufen.
  • src/lib/auth/ratelimit.ts:56-73: Jeder Versuch erzeugt eine neue DB-Zeile.
  • Aufräumen erfolgt erst nach 30 Tagen.

Auswirkung

Ein nicht authentifizierter Angreifer kann den Login-Endpunkt kontinuierlich aufrufen und pro Request dauerhaft eine Zeile erzeugen. Bereits gesperrte IP-/E-Mail-Kombinationen werden nicht früh verworfen, sondern schreiben weiter. Das ermöglicht DB-Wachstum, I/O-Last und letztlich Storage Exhaustion. Gleichzeitig verschiebt jeder neue Eintrag den jüngsten Fehlversuch und kann Sperren unbegrenzt verlängern.

Behebung

  • Bei aktiver Sperre nicht pro Request eine neue Detailzeile schreiben; stattdessen aggregierten Zähler/lastSeen mit begrenzter Kardinalität verwenden oder Schreibfrequenz drosseln.
  • Zusätzlich Edge-/Reverse-Proxy-Rate-Limit pro vertrauenswürdiger Client-IP und globales Notfalllimit einführen.
  • Harte Retention/Partitionierung und maximale Tabellen-/Kardinalitätsstrategie definieren.
  • Sperrzeit an einen festen Start/lockedUntil binden, nicht mit jedem gesperrten Request verlängern, sofern eine permanente Verlängerung nicht ausdrücklich gewollt ist.

Akzeptanzkriterien

  • 10.000 Requests gegen einen bereits gesperrten Schlüssel erzeugen keine 10.000 DB-Zeilen.
  • Speicher- und CPU-Verbrauch bleiben begrenzt.
  • Normale Anmeldung wird nicht durch beliebige Spoof-Header am Proxy-Limit vorbeigeführt.

[P1] Ressourcenlimits/Sandbox für PDF- und Bildverarbeitung ergänzen

Fundstellen

  • src/lib/ocr/pdf-text.ts:106-160: Angreifergesteuerte PDF-Streams werden synchron mit inflateSync, unzipSync und inflateRawSync ohne maxOutputLength dekomprimiert.
  • src/lib/ocr/index.ts:146-151: pdftoppm hat ein Zeitlimit, aber kein OS-/Container-Memory-/CPU-Limit.
  • src/lib/ocr/index.ts:110-150: Tesseract/Poppler verarbeiten fremde Binärdaten direkt.
  • Uploadlimit 25 MiB schützt nicht gegen hohe Kompressionsraten oder Bild-/PDF-Dekompressionsbomben.

Auswirkung

Ein berechtigter Benutzer (oder ein kompromittiertes Mitbearbeiterkonto) kann mit einer kleinen komprimierten Datei sehr hohe RAM-/CPU-Last erzeugen. Synchrone Dekompression blockiert zusätzlich den Node-Eventloop. Native Parser vergrößern die Angriffsfläche.

Behebung

  • zlib mit maxOutputLength und einem niedrigen globalen Dekompressionsbudget verwenden; synchrone Verarbeitung aus dem Request-Prozess entfernen.
  • OCR in separaten Worker/Container mit CPU-, RAM-, PID-, Dateigrößen- und Zeitlimits ausführen; idealerweise ohne Netzwerk, read-only Root-FS und mit seccomp/AppArmor.
  • Pixelzahl, Seitenzahl und entpackte Größe vor weiterer Verarbeitung begrenzen.
  • Warteschlange und Concurrency-Limit einführen.
  • Fehler-/Timeoutpfade testen; Dokument darf gespeichert werden, OCR muss kontrolliert abbrechen.

Akzeptanzkriterien

  • Kompressionsbomben, extrem große Seitenflächen und absichtlich langsame PDFs überschreiten definierte RAM-/CPU-Grenzen nicht.
  • Der Webprozess bleibt während des Tests erreichbar.

[P1] Passkey-Verwaltung nur nach frischer Reauth erlauben

Fundstellen

  • src/server/actions/passkey.ts:77-126: Passkey hinzufügen verlangt nur erfordereBenutzer().
  • src/server/actions/passkey.ts:133-167: Passkey löschen verlangt ebenfalls nur erfordereBenutzer().
  • Für andere sensible Aktionen existiert bereits erfordereFrischeAnmeldung().

Auswirkung

Wer eine aktive Sitzung übernimmt (unbeaufsichtigtes Gerät, Cookie-Diebstahl), kann einen eigenen Passkey registrieren und erhält damit dauerhaften Zugang, selbst nachdem die Sitzung abläuft. Außerdem kann er legitime Passkeys entfernen.

Behebung

  • Start und Abschluss der Registrierung sowie Entfernen eines Passkeys an eine frische Reauth binden.
  • Die Reauth muss spätestens beim Abschluss erneut geprüft werden, da zwischen Challenge und Antwort Zeit vergeht.
  • Optional Benachrichtigung/Audit-Warnung an den Kontoinhaber.

Akzeptanzkriterien

  • Eine alte, aber gültige Sitzung kann weder Passkey hinzufügen noch entfernen.
  • Eine fremde oder nach Reauth-Ablauf beantwortete Challenge wird abgewiesen.

[P2] TOTP-Replay verhindern

Fundstellen

  • src/lib/crypto/totp.ts:69-84: Kommentar verspricht, dass der Aufrufer das getroffene Zeitfenster speichert.
  • src/auth.ts:151-153 und src/server/actions/bestaetigen.ts:59-63: Es wird nur gueltig ausgewertet; delta/Zeitschritt wird nirgends gespeichert.
  • Im User-Modell existiert kein Feld für den zuletzt akzeptierten TOTP-Zeitschritt.

Auswirkung

Derselbe abgefangene Code kann innerhalb des akzeptierten Fensters (aktuelle Periode plus Toleranz) mehrfach verwendet werden, beispielsweise einmal zur Anmeldung und erneut für eine sensible Reauth. TOTP ist damit faktisch nicht „one time“.

Behebung

  • Absoluten TOTP-Zeitschritt atomar pro Benutzer speichern und nur Werte größer als der zuletzt akzeptierte Schritt zulassen.
  • Prüfung und Update in einer atomaren DB-Operation/Transaktion durchführen, damit parallele Replays nicht beide gewinnen.
  • Login und Reauth müssen dieselbe Replay-Sperre verwenden.

Akzeptanzkriterien

  • Derselbe Code wird beim zweiten Einsatz abgewiesen, auch bei parallelen Requests.
  • Ein neuer Code im nächsten gültigen Schritt funktioniert.

[P2] Vertrauensgrenze für Proxy-/Client-IP-Header definieren

Fundstelle

src/auth.ts:65-68

Problem

Die Anwendung verwendet ungeprüft den ersten Wert von X-Forwarded-For, alternativ X-Real-IP. Ist die App entgegen der Empfehlung direkt erreichbar oder überschreibt der Reverse Proxy eingehende Header nicht zuverlässig, kann der Client seine IP frei wählen und IP-basierte Limits umgehen.

Behebung

  • Eingehende Forwarded-Header am Reverse Proxy immer entfernen/neu setzen.
  • In der Anwendung nur eine dokumentierte Anzahl vertrauenswürdiger Proxy-Hops akzeptieren oder ausschließlich einen vom eigenen Proxy gesetzten Header auswerten.
  • Direktzugriff auf die App per Firewall/Loopback ausschließen.
  • trustHost: true nicht hart kodieren, sondern nur in kontrollierter Proxy-Konfiguration aktivieren.

Akzeptanzkriterien

  • Ein Client kann durch selbst gesetzte X-Forwarded-For-Werte seine Rate-Limit-Identität nicht ändern.
  • Proxy- und Direktzugriffsfall sind automatisiert getestet/dokumentiert.

[P3] Sicherheitslogik zentralisieren und irreführende Aussagen korrigieren

Beispiele

  • src/middleware.ts:25 behauptet „Ohne Authentifizierungs-Cookie gar nicht erst weiterreichen“, die Middleware prüft aber überhaupt kein Cookie.
  • Rollen-/Zugangsauflösung wird in erfordereZugang(), Dokumentdownload, Kalenderroute, Export und App-Layout mehrfach manuell nachgebaut.
  • src/lib/speicher/datei.ts:45 behauptet, die kanonische Pfadprüfung fange Symlink-Tricks ab. Eine reine resolve()-Präfixprüfung verhindert keine bereits im Ablagebaum platzierten Symlinks.
  • MFA- und Verschlüsselungskommentare versprechen teilweise stärkere Garantien, als die Implementierung tatsächlich liefert.

Behebung

  • Gemeinsame Guards für requireAuthenticated, requireMfa, requireAccess und requireRecentAuth schaffen und überall verwenden.
  • Zugangsauflösung nicht kopieren, sondern zentral aufrufen.
  • Kommentare als überprüfbare Sicherheitsinvarianten formulieren und durch Tests belegen.
  • Für lokale Dateien symlink-sichere Zugriffe verwenden bzw. Ablagebaum exklusiv besitzen, Rechte härten und lstat/realpath/O_NOFOLLOW-Strategie dokumentieren.

Akzeptanzkriterien

  • Keine Route dupliziert die Rollenauflösung.
  • Sicherheitskommentare entsprechen nachweislich dem Verhalten.
  • Tests decken die zentralen Guards und negative Zugriffsfälle ab.

Durchgeführte Prüfungen

  • Commit: ece995b
  • npm ci --ignore-scripts mit lokalem Cache: erfolgreich (Peer-Warnungen zwischen NextAuth und SimpleWebAuthn)
  • Prisma Client Generation: erfolgreich
  • Production Build (next build): erfolgreich
  • TypeScript (tsc --noEmit): erfolgreich
  • ESLint (npm run lint): erfolgreich, 0 Warnungen/Fehler
  • Unit-Tests (vitest run): 28 Testdateien, 658 Tests, alle erfolgreich
  • npm audit: 32 Funde (5 critical, 5 high, 20 moderate, 2 low)
  • Statischer Review: Auth/AuthZ, Server Actions, Route Handler, Kryptografie, Upload/OCR, Datei/S3-Speicher, CSP/Headers, Docker/Deployment und Setup

Nicht vollständig dynamisch geprüft

  • Die Playwright-E2E-Suite benötigt laut Repository eine laufende PostgreSQL-/MinIO-Entwicklungsumgebung, Seed-Daten, .env, Browser-Binaries und einen separat gestarteten Dev-Server. Diese Infrastruktur wurde in diesem read-only Audit nicht aufgebaut; die vorhandenen E2E-Tests wurden daher nicht als bestanden gewertet.
  • Kein aktiver Penetrationstest gegen eine laufende produktionsnahe Instanz.
  • Keine Prüfung der realen Reverse-Proxy-, Firewall-, S3-Bucket-, Backup- und TLS-Konfiguration.

Empfohlene Reihenfolge

  1. Runtime-Dependencies patchen.
  2. Zentralen MFA-Guard einführen und alle Daten-Endpunkte absichern.
  3. Erstsetup atomar und geheimnisgebunden machen.
  4. S3-Anwendungsverschlüsselung und Migration umsetzen.
  5. Login- und Datei-Verarbeitungs-DoS begrenzen.
  6. Passkey-Reauth, TOTP-Replay-Sperre und Proxy-Vertrauensgrenze schließen.
  7. Danach vollständige E2E- und negative Security-Tests in einer produktionsnahen Umgebung ausführen.
Security-Audit: Betreuungs-Cockpit (Stand `ece995b`) ## Ziel Die folgenden Punkte stammen aus einem statischen Security-/Code-Review sowie einem lokalen Build- und Testlauf des Commits `ece995b` (`v1.10.2`). Bitte die Punkte in der angegebenen Reihenfolge beheben und für jede Sicherheitsgrenze einen Regressionstest ergänzen. ## Kurzfazit Die Anwendung besitzt bereits viele gute Schutzmaßnahmen (serverseitige Mandantenzugriffsprüfung, Argon2id, AES-GCM für Felder und lokale Dateien, sichere Download-Header, CSP, Audit-Log). Vor einem produktiven Einsatz mit Gesundheits- und Finanzdaten sollten aber mindestens die Punkte P0 und P1 behoben werden. | Priorität | Befund | Risiko | |---|---|---| | P0 | Stark veraltete Runtime-Dependencies mit kritischen Advisories | Remote-Angriffe auf Framework/Auth/Parser | | P0 | MFA wird nur im App-Layout, nicht an Server Actions/API-Routen erzwungen | Fachzugriff mit Passwort-only-Sitzung | | P0 | Öffentliche Erstinstallation ist übernehmbar und race-anfällig | Unbefugter Administratorzugang | | P1 | S3-Dokumente sind nicht anwendungsseitig verschlüsselt | Klartextzugriff durch Storage-Anbieter/Storage-Kompromittierung | | P1 | Login-Endpunkt schreibt auch während Sperre unbegrenzt DB-Zeilen | Unauthentifizierter Storage-/DB-DoS | | P1 | Unbegrenzte Dekompression/Verarbeitung fremder PDFs und Bilder | Authentifizierter Memory-/CPU-DoS | | P1 | Passkeys lassen sich ohne frische Bestätigung hinzufügen/entfernen | Persistenz nach Sitzungsdiebstahl | | P2 | TOTP-Codes können im gültigen Zeitfenster wiederverwendet werden | Replay eines abgefangenen Codes | | P2 | Proxy-Header werden ungeprüft als Client-IP vertraut | Rate-Limit-Spraying bei Fehlkonfiguration | | P3 | Sicherheitslogik und Kommentare sind teilweise dupliziert/irreführend | Erhöhte Gefahr künftiger Schutzlücken | --- ## [P0] Verwundbare Runtime-Dependencies unverzüglich aktualisieren ### Fundstellen - `package.json`: `next@15.1.6` - `package.json`: `next-auth@5.0.0-beta.25` / transitiv `@auth/core@0.37.2` - `package.json`: `nodemailer@6.10.0` - `package.json`: `@aws-sdk/client-s3@3.735.0` und `@aws-sdk/s3-request-presigner@3.735.0` - Außerdem verwundbare Build-/Test-Pakete: `vitest@3.0.5`, `@playwright/test@1.50.1`, `tsx@4.19.2`, `postcss@8.5.1`, `eslint@9.19.0` ### Evidenz `npm audit` meldet **32 Vulnerabilities: 5 critical, 5 high, 20 moderate, 2 low**. Besonders relevant: - Next.js: mehrere DoS-, SSRF-, Cache-Poisoning-, Middleware-Bypass- und Server-Action-Probleme; Audit-Fix: `next@15.5.23`. - Auth.js/NextAuth: unter anderem ein kritischer Fail-open bei fehlerhaften Auth-Konfigurationen; Audit-Fix: mindestens `next-auth@5.0.0-beta.32`. - Nodemailer: mehrere Header-/SMTP-Injection-, SSRF/File-read- und DoS-Advisories; Audit schlägt `9.0.5` vor (Major-Upgrade). - `fast-xml-parser` transitiv über AWS SDK: kritische Entity-/Regex-Probleme; AWS SDK aktualisieren. - Vitest < 3.2.6: kritischer File-read/RCE-Befund im UI-Server (primär Entwicklungsumgebung). Nicht jedes Advisory ist mit der aktuellen Konfiguration unmittelbar ausnutzbar (zum Beispiel wird kein NextAuth-E-Mail-Provider verwendet). Bei der Sensitivität der Daten ist die verwundbare Basis trotzdem nicht vertretbar. ### Behebung 1. Next.js innerhalb der 15er-Linie mindestens auf die von `npm audit` genannte gepatchte Version aktualisieren; danach aktuellen Security-Stand erneut prüfen. 2. NextAuth/Auth.js mindestens auf die gepatchte Beta aktualisieren und Auth-/Passkey-Flows vollständig regressionsprüfen. 3. Nodemailer auf eine aktuell gepatchte Version migrieren; Transportoptionen und Typen prüfen. 4. AWS SDK, Vitest, Playwright, tsx, PostCSS und ESLint aktualisieren. 5. `package-lock.json` neu erzeugen und `npm audit --omit=dev` sowie vollständiges `npm audit` in CI als Gate ausführen. ### Akzeptanzkriterien - `npm audit --omit=dev` enthält keine Critical/High-Funde. - Das vollständige Audit enthält keine Critical/High-Funde oder dokumentierte, nachweislich nicht anwendbare Ausnahmen mit Ablaufdatum. - Login mit Passwort+TOTP, Passkey-Anmeldung, Reauth, Upload, PDF/DOCX/EML und S3-Download sind regressionsgetestet. --- ## [P0] MFA-Schranke an jeder serverseitigen Fachoperation erzwingen ### Fundstellen - `src/auth.ts:144-147`: Benutzer ohne eingerichtetes TOTP erhalten bewusst bereits eine gültige Sitzung. - `src/app/(app)/layout.tsx:22-23`: MFA wird nur beim Rendern des App-Layouts erzwungen. - `src/lib/auth/sitzung.ts:50-116`: `erfordereBenutzer()` und `erfordereZugang()` prüfen nicht, ob MFA eingerichtet/abgeschlossen wurde. - Nahezu alle Fach-Server-Actions verwenden nur `erfordereBenutzer()` oder `erfordereZugang()`. - Beispiel: `src/server/actions/betreute.ts:111-157` erlaubt mit einer Passwort-only-Sitzung das Anlegen einer Betreuung. - Auch direkte Routen wie `src/app/(app)/schreiben/docx/route.ts`, `eml/route.ts` und mehrere Downloads verlassen sich nur auf diese Helfer. ### Auswirkung Ein neu angelegter Benutzer kann sich nur mit E-Mail und Passwort anmelden und erhält vor Abschluss der MFA-Einrichtung bereits ein gültiges JWT. Die UI leitet ihn zwar zur TOTP-Einrichtung um, Server Actions und Route Handler sind aber direkt aufrufbar. Damit ist „MFA Pflicht vor Fachdaten“ keine serverseitige Sicherheitsgrenze. Bei einem neuen Admin ist das besonders kritisch: Mit der Passwort-only-Sitzung können direkt Fachaktionen angestoßen werden, ohne die TOTP-Einrichtung abzuschließen. ### Behebung - Einen zentralen Guard einführen, der neben Benutzerstatus/Sitzungsgeneration auch einen serverseitig vertrauenswürdigen MFA-Zustand prüft. - `erfordereZugang()` muss diesen Guard standardmäßig einschließen. - Nur eine sehr kleine Allowlist darf ohne abgeschlossene MFA funktionieren: TOTP vorbereiten/bestätigen, Logout und unbedingt benötigte Konto-/Setup-Schritte. - Nicht allein `session.user.zweiterFaktorAktiv` vertrauen: `jwt({ trigger: 'update' })` übernimmt dieses Feld aktuell aus clientgesteuerten Session-Updates (`src/auth.ts:331-343`). Maßgeblich muss der frische DB-Zustand sein oder ein serverseitig ausgestelltes Auth-Level. - Route Handler müssen mit 401/403 antworten statt durch Layout-Redirects geschützt zu werden. ### Akzeptanzkriterien - Integrationstest: Passwort-login eines Users ohne aktiviertes TOTP; direkte Aufrufe repräsentativer Lese-, Schreib-, Export- und Download-Endpunkte werden abgewiesen. - Nur TOTP-Setup und Logout bleiben erreichbar. - Nach erfolgreicher TOTP-Bestätigung funktionieren Fachoperationen. - Ein manipuliertes `session.update({ zweiterFaktorAktiv: true })` schaltet keine Fachoperation frei. --- ## [P0] Erstinstallation gegen Übernahme und Race Conditions absichern ### Fundstelle `src/server/actions/ersteinrichtung.ts:44-87` ### Problem `erstenZugangAnlegen()` ist unauthentifiziert und verwendet das Muster „`user.count()`; danach `user.create()`“. Wird eine frisch gestartete Instanz vor dem Betreiber erreicht, kann jeder den ersten ADMIN anlegen. Außerdem können zwei parallele Requests beide `count() === 0` sehen und anschließend zwei Administratoren mit unterschiedlichen E-Mail-Adressen anlegen. Es gibt keine DB-Constraint, die „genau ein erster Benutzer“ atomar erzwingt. ### Behebung Eine der folgenden Varianten atomar umsetzen: - Ein zufälliges, einmaliges Setup-Token aus Secret/Datei/Environment verlangen und nach erfolgreicher Nutzung serverseitig verbrauchen; oder - ersten Admin ausschließlich per CLI/Seed auf dem Server anlegen; oder - Setup nur auf Loopback/Unix-Socket zulassen. Zusätzlich die Initialisierung in einer serialisierbaren DB-Transaktion bzw. über eine eindeutige Setup-State-Zeile absichern. Ein bloßer zweiter `count()` reicht nicht. ### Akzeptanzkriterien - Ohne Setup-Berechtigung kann kein erster Benutzer angelegt werden. - Zwei parallele gültige Setup-Requests erzeugen exakt einen Admin. - Nach Abschluss ist das Setup-Secret unwiderruflich verbraucht. - Der Vorgang wird auditierbar protokolliert, ohne das Setup-Secret zu loggen. --- ## [P1] Dokumente auch im S3-Treiber anwendungsseitig verschlüsseln ### Fundstellen - `src/lib/speicher/datei.ts:63-100`: lokaler Dateitreiber verschlüsselt Inhalte korrekt mit AES-256-GCM. - `src/lib/speicher/s3.ts:45-61`: S3-Treiber lädt den Klartext hoch und setzt nur `ServerSideEncryption: 'AES256'`. - `docker-compose.yml`: Produktion erzwingt `SPEICHER_TREIBER=s3`. ### Auswirkung Bei SSE-S3 verwaltet der Storage-Anbieter den Schlüssel und sieht technisch den Klartext. Ein kompromittierter S3-Account, Anbieterzugriff oder falsch konfigurierter Bucket umgeht damit die in den Kommentaren formulierte Trennung „Daten und Anwendungsschlüssel werden zusammen benötigt“. Gerade Arztbriefe, Bescheide und Kontoauszüge sind besonders schützenswert. ### Behebung - Das vorhandene binäre AES-GCM-Umschlagformat treiberunabhängig vor `PutObject` und nach `GetObject` anwenden. - Objektmetadaten dürfen keine sensitiven Klartexte enthalten. - Schlüsselrotation für Dateiobjekte implementieren und dokumentieren. - Bestehende Klartextobjekte migrieren; Migration wiederanlaufbar und prüfbar gestalten. - SSE-S3 zusätzlich beibehalten (Defense in Depth), aber nicht als Ersatz für Client-side/Application-level Encryption behandeln. ### Akzeptanzkriterien - Direkt aus dem Bucket geladenes Objekt enthält weder Originaldatei noch erkennbaren Klartext. - Manipulierte Ciphertexte werden durch GCM-Authentifizierung abgewiesen. - Alte und neue Schlüsselversionen können gelesen, neue Objekte nur mit aktiver Version geschrieben werden. --- ## [P1] Unbegrenztes Schreiben in `login_versuch` verhindern ### Fundstellen - `src/auth.ts:110-114`: Auch bei bereits festgestellter Sperre wird erneut `versuchFesthalten()` aufgerufen. - `src/lib/auth/ratelimit.ts:56-73`: Jeder Versuch erzeugt eine neue DB-Zeile. - Aufräumen erfolgt erst nach 30 Tagen. ### Auswirkung Ein nicht authentifizierter Angreifer kann den Login-Endpunkt kontinuierlich aufrufen und pro Request dauerhaft eine Zeile erzeugen. Bereits gesperrte IP-/E-Mail-Kombinationen werden nicht früh verworfen, sondern schreiben weiter. Das ermöglicht DB-Wachstum, I/O-Last und letztlich Storage Exhaustion. Gleichzeitig verschiebt jeder neue Eintrag den jüngsten Fehlversuch und kann Sperren unbegrenzt verlängern. ### Behebung - Bei aktiver Sperre nicht pro Request eine neue Detailzeile schreiben; stattdessen aggregierten Zähler/`lastSeen` mit begrenzter Kardinalität verwenden oder Schreibfrequenz drosseln. - Zusätzlich Edge-/Reverse-Proxy-Rate-Limit pro vertrauenswürdiger Client-IP und globales Notfalllimit einführen. - Harte Retention/Partitionierung und maximale Tabellen-/Kardinalitätsstrategie definieren. - Sperrzeit an einen festen Start/`lockedUntil` binden, nicht mit jedem gesperrten Request verlängern, sofern eine permanente Verlängerung nicht ausdrücklich gewollt ist. ### Akzeptanzkriterien - 10.000 Requests gegen einen bereits gesperrten Schlüssel erzeugen keine 10.000 DB-Zeilen. - Speicher- und CPU-Verbrauch bleiben begrenzt. - Normale Anmeldung wird nicht durch beliebige Spoof-Header am Proxy-Limit vorbeigeführt. --- ## [P1] Ressourcenlimits/Sandbox für PDF- und Bildverarbeitung ergänzen ### Fundstellen - `src/lib/ocr/pdf-text.ts:106-160`: Angreifergesteuerte PDF-Streams werden synchron mit `inflateSync`, `unzipSync` und `inflateRawSync` ohne `maxOutputLength` dekomprimiert. - `src/lib/ocr/index.ts:146-151`: `pdftoppm` hat ein Zeitlimit, aber kein OS-/Container-Memory-/CPU-Limit. - `src/lib/ocr/index.ts:110-150`: Tesseract/Poppler verarbeiten fremde Binärdaten direkt. - Uploadlimit 25 MiB schützt nicht gegen hohe Kompressionsraten oder Bild-/PDF-Dekompressionsbomben. ### Auswirkung Ein berechtigter Benutzer (oder ein kompromittiertes Mitbearbeiterkonto) kann mit einer kleinen komprimierten Datei sehr hohe RAM-/CPU-Last erzeugen. Synchrone Dekompression blockiert zusätzlich den Node-Eventloop. Native Parser vergrößern die Angriffsfläche. ### Behebung - `zlib` mit `maxOutputLength` und einem niedrigen globalen Dekompressionsbudget verwenden; synchrone Verarbeitung aus dem Request-Prozess entfernen. - OCR in separaten Worker/Container mit CPU-, RAM-, PID-, Dateigrößen- und Zeitlimits ausführen; idealerweise ohne Netzwerk, read-only Root-FS und mit seccomp/AppArmor. - Pixelzahl, Seitenzahl und entpackte Größe vor weiterer Verarbeitung begrenzen. - Warteschlange und Concurrency-Limit einführen. - Fehler-/Timeoutpfade testen; Dokument darf gespeichert werden, OCR muss kontrolliert abbrechen. ### Akzeptanzkriterien - Kompressionsbomben, extrem große Seitenflächen und absichtlich langsame PDFs überschreiten definierte RAM-/CPU-Grenzen nicht. - Der Webprozess bleibt während des Tests erreichbar. --- ## [P1] Passkey-Verwaltung nur nach frischer Reauth erlauben ### Fundstellen - `src/server/actions/passkey.ts:77-126`: Passkey hinzufügen verlangt nur `erfordereBenutzer()`. - `src/server/actions/passkey.ts:133-167`: Passkey löschen verlangt ebenfalls nur `erfordereBenutzer()`. - Für andere sensible Aktionen existiert bereits `erfordereFrischeAnmeldung()`. ### Auswirkung Wer eine aktive Sitzung übernimmt (unbeaufsichtigtes Gerät, Cookie-Diebstahl), kann einen eigenen Passkey registrieren und erhält damit dauerhaften Zugang, selbst nachdem die Sitzung abläuft. Außerdem kann er legitime Passkeys entfernen. ### Behebung - Start und Abschluss der Registrierung sowie Entfernen eines Passkeys an eine frische Reauth binden. - Die Reauth muss spätestens beim Abschluss erneut geprüft werden, da zwischen Challenge und Antwort Zeit vergeht. - Optional Benachrichtigung/Audit-Warnung an den Kontoinhaber. ### Akzeptanzkriterien - Eine alte, aber gültige Sitzung kann weder Passkey hinzufügen noch entfernen. - Eine fremde oder nach Reauth-Ablauf beantwortete Challenge wird abgewiesen. --- ## [P2] TOTP-Replay verhindern ### Fundstellen - `src/lib/crypto/totp.ts:69-84`: Kommentar verspricht, dass der Aufrufer das getroffene Zeitfenster speichert. - `src/auth.ts:151-153` und `src/server/actions/bestaetigen.ts:59-63`: Es wird nur `gueltig` ausgewertet; `delta`/Zeitschritt wird nirgends gespeichert. - Im User-Modell existiert kein Feld für den zuletzt akzeptierten TOTP-Zeitschritt. ### Auswirkung Derselbe abgefangene Code kann innerhalb des akzeptierten Fensters (aktuelle Periode plus Toleranz) mehrfach verwendet werden, beispielsweise einmal zur Anmeldung und erneut für eine sensible Reauth. TOTP ist damit faktisch nicht „one time“. ### Behebung - Absoluten TOTP-Zeitschritt atomar pro Benutzer speichern und nur Werte größer als der zuletzt akzeptierte Schritt zulassen. - Prüfung und Update in einer atomaren DB-Operation/Transaktion durchführen, damit parallele Replays nicht beide gewinnen. - Login und Reauth müssen dieselbe Replay-Sperre verwenden. ### Akzeptanzkriterien - Derselbe Code wird beim zweiten Einsatz abgewiesen, auch bei parallelen Requests. - Ein neuer Code im nächsten gültigen Schritt funktioniert. --- ## [P2] Vertrauensgrenze für Proxy-/Client-IP-Header definieren ### Fundstelle `src/auth.ts:65-68` ### Problem Die Anwendung verwendet ungeprüft den ersten Wert von `X-Forwarded-For`, alternativ `X-Real-IP`. Ist die App entgegen der Empfehlung direkt erreichbar oder überschreibt der Reverse Proxy eingehende Header nicht zuverlässig, kann der Client seine IP frei wählen und IP-basierte Limits umgehen. ### Behebung - Eingehende Forwarded-Header am Reverse Proxy immer entfernen/neu setzen. - In der Anwendung nur eine dokumentierte Anzahl vertrauenswürdiger Proxy-Hops akzeptieren oder ausschließlich einen vom eigenen Proxy gesetzten Header auswerten. - Direktzugriff auf die App per Firewall/Loopback ausschließen. - `trustHost: true` nicht hart kodieren, sondern nur in kontrollierter Proxy-Konfiguration aktivieren. ### Akzeptanzkriterien - Ein Client kann durch selbst gesetzte `X-Forwarded-For`-Werte seine Rate-Limit-Identität nicht ändern. - Proxy- und Direktzugriffsfall sind automatisiert getestet/dokumentiert. --- ## [P3] Sicherheitslogik zentralisieren und irreführende Aussagen korrigieren ### Beispiele - `src/middleware.ts:25` behauptet „Ohne Authentifizierungs-Cookie gar nicht erst weiterreichen“, die Middleware prüft aber überhaupt kein Cookie. - Rollen-/Zugangsauflösung wird in `erfordereZugang()`, Dokumentdownload, Kalenderroute, Export und App-Layout mehrfach manuell nachgebaut. - `src/lib/speicher/datei.ts:45` behauptet, die kanonische Pfadprüfung fange Symlink-Tricks ab. Eine reine `resolve()`-Präfixprüfung verhindert keine bereits im Ablagebaum platzierten Symlinks. - MFA- und Verschlüsselungskommentare versprechen teilweise stärkere Garantien, als die Implementierung tatsächlich liefert. ### Behebung - Gemeinsame Guards für `requireAuthenticated`, `requireMfa`, `requireAccess` und `requireRecentAuth` schaffen und überall verwenden. - Zugangsauflösung nicht kopieren, sondern zentral aufrufen. - Kommentare als überprüfbare Sicherheitsinvarianten formulieren und durch Tests belegen. - Für lokale Dateien symlink-sichere Zugriffe verwenden bzw. Ablagebaum exklusiv besitzen, Rechte härten und `lstat`/`realpath`/`O_NOFOLLOW`-Strategie dokumentieren. ### Akzeptanzkriterien - Keine Route dupliziert die Rollenauflösung. - Sicherheitskommentare entsprechen nachweislich dem Verhalten. - Tests decken die zentralen Guards und negative Zugriffsfälle ab. --- ## Durchgeführte Prüfungen - Commit: `ece995b` - `npm ci --ignore-scripts` mit lokalem Cache: erfolgreich (Peer-Warnungen zwischen NextAuth und SimpleWebAuthn) - Prisma Client Generation: erfolgreich - Production Build (`next build`): **erfolgreich** - TypeScript (`tsc --noEmit`): **erfolgreich** - ESLint (`npm run lint`): **erfolgreich, 0 Warnungen/Fehler** - Unit-Tests (`vitest run`): **28 Testdateien, 658 Tests, alle erfolgreich** - `npm audit`: **32 Funde (5 critical, 5 high, 20 moderate, 2 low)** - Statischer Review: Auth/AuthZ, Server Actions, Route Handler, Kryptografie, Upload/OCR, Datei/S3-Speicher, CSP/Headers, Docker/Deployment und Setup ## Nicht vollständig dynamisch geprüft - Die Playwright-E2E-Suite benötigt laut Repository eine laufende PostgreSQL-/MinIO-Entwicklungsumgebung, Seed-Daten, `.env`, Browser-Binaries und einen separat gestarteten Dev-Server. Diese Infrastruktur wurde in diesem read-only Audit nicht aufgebaut; die vorhandenen E2E-Tests wurden daher nicht als bestanden gewertet. - Kein aktiver Penetrationstest gegen eine laufende produktionsnahe Instanz. - Keine Prüfung der realen Reverse-Proxy-, Firewall-, S3-Bucket-, Backup- und TLS-Konfiguration. ## Empfohlene Reihenfolge 1. Runtime-Dependencies patchen. 2. Zentralen MFA-Guard einführen und alle Daten-Endpunkte absichern. 3. Erstsetup atomar und geheimnisgebunden machen. 4. S3-Anwendungsverschlüsselung und Migration umsetzen. 5. Login- und Datei-Verarbeitungs-DoS begrenzen. 6. Passkey-Reauth, TOTP-Replay-Sperre und Proxy-Vertrauensgrenze schließen. 7. Danach vollständige E2E- und negative Security-Tests in einer produktionsnahen Umgebung ausführen.
florian.wilde added the in-arbeit label 2026-08-23 23:06:24 +00:00
florian.wilde started working 2026-08-23 23:06:45 +00:00
Author
Owner

🤖 Claude Code hat mit der Umsetzung begonnen.

  • Branch: fix/38-sicherheitsaudit
  • Verstanden als: Alle Befunde von P0 bis P3 beheben, je Sicherheitsgrenze einen Regressionstest — plus ein eigener Durchgang gegen die OWASP Top 10, weil ein fremdes Audit nie vollständig ist.

Vorab nachgeprüft. Ich habe die Befunde am Code gegengelesen, bevor ich angefangen habe. Ergebnis: P0-1, P0-2, P0-3, P1-1, P1-2, P1-3, P1-4, P2-1 und P2-2 sind zutreffend. Belege:

  • npm audit --omit=dev → 27 Funde, davon 4 kritisch, 5 hoch in der Laufzeit (next, next-auth/@auth/core, fast-xml-parser, nodemailer, sharp).
  • erfordereZweitenFaktor() wird an genau einer Stelle aufgerufen: src/app/(app)/layout.tsx:21. Kein Server Action, kein Route Handler prüft MFA.
  • ersteinrichtung.ts: count() dann create(), ohne Transaktion, ohne DB-Zwang.
  • s3.ts lädt Klartext hoch, datei.ts verschlüsselt mit AES-256-GCM — die Treiber sind ungleich.

Eine Korrektur am Audit: Der Punkt P3 zur Middleware trifft nicht. Der Kommentar „Ohne Authentifizierungs-Cookie gar nicht erst weiterreichen" steht in Zeile 33 (nicht 25) und gehört zur Konstante OEFFENTLICHE_PFADE, die in Zeile 82 für Cache-Control benutzt wird. Der Kommentar ist irreführend platziert und wird korrigiert — die Middleware ist aber nicht als Zugriffsschutz gedacht und behauptet das an keiner anderen Stelle.

Geplante Reihenfolge (abweichend vom Audit, mit Begründung):

  1. P0-1 Abhängigkeiten zuerst — das ist die einzige Angriffsfläche, die von außen ohne Zugangsdaten erreichbar ist.
  2. P0-2 MFA-Guard — billig umzusetzen und macht die Zusage wahr, bevor ein zweiter Zugang existiert.
  3. P1-2 Login-Schreiblast, P1-4 Passkey-Reauth, P2-1 TOTP-Replay, P2-2 Proxy-Grenze — alle klein und in der Auth-Ecke.
  4. P0-3 Ersteinrichtung und P1-1 S3-Verschlüsselung — beide real, greifen bei der laufenden Instanz aber derzeit nicht (es gibt Benutzer, count() > 0; und wem der Objektspeicher gehört, entscheidet über das Risiko).
  5. P1-3 OCR-Grenzen, P3 Aufräumen.
  6. Eigener OWASP-Durchgang, Befunde daraus hinterher.

Zwischenstände kommen als Kommentare. Die Änderung an den Abhängigkeiten wird mit der vollen Testsuite geprüft, bevor irgendetwas anderes angefasst wird.

Zeiterfassung läuft.

🤖 **Claude Code hat mit der Umsetzung begonnen.** - Branch: `fix/38-sicherheitsaudit` - Verstanden als: Alle Befunde von P0 bis P3 beheben, je Sicherheitsgrenze einen Regressionstest — plus ein eigener Durchgang gegen die OWASP Top 10, weil ein fremdes Audit nie vollständig ist. **Vorab nachgeprüft.** Ich habe die Befunde am Code gegengelesen, bevor ich angefangen habe. Ergebnis: **P0-1, P0-2, P0-3, P1-1, P1-2, P1-3, P1-4, P2-1 und P2-2 sind zutreffend.** Belege: - `npm audit --omit=dev` → 27 Funde, davon **4 kritisch, 5 hoch** in der Laufzeit (`next`, `next-auth`/`@auth/core`, `fast-xml-parser`, `nodemailer`, `sharp`). - `erfordereZweitenFaktor()` wird an **genau einer** Stelle aufgerufen: `src/app/(app)/layout.tsx:21`. Kein Server Action, kein Route Handler prüft MFA. - `ersteinrichtung.ts`: `count()` dann `create()`, ohne Transaktion, ohne DB-Zwang. - `s3.ts` lädt Klartext hoch, `datei.ts` verschlüsselt mit AES-256-GCM — die Treiber sind ungleich. **Eine Korrektur am Audit:** Der Punkt P3 zur Middleware trifft nicht. Der Kommentar „Ohne Authentifizierungs-Cookie gar nicht erst weiterreichen" steht in Zeile 33 (nicht 25) und gehört zur Konstante `OEFFENTLICHE_PFADE`, die in Zeile 82 für `Cache-Control` benutzt wird. Der Kommentar ist irreführend platziert und wird korrigiert — die Middleware ist aber nicht als Zugriffsschutz gedacht und behauptet das an keiner anderen Stelle. **Geplante Reihenfolge** (abweichend vom Audit, mit Begründung): 1. **P0-1 Abhängigkeiten** zuerst — das ist die einzige Angriffsfläche, die von außen ohne Zugangsdaten erreichbar ist. 2. **P0-2 MFA-Guard** — billig umzusetzen und macht die Zusage wahr, bevor ein zweiter Zugang existiert. 3. **P1-2 Login-Schreiblast**, **P1-4 Passkey-Reauth**, **P2-1 TOTP-Replay**, **P2-2 Proxy-Grenze** — alle klein und in der Auth-Ecke. 4. **P0-3 Ersteinrichtung** und **P1-1 S3-Verschlüsselung** — beide real, greifen bei der laufenden Instanz aber derzeit nicht (es gibt Benutzer, `count() > 0`; und wem der Objektspeicher gehört, entscheidet über das Risiko). 5. **P1-3 OCR-Grenzen**, **P3 Aufräumen**. 6. Eigener OWASP-Durchgang, Befunde daraus hinterher. Zwischenstände kommen als Kommentare. Die Änderung an den Abhängigkeiten wird mit der vollen Testsuite geprüft, bevor irgendetwas anderes angefasst wird. Zeiterfassung läuft.
florian.wilde worked for 8 minutes 2026-08-23 23:14:48 +00:00
florian.wilde added spent time 1 hour 56 minutes 2026-08-24 09:08:28 +00:00
florian.wilde added blockiert and removed in-arbeit labels 2026-08-24 09:08:34 +00:00
Author
Owner

⛔ Umsetzung fertig — aber der Push scheitert an der Anmeldung.

Alle Punkte sind umgesetzt, geprüft und lokal committet (10 Commits auf
fix/38-sicherheitsaudit, v1.11.0). Was fehlt, ist der Push:

remote: Failed to authenticate user
fatal: Authentication failed for 'https://git.wilde-server.de/florian.wilde/betreuungs-cockpit.git/'

Der MCP-Zugang funktioniert weiterhin (get_me antwortet), nur das
gespeicherte Git-Zugangsdatum im Windows-Credential-Manager wird
abgewiesen. Früher in dieser Sitzung ging der Push noch — vermutlich ist
das Token abgelaufen. $env:GITEA_TOKEN ist in der Shell nicht gesetzt,
ich kann also nicht ausweichen.

Was ich brauche: ein gültiges Git-Zugangsdatum. Danach lege ich PR an,
setze das Label auf zu-testen und melde mich.

Bis dahin steht die Uhr. Nichts geht verloren — die Arbeit liegt lokal.


Erledigt und lokal committet

Punkt Kurz
P0-1 Abhängigkeiten: 32 Schwachstellen (5 kritisch) → 0, ohne Sprung auf Next 16
P0-2 MFA-Pflicht galt nur für Seiten; sitzt jetzt in erfordereBenutzer(). Dabei ein zweites Loch gefunden: /api/dokumente/datei fragte nur nach der Anmeldung
P0-3 Ersteinrichtung: Geheimnis aus der .env + Sperrzeile statt count()-dann-create()
P1-1 Objektspeicher lag mit dem Schlüssel des Anbieters da; jetzt eigener Umschlag, an den Ablageort gebunden. Migrationslauf für Bestand
P1-2 Anmeldesperre verlängerte sich selbst → dauerhafte Aussperrung fremder Zugänge ohne Passwortkenntnis
P1-3 Dekompressionsbombe im PDF-Textleser, unbegrenzte Bildgröße beim Rendern
P1-4 Passkeys ließen sich ohne erneute Bestätigung anlegen und entfernen
P2-1 TOTP-Codes galten bis zu 90 s mehrfach
P2-2 X-Forwarded-For aus dem falschen Ende gelesen und ohne Proxy geglaubt; trustHost fest auf true
P3 Zwei Kommentare versprachen Schutz, den es nicht gab (Middleware, Symlinks)

Eigener OWASP-Durchgang (Top 10, 2021): keine weiteren Funde.
Festgehalten in docs/SICHERHEIT.md, samt Abschnitt über das, was
ausdrücklich nicht geschützt ist. Zwei Verdachtsfälle haben sich
aufgelöst: Prisma.join() fügt nur parametrisierte Fragmente zusammen,
und der Suchausschnitt aus ts_headline wird als React-Kinder gerendert,
nicht als HTML.

Geprüft: tsc sauber · Lint sauber · 709 Unit-Tests · 95 E2E
(6 übersprungen, viewport-bedingt) · npm audit 0 · Trockenlauf des
Migrationsskripts über 120 Dateien · Rückwärtsverträglichkeit an einer
echten Altdatei.

Beim Update nötig — zwei neue .env-Zeilen:

ERSTEINRICHTUNG_GEHEIMNIS=<zufällig, npm run schluessel>
VERTRAUE_PROXY=true

Ohne die zweite zählt die Anmeldesperre nur noch nach E-Mail-Adresse.
Steht in .env.example, INSTALLATION.md und im Changelog.

⛔ **Umsetzung fertig — aber der Push scheitert an der Anmeldung.** Alle Punkte sind umgesetzt, geprüft und lokal committet (10 Commits auf `fix/38-sicherheitsaudit`, v1.11.0). Was fehlt, ist der Push: ``` remote: Failed to authenticate user fatal: Authentication failed for 'https://git.wilde-server.de/florian.wilde/betreuungs-cockpit.git/' ``` Der MCP-Zugang funktioniert weiterhin (`get_me` antwortet), nur das gespeicherte Git-Zugangsdatum im Windows-Credential-Manager wird abgewiesen. Früher in dieser Sitzung ging der Push noch — vermutlich ist das Token abgelaufen. `$env:GITEA_TOKEN` ist in der Shell nicht gesetzt, ich kann also nicht ausweichen. **Was ich brauche:** ein gültiges Git-Zugangsdatum. Danach lege ich PR an, setze das Label auf `zu-testen` und melde mich. Bis dahin steht die Uhr. Nichts geht verloren — die Arbeit liegt lokal. --- **Erledigt und lokal committet** | Punkt | Kurz | |---|---| | P0-1 | Abhängigkeiten: 32 Schwachstellen (5 kritisch) → 0, ohne Sprung auf Next 16 | | P0-2 | MFA-Pflicht galt nur für Seiten; sitzt jetzt in `erfordereBenutzer()`. Dabei ein zweites Loch gefunden: `/api/dokumente/datei` fragte nur nach der Anmeldung | | P0-3 | Ersteinrichtung: Geheimnis aus der `.env` + Sperrzeile statt `count()`-dann-`create()` | | P1-1 | Objektspeicher lag mit dem Schlüssel des Anbieters da; jetzt eigener Umschlag, an den Ablageort gebunden. Migrationslauf für Bestand | | P1-2 | Anmeldesperre verlängerte sich selbst → dauerhafte Aussperrung fremder Zugänge ohne Passwortkenntnis | | P1-3 | Dekompressionsbombe im PDF-Textleser, unbegrenzte Bildgröße beim Rendern | | P1-4 | Passkeys ließen sich ohne erneute Bestätigung anlegen und entfernen | | P2-1 | TOTP-Codes galten bis zu 90 s mehrfach | | P2-2 | `X-Forwarded-For` aus dem falschen Ende gelesen und ohne Proxy geglaubt; `trustHost` fest auf `true` | | P3 | Zwei Kommentare versprachen Schutz, den es nicht gab (Middleware, Symlinks) | **Eigener OWASP-Durchgang (Top 10, 2021):** keine weiteren Funde. Festgehalten in `docs/SICHERHEIT.md`, samt Abschnitt über das, was ausdrücklich **nicht** geschützt ist. Zwei Verdachtsfälle haben sich aufgelöst: `Prisma.join()` fügt nur parametrisierte Fragmente zusammen, und der Suchausschnitt aus `ts_headline` wird als React-Kinder gerendert, nicht als HTML. **Geprüft:** `tsc` sauber · Lint sauber · **709 Unit-Tests** · **95 E2E** (6 übersprungen, viewport-bedingt) · `npm audit` 0 · Trockenlauf des Migrationsskripts über 120 Dateien · Rückwärtsverträglichkeit an einer echten Altdatei. **Beim Update nötig — zwei neue `.env`-Zeilen:** ```bash ERSTEINRICHTUNG_GEHEIMNIS=<zufällig, npm run schluessel> VERTRAUE_PROXY=true ``` Ohne die zweite zählt die Anmeldesperre nur noch nach E-Mail-Adresse. Steht in `.env.example`, `INSTALLATION.md` und im Changelog.
florian.wilde added zu-testen and removed blockiert labels 2026-08-24 10:16:23 +00:00
Author
Owner

✅ Umsetzung abgeschlossen — bereit zum Testen.

Der Push ging beim zweiten Anlauf durch, der Blocker ist erledigt.

Was geändert wurde

  • Zehn Punkte der Prüfung abgearbeitet (P0-1 bis P3), dazu ein eigener
    Durchgang entlang der OWASP Top 10 ohne weitere Funde
  • Drei Kommentare versprachen Schutz, den es nicht gab — entfernt bzw.
    richtiggestellt
  • docs/SICHERHEIT.md neu: Bedrohungsmodell, OWASP-Durchgang, und ein
    Abschnitt darüber, was ausdrücklich nicht geschützt ist
  • Version 1.11.0

So testest du es

  1. git switch fix/38-sicherheitsaudit
  2. npm install && npm run db:deploy && npm run dev
  3. Anmelden — muss normal gehen
  4. Denselben TOTP-Code sofort noch einmal eingeben → wird abgewiesen
  5. Unter Mein Konto einen Passkey anlegen wollen → fragt vorher nach
    Passwort und Code
  6. Ein Dokument öffnen → lädt über die Anwendung, nicht direkt vom Speicher

Vor dem Ausrollen zwei neue .env-Zeilen, siehe PR-Text und Changelog.

Aufgewendete Zeit: 1:57 (nachgetragen, weil die Uhr an einem anderen
Repo lief)
Pull Request: #39

Nach Freigabe: /issue-freigabe 38

✅ **Umsetzung abgeschlossen — bereit zum Testen.** <!-- claude-status branch: fix/38-sicherheitsaudit pr: 39 zeit_sekunden: 7000 --> Der Push ging beim zweiten Anlauf durch, der Blocker ist erledigt. **Was geändert wurde** - Zehn Punkte der Prüfung abgearbeitet (P0-1 bis P3), dazu ein eigener Durchgang entlang der OWASP Top 10 ohne weitere Funde - Drei Kommentare versprachen Schutz, den es nicht gab — entfernt bzw. richtiggestellt - `docs/SICHERHEIT.md` neu: Bedrohungsmodell, OWASP-Durchgang, und ein Abschnitt darüber, was ausdrücklich **nicht** geschützt ist - Version 1.11.0 **So testest du es** 1. `git switch fix/38-sicherheitsaudit` 2. `npm install && npm run db:deploy && npm run dev` 3. Anmelden — muss normal gehen 4. Denselben TOTP-Code sofort noch einmal eingeben → wird abgewiesen 5. Unter *Mein Konto* einen Passkey anlegen wollen → fragt vorher nach Passwort und Code 6. Ein Dokument öffnen → lädt über die Anwendung, nicht direkt vom Speicher **Vor dem Ausrollen** zwei neue `.env`-Zeilen, siehe PR-Text und Changelog. **Aufgewendete Zeit:** 1:57 (nachgetragen, weil die Uhr an einem anderen Repo lief) **Pull Request:** #39 Nach Freigabe: `/issue-freigabe 38`
Author
Owner

🚀 Freigegeben und nach main übernommen.

  • Merge-Commit: 3525777
  • Pull Request: #39 (gemergt)
  • Gesamtaufwand: 2:04

Vor den Prüfungen noch einmal alles durchlaufen lassen: 709 Unit-Tests
grün, Arbeitsverzeichnis sauber, PR ohne Konflikte.

Beim Ausrollen nicht vergessen — zwei neue .env-Zeilen:

ERSTEINRICHTUNG_GEHEIMNIS=<zufällig, npm run schluessel>
VERTRAUE_PROXY=true

Und, nur mit Objektspeicher, einmalig:

npm run speicher:verschluesseln -- --anwenden

Issue wird geschlossen.

🚀 **Freigegeben und nach `main` übernommen.** <!-- claude-status merge: 3525777fb4b9681faa732d0545ccf86b074233e3 pr: 39 zeit_sekunden: 7483 --> - Merge-Commit: `3525777` - Pull Request: #39 (gemergt) - Gesamtaufwand: 2:04 Vor den Prüfungen noch einmal alles durchlaufen lassen: 709 Unit-Tests grün, Arbeitsverzeichnis sauber, PR ohne Konflikte. **Beim Ausrollen nicht vergessen** — zwei neue `.env`-Zeilen: ```bash ERSTEINRICHTUNG_GEHEIMNIS=<zufällig, npm run schluessel> VERTRAUE_PROXY=true ``` Und, nur mit Objektspeicher, einmalig: ```bash npm run speicher:verschluesseln -- --anwenden ``` Issue wird geschlossen.
florian.wilde removed the zu-testen label 2026-08-24 10:35:43 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Total Time Spent: 2 hours 4 minutes
florian.wilde
2 hours 4 minutes
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: florian.wilde/betreuungs-cockpit#38