# Security-Audit - 17.09.2026 Geprüfter Commit: `cb30eca`. Ergebnis: **ein Befund hoher Priorität (P1), fünf Befunde mittlerer Priorität (P2)** sowie Hinweise zu Abhängigkeiten. Keine Behebung vorgenommen. ## Umfang und Nachweisgrenzen Quellcodeprüfung von Payload-Zugriffsregeln, Kunden-/Admin-Authentifizierung, Buchungen, Meetings, Analytics, Kalender-Feed, E-Mail-Versand und Deployment-Konfiguration. Die installierten Next.js-Guides zu Datensicherheit und Route Handlers sowie Payloads tatsächliche Local-API-Implementierung wurden herangezogen. `npm test`: 5/5 Tests bestanden. Zusätzlich wurden die unveränderten Route Handler für Stornierung, Meeting-Beitritt, Token-Austausch und Buchungsanfragen isoliert ausgeführt, mit simulierter Datenbank und simuliertem LiveKit. Keine Produktivzugriffe, echten E-Mails oder Datenänderungen. Diese Prüfungen belegen die Handler-Logik, nicht das Verhalten eines realen Reverse-Proxys oder Video-Servers. Produktionskonfiguration, Git-Historie und laufende Infrastruktur waren nicht Gegenstand eines Penetrationstests. ## 1. P1 - Interne Buchungsnotizen gelangen in den Kundenbrowser **Stellen:** `lib/booking/queries.ts:12`, `app/(frontend)/konto/termine/page.tsx:27`, `app/(frontend)/konto/page.tsx:46`, `collections/BookingRequests.ts:364`. `getCustomerBookings()` liest vollständige Buchungsdokumente mit `depth: 2` über die Payload Local API. Ohne explizites `overrideAccess: false` umgeht diese sowohl Collection- als auch Feldzugriffsregeln. Das wurde in `node_modules/payload/dist/collections/operations/local/find.js` und `node_modules/payload/dist/fields/hooks/afterRead/promise.js` verifiziert. Die Kontoseiten übergeben das gesamte Ergebnis als `booking` an die Client Component `BookingCard`. Dadurch wird auch `internalNote` an den Browser serialisiert, obwohl es durch `isAdminFieldLevel` geschützt und als „Nur für Anna sichtbar“ bezeichnet ist. Dass die Komponente die Notiz nicht sichtbar rendert, schützt sie nicht. **Auswirkung:** Ein angemeldeter Kunde kann interne Notizen zu seinen Buchungen in der Seiten-/RSC-Antwort lesen. Kein Zugriff auf fremde Buchungen wurde hier festgestellt. **Nachweis:** Statisch vollständiger Datenfluss von privilegierter Abfrage bis zur Client-Grenze; kein Live-Abruf realer Kundendaten. **Behebung:** Kundenansichten nur explizit ausgewählte Felder als separates DTO übergeben. Wenn möglich zusätzlich mit `overrideAccess: false` und authentifiziertem Benutzer lesen. Privilegiert benötigte Meeting-Daten gesondert und minimal auflösen. Auch verschachtelte Beziehungen nicht vollständig serialisieren. **Regressionstest:** Eine Buchung mit einer eindeutigen internen Testnotiz anlegen; die Notiz darf weder in HTML noch in einer RSC-Antwort des Kundenkontos vorkommen. ## 2. P2 - Stornierung widerruft den privaten Meeting-Zugang nicht **Stellen:** `app/(payload)/api/booking/[id]/cancel/route.ts:29`, `app/(payload)/api/meetings/[slug]/join/route.ts:49`, `collections/BookingRequests.ts:229`. Die Stornierung setzt nur den Buchungsstatus. Der verknüpfte Online-Event und sein Passwort bleiben gültig. Der Join-Endpunkt prüft Online-Status, Passwort und Zeitfenster, aber nicht den Status der zugehörigen privaten Buchung. Der E-Mail-Hook bei Stornierung sperrt den Event ebenfalls nicht. **Auswirkung:** Wer das zuvor erhaltene Passwort kennt, kann innerhalb des ursprünglichen Zeitfensters auch nach der Stornierung neue Meeting-Zugangsdaten erhalten. **Nachweis:** Isolierter Aufruf des Cancel-Handlers änderte die Buchung auf `cancelled`; anschließender anonymer Join mit dem alten Passwort lieferte HTTP 200 und Zugangsdaten. **Behebung:** Für private Events bei jedem Join und Connect eine weiterhin bestätigte, passend verknüpfte Buchung verlangen. Bei Stornierung bestehende Sitzungen widerrufen; gegebenenfalls aktive Teilnehmer über LiveKit entfernen. ## 3. P2 - Token-Austausch umgeht das Meeting-Ende **Stelle:** `app/(payload)/api/meetings/[slug]/connect/route.ts:27`. `/connect` prüft das App-Token und die Kick-Liste, anschließend nur noch `isOnline`. Anders als `/join` prüft der Handler weder das aktuelle Meeting-Zeitfenster noch einen Widerruf durch Passwortwechsel. App-Tokens sind standardmäßig sechs Stunden gültig; neu ausgestellte LiveKit-Tokens gelten jeweils zwei Stunden. **Auswirkung:** Ein zuvor zugelassener Teilnehmer erhält nach Ende des erlaubten Beitrittsfensters weiterhin neue Video-Zugangsdaten, solange sein App-Token gültig ist. Ein Passwortwechsel allein invalidiert bereits ausgestellte App-Tokens ebenfalls nicht. **Nachweis:** Mit gültigem App-Token und einem simulierten, bereits am Vortag beendeten Event lieferte der unveränderte Connect-Handler HTTP 200 mit einem neuen Video-Token. **Behebung:** Dieselbe serverseitige Zugangsprüfung für Join und Connect verwenden. Token-Laufzeiten am erlaubten Meeting-Ende begrenzen und einen Sitzungs-/Berechtigungsstand für Widerrufe prüfen. Die beabsichtigte Admin-Testzimmer-Ausnahme separat behandeln. ## 4. P2 - Krankheits- und Urlaubsgründe sind öffentlich abrufbar **Stelle:** `collections/AvailabilityOverrides.ts:17`. Die Collection erlaubt anonymes Lesen aller Dokumente. Das Feld `reason` enthält unter anderem `krankheit` und `urlaub` und besitzt keinen eigenen Leseschutz. Die generische Payload-API unter `/api/availability-overrides` macht damit Datum und Abwesenheitsgrund öffentlich; gleiches gilt grundsätzlich für GraphQL. **Auswirkung:** Persönliche Abwesenheitsgründe lassen sich ohne Anmeldung abfragen, soweit entsprechende Einträge gespeichert sind. Zur Buchung reicht die Information, welche Slots verfügbar sind. **Nachweis:** Statisch anhand von Collection-Zugriff, Felddefinition und generischem REST-Router. **Behebung:** Collection-Lesen auf Admins beschränken oder mindestens den Grund schützen. Der bestehende serverseitige Slot-Endpunkt kann weiterhin ausschließlich verfügbare Zeiten herausgeben. ## 5. P2 - Registrierung und E-Mail-Wechsel umgehen die Versandlimits **Stellen:** `collections/Customers.ts:84`, `collections/Customers.ts:46`. Die öffentliche Kundenerstellung löst unmittelbar eine Bestätigungs-E-Mail aus. Änderungen der eigenen E-Mail-Adresse lösen denselben Versand erneut aus. Für diese Operationen gibt es keine entsprechenden Limits in der Collection. Die Limits des separaten `/api/auth/resend-verification`-Handlers greifen hier nicht; `maxLoginAttempts` schützt nur Login-Versuche. **Auswirkung:** Ohne bestätigte Adresse lassen sich neue Konten für verschiedene Empfänger erzeugen beziehungsweise eigene E-Mail-Adressen wiederholt ändern und damit Bestätigungsmails auslösen. Das ermöglicht Versandmissbrauch, Datenbankwachstum und Belastung des SMTP-Kontos. Ein externes Proxy-Limit könnte die Häufigkeit mindern, ist im geprüften Repository aber nicht als Schutz für diese Vorgänge implementiert. **Nachweis:** Statisch anhand der öffentlichen Collection-Operationen und Versand-Hooks. Kein Versandtest durchgeführt. **Behebung:** Erstellung und E-Mail-Änderung vor dem Versand zentral pro IP, Konto und Zieladresse begrenzen. Regeln auf Collection-/Hook-Ebene oder einem exklusiven, geschützten Einstiegspunkt durchsetzen, sodass REST und GraphQL sie nicht umgehen können. ## 6. P2 - Ein bestätigtes Konto kann beliebig viele identische Buchungsanfragen erzeugen **Stellen:** `app/(payload)/api/booking/request/route.ts:75`, `collections/BookingRequests.ts:163`. Der Handler prüft Anmeldung, E-Mail-Bestätigung und Slot-Verfügbarkeit, aber weder eine Rate-Limit-Quote noch eine schon bestehende identische offene Anfrage. Offene Anfragen belegen den Slot nicht. Jeder erfolgreiche Create löst Bestätigungs- und Admin-Benachrichtigungen aus. **Auswirkung:** Ein einziges verifiziertes Konto kann den Admin-Eingang, die Buchungsliste und den Mailversand mit Duplikaten fluten. **Nachweis:** 25 unmittelbar aufeinanderfolgende identische Anfragen wurden vom unveränderten Handler mit HTTP 200 angenommen und an die simulierte Create-Operation weitergegeben. Keine echten E-Mails verschickt. **Behebung:** Pro Kunde begrenzen und identische offene Anfragen atomar deduplizieren. Wiederholte identische Requests sollten keinen weiteren Datensatz und keinen erneuten Mailversand erzeugen. ## Abhängigkeiten und weitere Prüfergebnisse `npm audit --json` meldete im Lockfile **7 betroffene Pakete: 6 moderate, 1 low, 0 high, 0 critical**. Die Zahl enthält transitive Abhängigkeitsketten und bedeutet nicht sieben unabhängig ausnutzbare Anwendungslücken. - **esbuild 0.18.20** ist über `@payloadcms/db-postgres → drizzle-kit → @esbuild-kit/esm-loader → @esbuild-kit/core-utils` installiert. Das [Advisory GHSA-67mh-4wv8-2f99](https://github.com/advisories/GHSA-67mh-4wv8-2f99) betrifft esbuilds Entwicklungs-Webserver. Im geprüften App-Startpfad wurde dieser Server nicht gefunden; daraus wird keine bestätigte Produktionslücke abgeleitet. npm meldet für diese Kette keinen verfügbaren automatischen Fix. - **DOMPurify/Monaco:** Das Lockfile-Audit meldete mehrere DOMPurify-Advisories, darunter [GHSA-55q2-fjhq-7xh7](https://github.com/advisories/GHSA-55q2-fjhq-7xh7), mit verfügbarem Fix. Dieses Advisory setzt spezielle `IN_PLACE`-/Hook-Nutzung voraus. `npm ls dompurify monaco-editor --all` zeigte beide Pakete im aktuellen Installationsbaum nicht an. Produktionsinstallation aus dem Lockfile und tatsächliche Nutzung müssen vor einer Aussage zur Ausnutzbarkeit abgeglichen werden; ein XSS-Exploit wurde nicht nachgewiesen. - Ein begrenzter Musterscan der aktuell versionierten Dateien fand keine privaten Schlüsselblöcke oder GitHub-/AWS-Tokenmuster. Das ist kein vollständiger Secret- oder Historien-Scan. - Positiv: Adminrechte verlangen Auth-Collection und Rolle; Buchungsaktionen prüfen den Eigentümer; Verifizierungs-/Kalender-Tokens werden gehasht gespeichert; öffentliche Event-Felder geben das Meeting-Passwort nicht über die reguläre Payload-API frei; Cron-Aufrufe prüfen ein Secret. ## Reihenfolge der Behebung Zuerst die Weitergabe interner Notizen stoppen. Danach private Meeting-Zugänge und Token-Austausch an den aktuellen Berechtigungsstatus binden, Abwesenheitsgründe schützen und die beiden Versand-/Buchungsmissbrauchspfade begrenzen. Abhängigkeitsupdates separat prüfen und mit dem tatsächlich ausgelieferten Installationsbaum abgleichen.