10 KiB
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-utilsinstalliert. Das Advisory 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, mit verfügbarem Fix. Dieses Advisory setzt spezielle
IN_PLACE-/Hook-Nutzung voraus.npm ls dompurify monaco-editor --allzeigte 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.