presseportale/dev/archive/2026-09-audit/AUDIT.md
Kevin Adametz 8e3554bf91 dev: Doku aufgeräumt, neue Roadmap als einzige Liste offener Punkte
- dev/roadmap.md neu: alle offenen Punkte aus Plänen, Audits und Protokollen,
  gegen Code und Git abgeglichen (Stand v1.7.0)
- Abgeschlossenes ins Archiv: 2026-09-01 → archive/2026-09-nutzergewinnung,
  2026-09-11-audit → archive/2026-09-audit, lagacy-rechnungen →
  archive/legacy-rechnungen, Deploy-Protokoll v1.1.0, Umami-Snippets,
  Go-Live-Roadmap vom 20.07.
- Aktiv bleiben eigene Ordner bildrechte/ (Plan Fassung 2, Decision-Update)
  und changelog/ (Decision-Update Changelog)
- dev/README.md als Ordnerkarte neu, Verweise und zwei Code-Kommentare auf
  die neuen Pfade

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:25:09 +00:00

81 KiB
Raw Permalink Blame History

Code-, Projekt- und Entwicklungsaudit

Projekt: pressekonto / presseecho / businessportal24

Auditdatum: 11.09.2026

Geprüfter Commit: 5d0570bee0d94275465b19d6ecb9950cf4d1c1b1

Arbeitsverzeichnis: /var/www/html

Auftrag: Umfangreicher Audit mit Fehleranalyse, Verbesserungsvorschlägen und priorisierten Optimierungen. Keine Änderungen am Anwendungscode.

Ergebnis: 28 priorisierte Befunde, ergänzt um Architekturbeurteilung, Prüfgrenzen, Optimierungsplan und konkrete Regressionstests.

1. Gesamtbeurteilung

Die Anwendung ist ein gewachsenes, fachlich anspruchsvolles Publishing-System. Sie verbindet öffentliche Nachrichtenportale mit Kunden- und Redaktionsverwaltung, Firmen- und Kontaktzuordnungen, mehreren Anmeldeverfahren, KI-Prüfung, Abonnements, Einmalkäufen, Credits, Legacy-Abrechnung, Newsletter und rechtlich motivierten Änderungsprozessen.

Die Grundlage ist deutlich weiter als ein unverändertes Starter-Kit: Es gibt zentralisierte Veröffentlichungs- und Abrechnungsservices, gezielte Datenbankindizes, eine ausgelagerte Volltextsuche, Transaktionen und Zeilensperren an wichtigen Geld- und Kontingentoperationen sowie einen großen Bestand an fachlichen Tests. Die jüngere Entwicklung enthält nachvollziehbare Verbesserungen an Sicherheit, Performance und Betrieb.

Trotzdem bestehen wesentliche Fehler an den Übergängen zwischen diesen Teilbereichen. Besonders relevant sind eine unerlaubte Vergabe von Firmenrechten über eine unbestätigte E-Mail-Adresse, unterschiedliche 2FA-Regeln je Anmeldeweg, übergangene Soft Deletes, fehlerhaftes Newsletter-Chunking, eine Abrechnung über ein explizites Vertragsende hinaus und die fehlende Bindung von KI-Ergebnissen an die analysierte Inhaltsversion.

Empfehlung: Die Befunde F01–F07 vor einer weiteren funktionalen Ausweitung bearbeiten. F01 ist innerhalb dieser Gruppe ein Sicherheits-Release-Blocker. Ein umfassender Neuaufbau ist nicht begründet; erforderlich sind gezielte Korrekturen an den bestehenden Sicherheits- und Zustandsgrenzen.

1.1 Wesentliche Ergebnisse auf einen Blick

Priorität Anzahl Einordnung
P1 7 Hohe Sicherheits- oder Geschäftsauswirkung; vorrangig beheben
P2 19 Relevante Funktions-, Betriebs-, Performance- oder Qualitätssicherungsfehler
P3 2 Entwicklungs- und Dokumentationsverbesserungen

Es wird kein P0 vergeben: Es wurde kein laufender Angriff oder eingetretener Produktionsschaden nachgewiesen. Die Vergabe P1 bedeutet bei F01 dennoch, dass die beschriebene Rechtevergabe vor dem nächsten regulären Release korrigiert werden sollte. Priorität bewertet die erforderliche Reaktion, nicht die Wahrscheinlichkeit eines bereits erfolgten Vorfalls.

1.2 Belastbarkeit des Ergebnisses

Der Audit kombiniert Quellcodeanalyse, Gegenprüfung des installierten Framework-Codes, Prüfung bestehender Tests, Git-Historie, Datenbankschema und ausschließlich lesende Datenbankabfragen. Der besonders relevante Authentifizierungsbefund wurde zusätzlich unabhängig anhand der vollständigen Aufrufkette gegengeprüft.

Die Anwendungstests, PHPStan und Dependency-Audits konnten nicht ausgeführt werden: Alle entsprechenden Sail-Aufrufe scheiterten mit Docker or Podman is not running. Auch vendor/bin/sail up -d scheiterte. Es wurde kein direkter PHP-/Node-Aufruf als Umgehung verwendet.

Damit sind die Fehler überwiegend statisch belegt, jedoch nicht als in dieser Sitzung dynamisch reproduzierte Angriffe oder Integrationstests zu verstehen. F22 ist zusätzlich durch tatsächliche MySQL-Query-Pläne gestützt. Die Datenbankverbindung des Boost-Servers funktionierte unabhängig von der nicht verfügbaren Sail-CLI. Ihre Betriebsrolle wurde nicht als Produktion unterstellt.

2. Prüfauftrag, Teilaufgaben und Vorgehen

Der Auftrag wurde in die folgenden kleinen Prüfpakete zerlegt und schrittweise bearbeitet. Für den Backend-Teil wurde entsprechend dem Laravel-Skill eine zusätzliche lesende Gegenprüfung eingesetzt.

Schritt Prüfpaket Bearbeitet Nachweis / Einschränkung
1 Bestandsaufnahme, Versionen, Umfang, Git-Zustand Ja Dateien, Composer-/npm-Konfiguration, Boost application-info, Git
2 Architektur, Domains, Portale, Routen und Zuständigkeiten Ja Routing, Provider, Context-Klassen, Services, Dokumentation
3 Anmeldung, Verifizierung, 2FA, Berechtigungen Ja Fortify, Socialite, Magic Links, Policies, Middleware, Login-Listener
4 Kunden-/Adminaktionen und Livewire-Zustand Ja Kritische öffentliche Actions, Locked-Properties, erneute Autorisierung
5 API-Verträge, Validierung, Eigentums- und Löschgrenzen Ja v1, Legacy-Aliasse, Requests, Resources, Scope-Verhalten
6 Veröffentlichung, KI-Prüfung, Quoten, Zeitsteuerung Ja Jobs, Veröffentlichungsservice, Zustandsübergänge und Scheduler
7 Geldflüsse und Legacy-Abrechnung Ja Wallet, Topups, Rechnungsnummern, MAN-Rechnungen, Käufe, Webhook-nahe Abläufe
8 Newsletter, Hintergrundverarbeitung, Wiederanlauf Ja Empfängerauswahl, Start, Versandjob und Fehlergrenzen
9 Datenbank, Suche, Performance und Migration Ja Migrations-/Indexprüfung, Query-Pläne, Importpfade
10 Medien, öffentliche Seiten, Cache, SEO und UI Ja, statisch Sichtbarkeitsregeln, Uploads, Rendering, Templates; kein visueller Browserlauf
11 Tests, CI, Entwicklungsworkflow und Betriebsdokumentation Ja Bestehende Testfälle gelesen, Gates und Runbook geprüft; Laufzeitchecks blockiert
12 Befunde zusammenführen, priorisieren, Maßnahmen und Regressionen Ja Dieser Bericht

2.1 Was nicht verändert oder ausgeführt wurde

  • Kein Anwendungscode, keine Tests, keine Migrationen, keine Konfiguration und keine Abhängigkeiten verändert.
  • Keine Datenbankänderungen, keine Datenbereinigung, keine Rechnungen erzeugt, keine Newsletter gestartet und keine E-Mails versendet.
  • Keine Authentifizierungs- oder Berechtigungsangriffe gegen reale Konten ausgeführt.
  • Keine Deployments, keine Commits und kein Push.
  • Keine produktiven Nutzdaten, Zugangsdaten oder Tokens in den Bericht übernommen.
  • Ausschließlich diese angeforderte Markdown-Datei neu angelegt.

2.2 Geltungsbereich und Grenzen

Die Prüfung deckt alle wesentlichen technischen und fachlichen Bereiche ab. Bei über 116.000 Zeilen in den betrachteten Anwendungs-, View-, Test- und Migrationsdateien ist sie keine Zusicherung einer lückenlosen Zeilenprüfung jeder Datei. Kritische Pfade wurden vertieft untersucht; übrige Bereiche wurden strukturell und anhand von Querschnittssuchen bzw. repräsentativen Implementierungen geprüft.

Nicht nachgewiesen wurden: vollständige Testgrünheit, reale Browser-/Mobilansichten, tatsächliche Produktionskonfiguration, Infrastruktur-Firewallregeln, Backup-Wiederherstellbarkeit, Produktions-Queueparameter, Lastgrenzen, erfolgreiche echte Stripe-/Google-/SMTP-/KI-Interaktionen oder ein aktueller vollständiger CVE-Status. Ebenso stellt der Bericht keine juristische oder steuerrechtliche Freigabe dar; rechtlich benannte Funktionen werden technisch auf Konsistenz geprüft.

3. Projektbestand und Architektur

3.1 Tatsächlicher Technologiebestand

Versionen aus Boost application-info am Auditdatum, nicht lediglich die Mindestanforderungen aus composer.json:

Baustein Installierter Stand
PHP 8.4
Laravel 12.63.0
Fortify 1.36.2
Sanctum 4.3.1
Socialite 5.27.0
Cashier 16.5.3
Livewire / Volt 4.2.4 / 1.10.5
Flux / Flux Pro jeweils 2.14.0
Pest / PHPUnit 3.8.6 / 11.5.50
Larastan / Pint 3.9.6 / 1.29.1
Sail / Boost 1.57.0 / 2.4.5
Alpine / Tailwind CSS 3.15.0 / 4.1.14
Datenbank der verfügbaren Boost-Verbindung MySQL

Ergänzend sind unter anderem Spatie Permission und HTMLPurifier eingebunden. composer.json beschreibt das Projekt weiterhin als offizielles Livewire-Starter-Kit. Das ist kosmetisch, bildet die fachliche Eigenständigkeit aber nicht mehr ab.

3.2 Umfang

Metrik Ergebnis Bedeutung
PHP-Dateien unter app/ 363 Einschließlich Concerns, Services, Commands etc.
Blade-Dateien unter resources/views/ 209 UI und erhebliche Teile der Volt-Anwendungslogik
*Test.php unter tests/ 203 Dateianzahl, keine Aussage über ausgeführte Tests
Statisch gezählte test()-/it()-Deklarationen 1.481 Datasets können die tatsächliche Zahl der Ausführungen erhöhen
Migrationsdateien 112 Hohe Schema- und Legacy-Komplexität
Zeilen in app, Views, Tests und Migrationen zusammen 116.414 Rohumfang einschließlich Kommentaren/Leerzeilen
PHPStan-Baseline 166 Einträge / 188 unterdrückte Vorkommen Keine neu gemessenen PHPStan-Fehler

Besonders große Dateien:

Datei Zeilen Konsequenz
resources/views/livewire/customer/press-releases/show.blade.php 1.997 Viele unterschiedliche Käufe, Korrekturen und Anfragen in einem UI-Lebenszyklus
app/Services/PressRelease/ProofPdfRenderer.php 1.476 Eigenständige komplexe PDF-Erzeugung; hoher Spezialwissenbedarf
resources/views/livewire/admin/press-releases/index.blade.php 1.401 Tabellen-, Filter-, Status- und Verwaltungslogik gebündelt
resources/views/livewire/admin/users/edit.blade.php 1.186 Hohe Kopplung von Rollen, Firmen, Kontakten und Profilen
resources/views/livewire/customer/bookings.blade.php 1.185 Umfangreicher Bestellzustand und UI in einer Datei

Große Dateien sind nicht automatisch fehlerhaft. Hier erschweren sie jedoch die Prüfung von Berechtigungswechseln und Zustandsübergängen. Die Zusammenführung vieler Funktionen sollte nicht mit einer Zusammenführung aller Verantwortlichkeiten verwechselt werden.

3.3 Fachliche Struktur

flowchart TD
    Public[Öffentliche Portale: presseecho / businessportal24]
    Hub[pressekonto: Anmeldung, Kundenbereich, Redaktion]
    Api[v1-API und Legacy-Service-Aliasse]
    Context[Portal- und Firmenkontext, Middleware, Policies]
    Publishing[Veröffentlichungsservice, KI-Prüfung, Medien]
    Billing[Cashier / Stripe, MAN-Rechnungen, Credits, Einzelkäufe]
    CRM[Firmen, Kontakte, Mitgliedschaften]
    Async[Queue, Newsletter, Scheduler, Importe]
    Data[MySQL, private Dateien, Cache, Suchindex]
    Public --> Context
    Hub --> Context
    Api --> Context
    Context --> Publishing
    Context --> Billing
    Context --> CRM
    Publishing --> Async
    Billing --> Async
    CRM --> Data
    Publishing --> Data
    Billing --> Data
    Async --> Data

Es bestehen zwei voneinander unabhängige Zugriffsachsen: Portal/Sprache für die Veröffentlichung und Benutzer/Firmenzuordnung für die Verwaltung. Dazu kommt der Lebenszyklus eines Datensatzes: aktiv, archiviert, mit 410 entfernt oder soft-gelöscht. Mehrere Befunde entstehen dadurch, dass eine Abfrage nur eine dieser Achsen berücksichtigt.

3.4 Bereits gut gelöst

  • Veröffentlichungen laufen zentral über PressReleaseService; Publikation prüft unter Sperren erneut Zustand, Quoten und verfügbare Käufe.
  • Walletsaldo und Ledger werden transaktional geschrieben. Topup-Gutschriften prüfen credited_at unter Row Lock.
  • Rechnungsnummern beruhen auf einer eindeutigen Sequenz und Zeilensperren.
  • Boost-Inventar verwendet stabile Sperrzeilen und eine definierte Sperrreihenfolge.
  • Die API besitzt Tokenfähigkeiten, aktive Benutzerprüfung und ein tokenbezogenes Rate Limit. Fremde Autorschaft wird in den untersuchten PM-Pfaden geprüft.
  • Magic-Login-Tokens werden gehasht gespeichert und beim Verbrauch atomar beansprucht. Der Magic-Login respektiert bereits die lokale 2FA.
  • Eigene Admin-/Customer-Middleware ist für Livewire-Folgeanfragen registriert. Viele Actions autorisieren zusätzlich explizit.
  • Presse-HTML wird über einen zentralen Sanitizer ausgegeben; strukturierte Daten verwenden JSON_HEX_TAG. Unescaped-Ausgabe ist deshalb nicht pauschal als XSS eingestuft worden.
  • Medien liegen konzeptionell auf privaten Disks und werden über Controller ausgeliefert. Das ist die richtige Grundlage, auch wenn F04 eine Lücke in der Sichtbarkeitsprüfung zeigt.
  • Zahlreiche Scheduler-Tasks besitzen withoutOverlapping() und Failure-Hooks; es existiert ein Betriebs-Runbook.
  • Die Suchindex-Auslagerung und zahlreiche zusammengesetzte Indizes zeigen eine bewusste Auseinandersetzung mit großen Beständen.
  • Tests decken viele sensible Themen ausdrücklich ab: Rechteeskalation, Verifizierung, Billing, Scheduling, Compliance, Legacy-Kompatibilität und Performanceverhalten.

4. Befundregister

Nachweisarten: S = statisch anhand konkreter Aufrufkette belegt; D = durch lesende Datenbankprüfung ergänzt; B = Auswirkung hängt von einer ausdrücklich genannten Betriebs-/Parallelitätsbedingung ab. Ein B-Befund ist kein nachgewiesener Produktionsvorfall.

ID Priorität Nachweis Befund
F01 P1 S Unbestätigte E-Mail kann dauerhafte fremde Firmenrechte auslösen
F02 P1 S Google-Login umgeht lokale Zwei-Faktor-Authentifizierung
F03 P1 S Sicherheits-UI umgeht erneute Identitätsprüfung; Recovery-Anzeige manipulierbar
F04 P1 S Soft Deletes werden in API, Vorschau, Kundenlisten und Medien übergangen
F05 P1 S/D Newsletter-Chunking dupliziert und überspringt Empfänger
F06 P1 S MAN-Abrechnung ignoriert explizites Legacy-Vertragsende
F07 P1 S/B KI-Ergebnisse sind nicht an die analysierte Inhaltsversion gebunden
F08 P2 S/B Admin-Gutschriften autorisieren nur beim Öffnen der Komponente
F09 P2 S/B Globaler Superadmin-Kurzschluss umgeht Kontodeaktivierung
F10 P2 S Automatisch vergebene Firmenrechte werden bei Wegfall der Grundlage nicht entzogen
F11 P2 S API-Pagination und Eingabeverträge unvollständig validiert
F12 P2 S/D Meldeverfahren ordnen URLs nur anhand des Slugs zu
F13 P2 S/B Firmenlöschung/-deaktivierung invalidiert öffentliche Seitencaches nicht zuverlässig
F14 P2 S/B Newsletter-Start besitzt keinen sicheren Wiederanlauf
F15 P2 S/B Newsletter-Job kann doppelt senden und Zähler inkonsistent hinterlassen
F16 P2 S Nicht abrechenbare Altverträge können den Rechnungslauf blockieren
F17 P2 S/B KI-Retrybudget und Worker-Timeout passen nicht zusammen
F18 P2 S/B Monatskontingent-Reset ist nicht periodengebunden und nicht idempotent
F19 P2 S/B Gleichzeitige Aufrufaggregatoren zählen Ereignisse mehrfach
F20 P2 S Scheduler-Publikationsfehler liefern trotzdem Exit-Code Erfolg
F21 P2 S/B Legacy-Import lädt große Gesamttabellen und hinterlässt Teilimporte
F22 P2 S/D LOWER(email) verhindert den vorhandenen E-Mail-Index
F23 P2 S/B Dateifehler können scheinbar erfolgreiche Uploads und verlorene Logos verursachen
F24 P2 S Lint-CI formatiert, erzwingt aber keine saubere Formatierung im Commit
F25 P2 S Test-/Analysekonfiguration lässt wesentliche Produktionspfade unbewiesen
F26 P2 S Eigenes Anmeldemodal besitzt keine explizite Fokusführung
F27 P3 S Standard-Entwicklungsstart startet keinen Vite-Server
F28 P3 S Einstiegspunkte der Dokumentation zeigen widersprüchliche Entwicklungsstände

5. Sicherheits- und Zugriffsprüfung

F01 — Fremde Firmenrechte über eine unbestätigte E-Mail-Adresse

Priorität: P1, innerhalb dieses Audits Sicherheits-Release-Blocker.

Belastbarkeit: Vollständige statische Aufrufkette, unabhängig gegengeprüft; nicht gegen reale Konten ausgeführt.

Fundstellen:

  • config/fortify.php:150: Profiländerung ist als Fortify-Feature aktiv.
  • app/Providers/FortifyServiceProvider.php:44: eigene Profil-Action ist gebunden.
  • vendor/laravel/fortify/routes/routes.php:100–103: PUT /user/profile-information benötigt Authentifizierung, keine verifizierte E-Mail.
  • app/Actions/Fortify/UpdateUserProfileInformation.php:48–56: E-Mail wird sofort ersetzt, email_verified_at auf null gesetzt.
  • resources/views/livewire/auth/login.blade.php:62–69: Login erfolgt vor der Verifizierungsprüfung.
  • app/Listeners/SyncCompanyMembershipsOnLogin.php:22–28 und app/Services/Auth/ContactAccessService.php:104–119: E-Mail-basierte Zuordnung ohne Verifizierungsprüfung.
  • app/Services/Auth/ContactAccessService.php:237–246: Vergabe von responsible und additive Kontaktzuordnung.
  • app/Policies/CompanyPolicy.php:34–40,64–74: diese Rolle gewährt Schreibzugriff.

Fehlerszenario: Ein Benutzer kontrolliert ein Passwortkonto und eine eigene E-Mail-Adresse. Eine fremde E-Mail steht in companies.email oder an einem zugeordneten Pressekontakt, gehört aber noch keinem anderen users.email-Datensatz. Der Benutzer ändert über den weiterhin aktiven Fortify-Endpunkt seine eigene Kontoadresse auf diese fremde Adresse. Nach Ab- und Anmeldung mit seinem unveränderten Passwort feuert der Login-Listener und trägt ihn bereits als Firmenverantwortlichen ein. Anschließend kann er über denselben Endpunkt auf seine eigene Adresse zurückwechseln und diese bestätigen. Die zuvor vergebenen Firmenmitgliedschaften bleiben erhalten.

Auswirkung: Dauerhafter Zugriff auf fremde Firmenstammdaten, Kontakte und entsprechend berechtigte Pressemitteilungen, ohne Besitznachweis des fremden Postfachs. Die verified-Middleware verhindert nur den unmittelbaren Zugang während des unverifizierten Zwischenzustands; sie verhindert nicht die Vergabe der Berechtigung.

Abgrenzung: Bestehende users.email-Einträge begrenzen durch Unique-Validierung die angreifbaren Zieladressen. CSRF ist keine Abwehr gegen einen Benutzer, der reguläre Änderungen am eigenen Konto ausführt. Ein fremdes Benutzerkonto wird durch diese Kette nicht unmittelbar übernommen; betroffen sind Firmenrechte.

Empfehlung: E-Mail-basierte Berechtigungen ausschließlich aufgrund einer aktuell nachgewiesenen Kontoadresse vergeben. Den Fortify-Endpunkt auf denselben pending_email-Prozess wie die reguläre Sicherheits-UI führen oder deaktivieren, wenn er nicht benötigt wird. Die Kontrolle muss im Zuordnungsservice liegen und alle Aufrufer erfassen. Bestehende automatisch entstandene Zuordnungen anschließend gezielt auf ihre Verifikationsgrundlage untersuchen; nicht pauschal alle Mitgliedschaften löschen.

Erforderlicher Regressionstest: Eigene bestätigte Adresse → fremde, nur am Kontakt gespeicherte Adresse → Passwortlogin → Rückwechsel und Bestätigung eigener Adresse. Während der gesamten Sequenz dürfen weder company_user noch contact_user unberechtigte Zuordnungen erhalten. Zusätzlich den direkten Serviceaufruf für unverifizierte und deaktivierte Benutzer prüfen.

F02 — Google-Login überspringt lokale 2FA

Priorität: P1.

Fundstellen: app/Http/Controllers/Auth/GoogleController.php:37–57; als Gegenbeispiel app/Http/Controllers/Auth/MagicLinkConsumeController.php:68–77 und resources/views/livewire/auth/login.blade.php:49–59.

Nach erfolgreicher Google-Auflösung ruft der Callback unmittelbar Auth::login($user, remember: true) auf. Eine Prüfung auf hasEnabledTwoFactorAuthentication() und die Übergabe in Fortifys Challenge fehlen. Passwort- und Magic-Login prüfen dies bereits.

Trigger: Ein aktives Bestandskonto mit bestätigter lokaler 2FA meldet sich über Google mit der passenden Adresse an. Die Anwendung erstellt sofort eine Sitzung. Dass der Google-Account eventuell selbst MFA verwendet, beweist nicht die Erfüllung der lokal konfigurierten TOTP-Anforderung.

Auswirkung: Die Sicherheitszusage „2FA aktiviert“ hängt vom gewählten Anmeldeweg ab. Das betrifft auch privilegierte Konten, sofern Google-Login für ihre Adresse funktioniert.

Empfehlung: Gemeinsame Login-Abschlusslogik für Passwort, Magic Link und Google: aktive Identität prüfen, gegebenenfalls Challengezustand setzen und erst nach erfolgreicher Challenge authentifizieren. Die Entscheidung über dauerhafte Anmeldung ebenfalls zentralisieren.

Regression: Google-Callback mit aktiviertem lokalen TOTP muss als Gast enden und zur Challenge führen. Ohne bestätigte 2FA bleibt das bisherige Verhalten. Deaktivierte Konten müssen gesperrt bleiben. tests/Feature/Auth/GoogleLoginTest.php deckt derzeit Neuanlage, Zuordnung, Deaktivierung und Fehler ab, jedoch diesen 2FA-Fall nicht.

F03 — Sicherheitsaktionen ohne erneute Identitätsprüfung; Recovery-Anzeige clientseitig freischaltbar

Priorität: P1.

Fundstellen: resources/views/livewire/customer/security.blade.php:39,74–109,120–164,179–186; routes/customer.php:28–31,73; config/fortify.php:152–155; vendor/laravel/fortify/routes/routes.php:142–172.

Die Konfiguration verlangt confirmPassword => true. Fortifys eigene 2FA-Routen setzen dafür password.confirm ein. Die Volt-Komponente ruft die Fortify-Actions direkt auf, ohne diesen Schutz gleichwertig zu prüfen. Deaktivierung der 2FA, Erzeugung neuer Recovery-Codes und Start einer E-Mail-Änderung sind allein mit einer bestehenden Sitzung möglich.

Zusätzlich hängt die Anzeige entschlüsselter Recovery-Codes von der ungesperrten öffentlichen Property showRecoveryCodes ab. Diese Property ist vom Client veränderbar. „Nur einmal anzeigen“ ist dadurch lediglich das normale UI-Verhalten, keine serverseitige Sicherheitsgrenze.

Trigger: Eine bereits bestehende oder entwendete Sitzung ruft disableTwoFactorAuthentication() bzw. regenerateRecoveryCodes() auf oder setzt showRecoveryCodes auf true. Dafür ist keine erneute Kenntnis des Passworts oder eines anderen starken Nachweises erforderlich.

Auswirkung: Temporärer Sitzungszugriff kann zu dauerhaft geschwächter Kontosicherheit führen. Der Angreifer braucht eine Sitzung; es handelt sich nicht um einen anonymen Zugriff auf fremde Recovery-Codes.

Empfehlung: Einen zeitlich begrenzten, serverseitig geprüften erneuten Identitätsnachweis vor sicherheitskritischen Aktionen und vor Recovery-Ausgabe verlangen. Für passwortlose Konten einen ausdrücklich definierten sicheren Ersatz vorsehen. showRecoveryCodes mindestens gegen Clientmutation schützen; das ersetzt die erneute Identitätsprüfung nicht. Sicherheitsänderungen nachvollziehbar protokollieren und den Kontoinhaber benachrichtigen.

Regression: Ohne aktuellen Nachweis müssen direkte Action-Aufrufe und set('showRecoveryCodes', true) scheitern. Die Tests in tests/Feature/CustomerProfileSecurityTest.php:209–270 prüfen zwar Einrichtung und normale Sichtbarkeit, erlauben die Regeneration bisher ohne erneuten Nachweis.

F04 — Soft Deletes werden als Nebenwirkung der Portal-Entkopplung entfernt

Priorität: P1.

Fundstellen:

  • app/Http/Controllers/Api/V1/PressReleaseController.php:36,238,257.
  • app/Http/Controllers/Api/V1/CompanyController.php:17,34.
  • app/Http/Controllers/PressReleasePreviewController.php:39–49.
  • app/Http/Controllers/Web/PressReleaseImageMediaController.php:23–26 und entsprechender Attachment-Controller.
  • app/Http/Controllers/Web/Concerns/AuthorizesPressReleaseMediaAccess.php:39–46.
  • app/Models/PressRelease.php:537–542.
  • resources/views/livewire/customer/press-kits/show.blade.php:233–260.

withoutGlobalScopes() deaktiviert nicht nur den Portalfilter, sondern auch den Soft-Delete-Filter. Nachfolgende ownedByApiUser()-/accessibleBy()-Scopes stellen den Soft-Delete-Schutz nicht wieder her. Die betroffenen Modelle verwenden tatsächlich SoftDeletes.

Konkrete Folgen:

  1. Eine über die API gelöschte eigene Entwurfs-PM kann weiterhin über Index/Show gefunden werden; auch das Update sucht ohne Soft-Delete-Scope.
  2. Ein bestehender Vorschau-Token kann eine soft-gelöschte PM weiter laden.
  3. Gelöschte Kontakte erscheinen weiterhin in der Firmenansicht, weil die Kontaktliste und der Einzelkontaktlookup alle Scopes entfernen.
  4. Medien laden die zugehörige Firma ohne Scopes. Eine soft-gelöschte, zuvor aktive Firma bleibt auf dem geladenen Model is_active=true; eine veröffentlichte PM kann dadurch weiterhin als öffentlich gelten. Die Instanzprüfung berücksichtigt deleted_at nicht. Die normale Firmenlöschung in resources/views/livewire/admin/companies/edit.blade.php:262 ändert diesen Aktivwert nicht.

Wichtiger Testbefund: tests/Feature/PressReleasePreviewTest.php:237–247 heißt „preview link for soft deleted press release returns 404“, führt tatsächlich aber $pr->forceDelete() aus. Der Test beweist nur den Fall eines physisch entfernten Datensatzes und verdeckt genau diese Lücke.

Empfehlung: Portalübergreifende Abfragen mit withoutGlobalScope(PortalScope::class) formulieren. Gelöschte Datensätze nur in ausdrücklich dafür vorgesehenen Verwaltungsfunktionen über withTrashed() zulassen. Sichtbarkeitsregeln für PM und Firma einschließlich Soft Delete zentralisieren und sowohl in Query- als auch Instanzprüfungen gleich umsetzen.

Regression: Soft Delete separat für PM, Firma, Kontakt, Bild und Anhang testen. Bestehende Vorschau- und Medienlinks sowie API-GET/PATCH nach Löschung prüfen. Außerdem verifizieren, dass berechtigte portalübergreifende Zugriffe auf nicht gelöschte Daten erhalten bleiben.

F08 — Admin-Gutschriften prüfen Rechte nur in mount()

Priorität: P2.

Fundstellen: resources/views/livewire/admin/users/show.blade.php:25–27,39–82; app/Services/Billing/AdminGrantService.php:29–49,59–99; routes/admin.php, Benutzer-Detailroute.

mount() verlangt users:manage; grantCredits() und grantFreePressReleases() prüfen diese Berechtigung nicht erneut. Auch der Service prüft den übergebenen Ausführenden nicht. Die persistente Admin-Middleware kontrolliert lediglich den Adminbereichszugang.

Trigger: Ein berechtigter Admin öffnet die Seite. Danach wird ihm users:manage entzogen oder er wird zum Editor herabgestuft. Als Editor erfüllt er weiterhin access-admin und kann aus dem alten Livewire-Zustand Gutschriften auslösen. Das gesperrte $id verhindert die Mutation des Zielbenutzers, aber nicht diesen Rechteentzugfall.

Empfehlung: Jede finanzwirksame Action erneut autorisieren; alternativ bzw. ergänzend den Aufruf zentral im Service absichern. Berechtigungen auf der Route als persistente Grenze setzen, wenn das gesamte Modul diese benötigt.

Regression: Komponente berechtigt öffnen, Rolle/Berechtigung während ihrer Lebensdauer entziehen, dann beide Actions aufrufen. Weder Ledger noch Käufe dürfen verändert werden. Der vorhandene Test AdminGrantTest prüft nur den initial gesperrten Seitenaufruf.

F09 — Superadmin-Kurzschluss ignoriert deaktivierte Konten

Priorität: P2.

Fundstellen: app/Providers/AppServiceProvider.php:114–119; app/Models/User.php:537–558; app/Http/Middleware/EnsureUserIsAdmin.php:21–25 und EnsureUserIsCustomer.php.

Gate::before liefert für jeden Superadmin true, bevor die Access-Gates überhaupt canAccessAdmin() oder canAccessCustomer() ausführen. Diese Methoden prüfen eigentlich zuerst is_active.

Trigger: Ein bereits angemeldetes Superadmin-Konto wird deaktiviert. Bei nachfolgenden Requests mit bestehender Sitzung kann der Gate-Kurzschluss die Sperre umgehen. Die gesonderten Login-Prüfungen schützen einen neuen Login, ersetzen aber nicht die Prüfung bereits bestehender Sitzungen.

Empfehlung: Kontodeaktivierung als vorrangige, nicht übersteuerbare Grenze behandeln. Superadmin-Erleichterungen erst danach anwenden. Bestehende Sitzungen und Tokens im Sperrprozess nach definierter Regel widerrufen.

Regression: Aktiven Superadmin anmelden, deaktivieren, Admin-/Customer-Route und Livewire-Folgeaktion aufrufen. Access-Gate und Model-Methode müssen denselben Ablehnungswert liefern.

F10 — Automatisch verliehene Firmenrechte besitzen keinen Entzugslebenszyklus

Priorität: P2.

Fundstellen: app/Services/Auth/ContactAccessService.php:104–119,225–246; resources/views/livewire/customer/press-kits/show.blade.php:193–209; app/Policies/CompanyPolicy.php:64–74.

Die E-Mail-Synchronisation ist ausdrücklich rein additiv. Wird eine Kontaktadresse geändert, ein Pressekontakt gelöscht oder eine Firmenadresse ersetzt, bleibt ein zuvor automatisch vergebener responsible-Pivot bestehen. Die Policy benötigt später nur diesen Pivot und nicht mehr die ursprüngliche E-Mail-Übereinstimmung.

Trigger: Ein ehemaliger Ansprechpartner erhält zunächst legitimen Zugang, wird anschließend aus den Firmenkontakten entfernt und behält trotzdem Schreibrechte. Dies ist unabhängig vom missbräuchlichen Verifikationspfad F01.

Empfehlung: Herkunft automatisch verliehener Rechte speichern und einen eigenen Entzugsprozess implementieren. Manuell erteilte Team-/Eigentümerrechte dürfen dabei nicht pauschal mitgelöscht werden. Im Administrationsprozess sichtbar machen, dass Kontaktlöschung und Berechtigungsentzug aktuell verschiedene Vorgänge sind.

Regression: Automatische Zuordnung erzeugen, die letzte berechtigende E-Mail-Grundlage entfernen, anschließend Firmenzugriff prüfen. Manuell zusätzlich erteilte Rechte müssen nach der fachlich festgelegten Regel erhalten bleiben.

6. Veröffentlichung, API, Medien und öffentliche Auslieferung

F07 — KI-Bewertung kann auf einer veralteten Inhaltsversion beruhen

Priorität: P1.

Fundstellen: app/Jobs/ClassifyPressRelease.php:44–81; app/Jobs/ScorePressRelease.php:38–61; app/Services/PressRelease/PressReleaseService.php:123–160,177–189,214–240.

Die Jobs kennen die PM-ID, jedoch keine Inhaltsversion. Sie laden den Inhalt, warten auf die externe Bewertung und speichern danach das Ergebnis ohne Vergleich mit dem inzwischen aktuellen Inhalt. Die Veröffentlichung lädt und sperrt zwar das aktuelle Model, prüft aber nicht, ob dieses den tatsächlich bewerteten Text enthält.

Trigger: Während Job A Version V1 bewertet, wird V2 gespeichert. Danach kommt A zurück und übernimmt sein Ergebnis für V2; ein jüngerer Job kann zusätzlich durch eine ältere Antwort überschrieben werden. Bei der Erstprüfung sind classification/content_score noch null; die Recheck-Helfer planen in diesem Zustand keinen Ersatzlauf ein. Administrative Änderungen und erlaubte Review-Bearbeitung bieten entsprechende Übergänge.

Auswirkung: Veröffentlichter Inhalt, Bewertung und KI-Audit können auseinanderfallen. Die erneute Blacklistprüfung im Veröffentlichungsservice kontrolliert eine andere, engere Regel und ersetzt keine Inhaltsversionsbindung.

Empfehlung: Monotone Inhaltsversion oder Hash bei Einplanung und Analyse speichern. Ergebnisübernahme und daraus abgeleitete Publikation nur bei übereinstimmender aktueller Version erlauben. Veraltete Antworten nachvollziehbar verwerfen und gegebenenfalls eine aktuelle Prüfung einplanen.

Regression: Fake-Treiber verändert den Text während der Analyse; zwei Jobs beenden sich in umgekehrter Reihenfolge; erste Analyse läuft parallel zu einer Bearbeitung. Ein altes Ergebnis darf weder neue Werte überschreiben noch V2 freigeben.

F11 — API-Eingaben können Pagination und fachliche Verträge umgehen

Priorität: P2.

Fundstellen: app/Http/Controllers/Api/V1/PressReleaseController.php:36–41; app/Http/Controllers/Api/V1/CompanyController.php:17–20; app/Http/Requests/Api/V1/StorePressReleaseRequest.php:33–46; entsprechender Update-Request.

paginate(min((int) per_page, 100)) begrenzt nur nach oben. Bei per_page=-1 bleibt ein negativer Wert übrig. Der installierte Eloquent-Builder reicht diesen an forPage() weiter; Query\Builder::limit() setzt für negative Werte kein Limit (vendor/.../Eloquent/Builder.php:1116–1126, vendor/.../Query/Builder.php:3110–3118). Die Abfrage kann dadurch den vollständigen für den Benutzer sichtbaren Bestand einschließlich Relationen laden.

Weitere Vertragslücken: language verlangt lediglich zwei Zeichen statt der unterstützten Ausgaben de|en; category_id verlangt Existenz, aber keine aktive, zum Zielportal passende Kategorie. Der Statusfilter wird ungeprüft an einen auf string typisierten Callback übergeben; ein Arrayparameter kann statt einer kontrollierten 422-Antwort einen Typfehler auslösen.

Auswirkung: Unbegrenzte Ergebnismengen für große Konten, unnötige Speicher-/Datenbanklast und fachlich ungültige Datensätze. Dies ist keine nachgewiesene fremde Datenfreigabe: die Eigentumsbedingungen bleiben bestehen.

Empfehlung: Queryparameter explizit als Integer im Bereich 1–100, Status als erlaubten Enumwert und Sprache als unterstützte Ausgabe validieren. Kategorie und Portal zusammen validieren; Legacy-Verträge bewusst separat berücksichtigen. Maximale Textgröße definieren, wenn API- und Webeingaben dieselben fachlichen Grenzen haben sollen.

Regression: per_page mit -1, 0, 1, 100, 101, Text und Array; status[]; language=zz; inaktive bzw. portalunpassende Kategorie. Ungültige Eingaben kontrolliert ablehnen, SQL-Limit stets begrenzen.

F12 — Melde-URL wird ohne Portal und Sprache einer PM zugeordnet

Priorität: P2.

Fundstellen: app/Http/Controllers/Web/PressReleaseReportController.php:325–335; app/Http/Controllers/Web/ReportFormController.php:112–124; app/Models/PressRelease.php:43–45 und Schemaindex press_releases_portal_language_slug_unique.

Beide Controller extrahieren lediglich den Slug hinter /press-release/, entfernen den Portal-Scope und wählen die erste veröffentlichte PM. Host und Ausgabe der gemeldeten URL werden nicht berücksichtigt. Laut tatsächlich vorhandenem Unique-Index ist ein Slug nur innerhalb von Portal und Sprache eindeutig.

Trigger: Zwei Portale oder Ausgaben besitzen denselben Slug. Eine Meldung zur zweiten URL kann den zuerst gefundenen Datensatz der anderen Ausgabe treffen. Auch eine fremde Domain mit passendem Pfad wird nicht als fremd erkannt.

Auswirkung: Falsche Zuordnung im Bearbeitungsprozess und gegebenenfalls Versand des Verifikationslinks an den falschen hinterlegten Pressekontakt. Es wird nicht an eine beliebig eingegebene Angreiferadresse versendet; diese vorhandene Schutzmaßnahme bleibt wirksam.

Empfehlung: Gemeinsamen Resolver für eigene kanonische und ausdrücklich unterstützte Legacy-URLs verwenden: Host-Whitelist, Portal, Sprache und Slug zusammen auflösen; öffentliche Sichtbarkeit einheitlich berücksichtigen. Nicht zuordenbare Meldungen im allgemeinen Meldeweg bewusst ohne PM-Bezug behandeln.

Regression: Gleicher Slug in beiden Portalen und in de/en, URL einer fremden Domain, kodierter Slug, gelöschte bzw. künftig terminierte PM. Jeweils genau die beabsichtigte Zuordnung oder einen kontrollierten Nichttreffer erwarten.

F13 — Öffentliche Caches überleben normale Firmenlöschung/-deaktivierung

Priorität: P2.

Fundstellen: resources/views/livewire/admin/companies/edit.blade.php:121–186,250–265; app/Observers/AdminPerformanceCacheObserver.php:17–35; app/Services/Web/NewsroomDetailPageData.php:26–57,65–74; app/Support/WebPageCache.php:25–33,136–138.

Der Firmenobserver invalidiert Admin-Statistiken. Die normale Firmenlöschung ruft lediglich delete() auf. Der öffentliche Newsroom prüft vor dem Seiten-Cache nur den Sonderfall gone_at, nicht die normale Löschung bzw. Deaktivierung. Beim Cachetreffer bleibt der vorher gespeicherte Inhalt verfügbar.

Trigger: Newsroomseite wird gecacht; danach wird die Firma normal gelöscht oder deaktiviert. Der nächste Aufruf erhält zunächst die gespeicherten Daten. Das öffentliche Cachesystem erlaubt ein Stale-Fenster bis 3.600 Sekunden. Das tatsächliche Ende hängt vom Neuaufbau und dessen Fehlerbehandlung ab; eine exakte maximale Sichtbarkeitsdauer wurde nicht gemessen.

Abgrenzung: Der spezielle 410-Entfernungsprozess besitzt bereits einen bewussten öffentlichen Cache-Flush. Gerade deshalb darf dieses Verhalten nicht auf die übrigen Löschwege übertragen werden.

Empfehlung: Alle öffentlichen Sichtbarkeitswechsel an denselben Cache-/Entfernungsprozess anschließen. Bei sensiblen Entfernungen den aktuellen Sichtbarkeitsstatus vor jedem Cachetreffer prüfen. Seiten-Cache und Browser-/CDN-Mediencache getrennt behandeln; ein Anwendungsflush löscht keine bereits extern gecachten Bilder.

Regression: Cache aufbauen, Firma löschen/deaktivieren, unmittelbar danach Newsroom, Kategorie, Startseite und Medien abrufen. Auch Kontaktänderungen mit gecachter Newsroomansicht prüfen.

F23 — Dateisystemfehler können Erfolg vortäuschen oder bestehende Logos verlieren

Priorität: P2.

Fundstellen: app/Services/PressRelease/PressReleaseAttachmentStorage.php:59–80; config/filesystems.php:33–47; resources/views/livewire/customer/press-kits/show.blade.php:115–130; entsprechender Austausch in resources/views/livewire/admin/companies/edit.blade.php:150–165.

Der Attachment-Service ignoriert den Rückgabewert von $disk->put(). Gleichzeitig konfigurieren die Disks throw => false und report => false. Ein fehlgeschlagener Schreibvorgang kann somit normale Dateimetadaten zurückliefern, obwohl die Datei nicht gespeichert wurde.

Beim Logo-Austausch wird das bisherige Logo einschließlich Varianten vor der erfolgreichen Speicherung des neuen Logos gelöscht. Scheitert die neue Speicherung oder die anschließende Datenbankänderung, kann das alte Logo bereits unwiederbringlich fehlen.

Trigger: Voller Datenträger, Schreibrechteproblem oder fehlgeschlagener Storagezugriff beim Upload/Austausch.

Empfehlung: Storagefehler durch geprüften Rückgabewert bzw. geeignete Exceptionkonfiguration eindeutig behandeln. Neue Datei zuerst vollständig speichern, dann DB-Referenz wechseln und alte Datei erst anschließend entfernen. Neue Dateien bei gescheitertem DB-Commit aufräumen. Eine DB-Transaktion allein macht Dateisystemoperationen nicht atomar.

Regression: Storage-Fake/Mock liefert false; neue Datei kann nicht geschrieben werden; DB-Save nach erfolgreichem Upload schlägt fehl. Kein Erfolgshinweis ohne Datei und kein Verlust des bisherigen Logos.

F26 — Anmeldemodal ohne Fokusführung

Priorität: P2, Bedienbarkeit/Barrierefreiheit.

Fundstellen: resources/views/components/web/site-header.blade.php:102–105,158,189–236.

Das eigene Modal verwendet x-show, role="dialog" und aria-modal="true". Eine explizite Übernahme des Tastaturfokus, Fokusbegrenzung innerhalb des Modals und Rückgabe an den Auslöser sind im untersuchten Template nicht implementiert. Anders als ein Flux-Modal erhält dieser eigene Div-Aufbau solche Funktionen nicht allein durch ARIA-Attribute.

Trigger: Öffnen über Tastatur und anschließende Tab-Navigation. Der Fokus kann im Hintergrund bleiben bzw. außerhalb des sichtbaren Dialogs weiterlaufen. Escape ist implementiert und als positive Teilabsicherung zu berücksichtigen.

Empfehlung: Bestehende zugängliche Dialogkomponente verwenden oder Fokusübernahme, Fokusbegrenzung, Rückgabe und geeignete Hintergrund-Deaktivierung vollständig ergänzen. Kein pauschaler rechtlicher Konformitätsbefund: Ein visueller und assistiver Browserlauf fehlt.

Regression: Browserprüfung mit Tab/Shift-Tab, Escape, Öffnen/Schließen, Rückfokus und Screenreader-Beschriftung bei Desktop- und Mobilbreite.

7. Newsletter, Abrechnung und Hintergrundverarbeitung

F05 — Newsletter dupliziert und überspringt Empfänger ab der zweiten Seite

Priorität: P1.

Fundstellen: app/Services/Newsletter/CampaignSender.php:37–50; app/Services/Newsletter/CampaignRecipients.php:175–176; vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php:3179–3190; database/migrations/2026_07_21_111646_create_newsletter_campaign_tables.php:36–46.

Der Sender verwendet chunkById(500) auf einer Empfängerquery, die bereits nach created_at oder last_login_at sortiert. Laravel ersetzt dabei nur die Sortierung der ID-Spalte; die andere führende Sortierung bleibt bestehen.

Deterministisches Beispiel: 1.000 Benutzer mit zeitlich steigenden IDs und unterschiedlichen created_at-Werten, Sortierung created_at DESC. Die erste Seite liefert 1000 bis 501. Die zweite Seite setzt id > 501, sortiert aber weiterhin zeitlich absteigend und liefert 1000 bis 502 erneut. Die IDs 1 bis 500 werden nicht erreicht.

Die Migration und die tatsächlich abgefragten MySQL-Indizes enthalten keinen Unique-Constraint auf Kampagne/Benutzer oder Kampagne/E-Mail. Die wiederholten Empfängerzeilen können daher gespeichert und als eigene Jobs versendet werden.

Auswirkung: Doppelversand, ausgelassene Empfänger und falsche Kampagnenstatistiken. Das ist ein konkreter Grenzfehler, kein bloßer Performancevorschlag.

Empfehlung: Auswahlquery von der UI-Sortierquery trennen. Vor ID-Chunking eine eindeutige ID-Reihenfolge herstellen, beispielsweise über reorder(). Falls die Versandfolge fachlich relevant ist, einen passenden zusammengesetzten Cursor bzw. eine eingefrorene Versandposition einsetzen. Empfänger zusätzlich eindeutig absichern.

Regression: Mindestens 1.001 Benutzer, unterschiedliche Zeitwerte und alle angebotenen Sortierungen. Jede ausgewählte Benutzer-ID genau einmal und vollständige Abdeckung prüfen. Die vorhandenen Senderprüfungen in tests/Feature/Admin/NewsletterCampaignTest.php:230–281 überschreiten die Chunk-Grenze nicht.

F06 — MAN-Abrechnung läuft über das explizite Vertragsende hinaus

Priorität: P1.

Fundstellen: app/Services/Billing/ManualInvoiceService.php:44–58,71–80,143–148; app/Models/UserPaymentOption.php:94–115; app/Console/Commands/GrandfatherLegacySubscriptions.php:99–100,207–209.

Der Import übernimmt valid_until_date nach grandfathered_until. Die aktive Legacy-Berechtigungslogik berücksichtigt dieses Vertragsende. Weder die Rechnungsauswahl noch die erneute Prüfung unter dem Row Lock berücksichtigen es.

Trigger: Status bleibt Grandfathered, das explizite Vertragsende ist bereits überschritten, current_period_end ist fällig. Der Service erstellt weiterhin eine MAN-Rechnung und verlängert die Periode.

Auswirkung: Zugang und Abrechnung beurteilen denselben Vertrag widersprüchlich. Es können Rechnungen für einen technisch bereits als abgelaufen behandelten Vertrag entstehen. Ob solche Fälle im Bestand vorhanden sind, wurde nicht anhand personenbezogener Rechnungsdaten untersucht.

Empfehlung: Eine zentrale fachliche Abrechenbarkeitsregel verwenden und das Vertragsende sowohl bei der Auswahl als auch direkt vor dem Schreiben unter Sperre beachten. Behandlung des Endtags und einer gegebenenfalls abschließenden Abrechnung ausdrücklich festlegen.

Regression: Gestern beendeter, heute endender und unbefristeter Vertrag; auch direkter Aufruf von invoiceFor() muss dieselbe Grenze beachten.

F14 — Newsletter-Start ist nicht gegen Parallelität und Teilabbrüche abgesichert

Priorität: P2.

Fundstellen: app/Services/Newsletter/CampaignSender.php:29–75; Empfängertabelle wie F05.

Der Draft-Guard prüft ein bereits geladenes Model. Empfänger werden ohne gemeinsamen Start-Lock eingefügt, anschließend wird Sending gesetzt und erst danach werden Jobs einzeln erzeugt.

Fehlerfälle: Zwei Requests starten denselben Entwurf; ein Start bricht mitten im Empfängerimport ab; oder der Queue-Dispatch scheitert nach einigen Jobs. Je nach Zeitpunkt bleiben doppelte Empfänger, eine teilweise befüllte Draft-Kampagne oder eine nicht mehr startbare Sending-Kampagne mit dauerhaft wartenden Empfängern zurück.

Empfehlung: Atomare Übernahme des Kampagnenstarts, eindeutige Empfänger und wiederaufnehmbare Einplanung aus persistentem Zustand. Beispielsweise vorbereitender Status plus Compare-and-swap/Row Lock und separater Dispatcher/Outbox. Eine große DB-Transaktion allein garantiert die Übergabe an eine externe Queue nicht.

Regression: Zwei unabhängig geladene Modelinstanzen derselben Kampagne; Ausnahme nach dem ersten Empfängerbatch; Ausnahme nach dem ersten Dispatch. Wiederanlauf muss alle Empfänger genau einmal einplanen.

F15 — Versandjob kann doppelt senden und Fortschritt falsch zählen

Priorität: P2.

Fundstellen: app/Jobs/SendNewsletterCampaignMail.php:32–57,80–102.

Das Lesen von Pending ist kein atomarer Claim. Erst nach SMTP-Versand wird der Empfängerstatus geändert; Empfängerabschluss und Kampagnenzähler erfolgen in separaten Schreibvorgängen.

Trigger: SMTP nimmt eine Nachricht an, anschließend scheitert das DB-Update. Der Retry sieht weiterhin Pending und sendet nochmals. Bei einem Abbruch nach Empfängerupdate, aber vor Zählererhöhung bleibt der Fortschritt dagegen dauerhaft zu niedrig. Parallel laufende Instanzen können dieselbe Pending-Zeile gleichzeitig bearbeiten.

Empfehlung: Atomarer Versandclaim und idempotenter, transaktionaler DB-Abschluss. Für die Grenze SMTP/DB eine bewusste Zustellstrategie definieren: Provider-Idempotenz, falls verfügbar, oder sichtbarer Zustand „Zustellung unklar“. Exakt einmalige SMTP-Zustellung lässt sich nicht durch einen DB-Lock allein garantieren.

Regression: Fehler vor/nach SMTP-Annahme und zwischen Statusupdate und Zählerpflege; parallele Bearbeitung derselben Empfängerzeile. Wiederanlauf und Abschlussstatus prüfen.

F16 — Die 50 ältesten nicht abrechenbaren Verträge können alles Weitere blockieren

Priorität: P2.

Fundstellen: app/Console/Commands/GenerateManualInvoices.php:19–21,30,41–68; app/Services/Billing/ManualInvoiceService.php:44–58,87–107; routes/console.php:111–115.

Der tägliche Lauf wählt standardmäßig die 50 ältesten fälligen Vereinbarungen. Nicht abrechenbare Fälle, beispielsweise ohne Rechnungsadresse oder Intervall, werden übersprungen und behalten ihre alte Fälligkeit. Ein nächster Batch wird nicht verarbeitet.

Trigger: Die ersten 50 fälligen Vereinbarungen sind unvollständig. Sie werden jeden Tag erneut ausgewählt, während die 51. vollständig abrechenbare Vereinbarung nie erreicht wird. Auch ohne ungültige Fälle ist der planmäßige Durchsatz auf 50 Vereinbarungen täglich begrenzt.

Empfehlung: Den fälligen Bestand mit begrenzten Cursor-Batches durchgehen; fehlerhafte Fälle separat markieren und erneut bearbeiten. Metriken für Rückstand und Alter des ältesten bearbeitbaren Falls ergänzen.

Regression: 50 unvollständige Vereinbarungen vor mindestens einer vollständigen. Die vollständige muss trotz der übersprungenen Fälle verarbeitet werden.

F17 — KI-HTTP-Versuche können länger dauern als der Worker zulässt

Priorität: P2, bedingter Betriebsbefund.

Fundstellen: app/Services/PressRelease/Classification/Drivers/OpenAiClassificationDriver.php:35–45; app/Services/PressRelease/ContentScore/Drivers/OpenAiContentScoreDriver.php:43–53; config/scoring.php:38,62; app/Jobs/ClassifyPressRelease.php:33; app/Jobs/ScorePressRelease.php:29; app/Console/Commands/RunClassificationQueue.php:23–34; config/queue.php:42,70.

Der Content-Score-Treiber hat standardmäßig bis zu 30 Sekunden pro HTTP-Versuch; die Retry-Liste [200, 500] erlaubt drei Versuche. Sein mögliches Gesamtbudget liegt damit bei ungefähr 90,7 Sekunden zuzüglich sonstiger Arbeit. Die Klassifikation verwendet laut config/scoring.php dagegen standardmäßig 15 Sekunden und kommt auf ungefähr 45,7 Sekunden; das gleiche Überschreitungsrisiko entsteht dort erst bei entsprechend höherer Konfiguration oder zusätzlicher Laufzeit. Die Jobs besitzen kein eigenes Timeout; der Projekt-Worker setzt ebenfalls keines, sodass der Laravel-Standard von 60 Sekunden relevant ist. retry_after liegt standardmäßig bei 90 Sekunden.

Trigger: Mehrere langsame Transporttimeouts beim Content-Score. Der Worker kann den Prozess beenden, bevor der vorgesehene deterministische Fallback erreicht wird. Produktionsseitig anders konfigurierte Timeouts können das Verhalten ändern und wurden nicht verifiziert.

Empfehlung: HTTP-Gesamtbudget, Jobtimeout, Workerparameter und retry_after zusammen abstimmen. retry_after mit Sicherheitsabstand über dem maximalen Jobtimeout halten. Ein isoliertes Anheben nur des Worker-Timeouts kann Doppelverarbeitung begünstigen.

Regression: Kontrollierte langsame/fehlgeschlagene Treiberantworten mit realem Queue-Worker; Fallback und endgültiges Scheitern beobachten. Ein synchroner Jobtest beweist dieses Verhalten nicht.

F18 — Monatskontingent-Reset ist nicht idempotent

Priorität: P2.

Fundstellen: app/Console/Commands/ResetMonthlyPressReleaseQuota.php:30–34; routes/console.php:121–126; app/Services/PressRelease/PressReleaseService.php:291–296.

Der Command setzt alle positiven Monatszähler auf null, ohne die bereits zurückgesetzte Periode zu speichern. Er ist am Monatsersten um 00:05 geplant.

Trigger: Publikationen zwischen Mitternacht und 00:05 werden kurz darauf wieder aus dem Verbrauch entfernt. Eine Wiederholung nach weiteren Publikationen löscht den Verbrauch nochmals. Wird der Schedulertermin verpasst, ist im Resetpfad kein automatischer Catch-up vorgesehen.

Empfehlung: Verbrauch an eine eindeutige Kalenderperiode binden und den Periodenwechsel atomar genau einmal vornehmen. Alternativ periodisierte Verbrauchsdatensätze verwenden. Nachholung beim ersten Zugriff definieren, damit ein einzelner Crontermin nicht die gesamte Kontingentlogik bestimmt.

Regression: Monatswechsel, zweimaliger Reset mit Publikation dazwischen, verspäteter Lauf. Kalenderkontingent nicht ohne fachliche Entscheidung auf Stripe-Aboperioden umstellen.

F19 — Zwei Aufrufaggregatoren können dieselben Events doppelt zählen

Priorität: P2, Parallelitätsbedingung.

Fundstellen: app/Console/Commands/AggregatePressReleaseViews.php:45–46,73–91,99–113; Schedulerregistrierung in routes/console.php.

Counter und Fortschrittsmarke werden erfreulicherweise gemeinsam transaktional geschrieben. Die Fortschrittsmarke wird jedoch vorher gelesen und nicht unter einer gemeinsamen Sperre neu geprüft.

Trigger: Ein manueller Command-Lauf überlappt den Scheduler oder zwei unabhängig gestartete Prozesse laufen gleichzeitig. Beide lesen denselben Marker und addieren dieselben Ereignisse. withoutOverlapping() schützt den Scheduleraufruf, jedoch nicht automatisch jeden manuellen Einstieg.

Empfehlung: Einen für alle Einstiegspfade gemeinsamen Command-Lock verwenden oder die Watermark unter Row Lock lesen und den Batch innerhalb der geschützten Sequenz bestimmen. Laufzeitbegrenzung für dauerhaften Ereignisnachschub ergänzen.

Regression: Zwei DB-Verbindungen mit kontrolliertem Interleaving. Jeder Event darf nur einmal in hits eingehen; Wiederholung ohne neue Events muss wirkungslos sein.

F20 — Geplante Publikationen melden trotz technischer Fehler Erfolg

Priorität: P2.

Fundstellen: app/Console/Commands/PublishScheduledPressReleases.php:93–117; routes/console.php, Failure-Hook für press-releases:publish-scheduled.

Technische Exceptions werden gezählt und über report() protokolliert. Der Command liefert anschließend trotzdem immer SUCCESS. Der konfigurierte onFailure-Alarm hängt jedoch vom fehlgeschlagenen Command ab.

Trigger: Einzelne oder alle fälligen PMs scheitern im Veröffentlichungsservice. Exit-Code-basierte Überwachung meldet einen erfolgreichen Lauf.

Empfehlung: Erwartete fachliche Ablehnungen von technischen Fehlern unterscheiden. Bei technischen Fehlern einen Fehler-Exit-Code liefern oder einen ausdrücklichen Alarm auslösen. Vorhandenes Fehlerlogging beibehalten; der Befund lautet nicht „Fehler werden überhaupt nicht protokolliert“.

Regression: Service wirft technische Exception: Fehler-Exit-Code bzw. Alarm nachweisen. Blacklistablehnung separat testen und fachlich definieren.

8. Datenbank, Importe und Performance

F21 — Legacy-Import ist nicht durchgehend speicherbegrenzt oder atomar

Priorität: P2; Relevanz abhängig von weiteren Import-/Delta-Läufen.

Fundstellen: app/Services/Import/PressReleaseImporter.php:34–58,159–170,182–265; insbesondere die Identitätsübernahme bei :189.

Vor dem 200er-PM-Chunking werden die vollständigen Legacy-Bild- und Kontaktpivot-Tabellen geladen und gruppiert. Auch ein kleiner --since-Deltaimport benötigt dadurch Speicher abhängig vom Gesamtbestand.

Pro PM erfolgen Hauptdatensatz, Zeitstempel, Bilder, Kontakte und Importmap ohne gemeinsame Zieltransaktion. Scheitert der Import nach Speicherung der PM, aber vor dem Mapping, existiert ein Teilimport. Der Wiederanlauf identifiziert den bereits vorhandenen Datensatz nicht in allen Fällen unabhängig von der Importmap und kann seine öffentliche UUID-/Slug-Identität verändern.

Empfehlung: Relationen nur für die Legacy-IDs des aktuellen Chunks laden. Zieländerungen einschließlich Mapping je PM transaktional speichern. Vorhandene Zielidentität auch ohne Mapping anhand des stabilen Legacy-Schlüssels erkennen. Delta-Wiederanlauf ausdrücklich als fachlichen Vertrag testen.

Regression: Ausnahme nach Hauptdatensatz, aber vor Mapping; Wiederholung muss dieselbe UUID und denselben Slug behalten. Großer Mediengesamtbestand mit kleinem PM-Delta darf den Speicherbedarf nicht linear zum Gesamtbestand anwachsen lassen.

F22 — E-Mail-Suche umgeht den bereits angelegten Index

Priorität: P2.

Fundstellen: app/Services/Auth/ContactAccessService.php:128–146; app/Models/User.php:186; Indexmigration für companies.email und tatsächlich vorhandener companies_email_idx.

Die Firmenzuordnung fragt LOWER(email) = ? ab. Ein normaler Index auf email kann diese Funktionsbedingung in der beobachteten Query nicht verwenden. Das wurde mit zwei ausschließlich lesenden EXPLAIN-Abfragen überprüft; als Vergleichswert wurde eine synthetische Adresse unter example.invalid verwendet.

Vergleich MySQL-Zugriff Index Geschätzte Zeilen
LOWER(email) = ... AND deleted_at IS NULL ALL keiner 110.291
email = ... AND deleted_at IS NULL ref companies_email_idx 1

Die verfügbare Datenbank verwendet für companies.email utf8mb4_unicode_ci, also eine bereits case-insensitive Kollation. Die Zahlen sind Optimizerschätzungen, keine gemessenen Laufzeiten und keine exakt gelesenen Datenmengen. Für users und contacts wurde in diesem Audit kein eigener EXPLAIN-Vergleich vorgenommen.

Auswirkung: Ein vorhandener Performanceindex bleibt gerade in einem häufigen Login-/Zuordnungspfad ungenutzt. Mehrere Loginverfahren und erneute Zuordnungen können diese Suche wiederholt auslösen.

Empfehlung: E-Mail-Normalisierung und Kollationsvertrag festlegen. Unter der vorhandenen MySQL-Kollation einen direkten Vergleich nutzen und Groß-/Kleinschreibungsfälle testen; bei abweichenden Datenbanken gegebenenfalls eine indizierte normalisierte Spalte verwenden. Nicht ungeprüft die gleiche Annahme auf alle Tabellen/Umgebungen übertragen.

Regression: Groß-/Kleinschreibung, führende/nachfolgende Leerzeichen und Legacy-Adressen; zusätzlich EXPLAIN für den finalen Querypfad. Erfolgskriterium ist korrekte Zuordnung mit Indexzugriff, nicht nur eine kürzere Query.

8.1 Beobachtete Größenordnung und Konsequenzen

Lesend aus information_schema.TABLES entnommene InnoDB-Schätzwerte der verfügbaren Verbindung:

Tabelle Geschätzte Zeilen
press_releases 134.979
companies 110.291
users 65.572
contacts 19.018
press_release_events 952.754
activity_events 5.316

Diese Werte sind ungefähre Schema-/Optimierermetadaten. Es wurden dafür keine einzelnen Personen-, Kunden- oder Nachrichtendatensätze ausgelesen. Sie belegen, dass Vollscans, unbegrenzte Collection-Ladevorgänge und kleine Batch-Grenzfehler praktisch relevante Größenordnungen erreichen können.

8.2 Weitere Performanceverbesserungen

  1. Cacheinvalidierung gezielter machen: AdminPerformanceCacheObserver verwirft bei jeder beobachteten Änderung die gesamte Liste der Admin-Cachekeys. Bei Importen oder vielen einzelnen Änderungen entstehen unnötige Löschoperationen. Zuerst Anzahl und Dauer messen; danach nach Domäne bzw. Batch invalidieren.
  2. Abfragebudget pro Seite definieren: Für PM-Listen, Firmenansicht und Dashboard sowohl Abfrageanzahl als auch geladene Zeilen messen. Eine konstante Queryanzahl allein schließt übergroße Relationen nicht aus.
  3. Kein zusätzlicher Index ohne Query-Plan: Bereits viele kombinierte Indizes sind vorhanden. Weitere Indizes erhöhen Schreib- und Migrationskosten. Kandidaten erst anhand realer WHERE-/ORDER-BY-Pfade bewerten.
  4. Öffentliche Suche begrenzen: Kurze Suchbegriffe fallen bewusst auf LIKE zurück. Eingabelänge, Wiederholrate, teure Sortierungen und tiefe Seiten bei großen Beständen beobachten. Keine unbelegte SQL-Injection behaupten: die betrachteten Rohbedingungen verwenden Parameterbindung.
  5. Firmen-/Kontaktzuordnung bündeln: Die Zuordnung führt pro Firma weitere Existenz-/Pivotabfragen aus. Nach F01/F10 kann dieser Pfad auf konsistente, gebündelte Berechtigungsdaten ausgerichtet werden; Sicherheit geht vor Queryverkürzung.

9. Tests, CI und Entwicklungsqualität

F24 — Lint-Workflow prüft den eingecheckten Stil nicht verbindlich

Priorität: P2.

Fundstellen: .github/workflows/lint.yml:13–14,31–46.

Der Workflow führt vendor/bin/pint im schreibenden Modus aus. Die nachfolgende Commit-Stufe ist auskommentiert; ein Diff-/Fehlergate fehlt. Der CI-Checkout kann daher erfolgreich formatiert werden, während der eigentliche Commit unverändert unformatiert bleibt. Ein erfolgreicher Workflow ist kein Nachweis, dass der eingecheckte Code bereits dem Stil entspricht.

Empfehlung: Einen bewusst prüfenden CI-Mechanismus verwenden: beispielsweise nach dem erforderlichen Formatierungslauf einen verbleibenden Git-Diff als Fehler werten. Die für dieses Projekt geltende lokale Pint-Regel und den CI-Vertrag ausdrücklich unterscheiden. Schreibrechte contents: write sind für einen rein prüfenden Job nicht notwendig.

Regression/Abnahme: Absichtlich unformatierter Probe-Commit muss den erforderlichen CI-Check rot machen; sauberer Commit grün. In diesem Audit wurde kein Formatter ausgeführt, da keine PHP-Datei geändert werden sollte.

F25 — Hohe Testzahl, aber Lücken bei produktionsnahen und sicherheitskritischen Zuständen

Priorität: P2.

Fundstellen: phpunit.xml:7–14,29–43; phpstan.neon:16–27; .github/workflows/tests.yml:35–61; tests/Feature/PressReleasePreviewTest.php:237–247; tests/Feature/CustomerProfileSecurityTest.php:253–270.

Die Testkonfiguration erzwingt SQLite :memory: und synchrone Queues. Das ist für viele schnelle Featuretests sinnvoll, beweist aber keine MySQL-Zeilensperren, Fulltext-Ausführung, Kollationsdetails, Queue-Timeouts oder echte konkurrierende Transaktionen. Die CI-Datei zeigt keine ergänzende MySQL-/Queue-Matrix.

PHPStan arbeitet auf Level 4 mit Baseline. Die Analysepfade umfassen app, Bootstrap, Config, Factories, Seeder und Routen, jedoch nicht die umfangreiche PHP-Logik in Volt-Blade-Dateien. Diese Logik ist nicht schon dadurch statisch analysiert, dass das Projekt Larastan verwendet.

Hinzu kommen konkret irreführende bzw. zu kleine Tests: Der Soft-Delete-Test benutzt Force Delete (F04); Newslettertests überschreiten die Chunk-Grenze nicht (F05); die Recovery-Tests prüfen normale UI-Sichtbarkeit, nicht die Manipulation der öffentlichen Property (F03). Ein JS-/Browser-Testlauf ist in den betrachteten Paket- und CI-Dateien nicht eingerichtet.

Empfehlung: Bestehende schnelle Tests behalten und gezielt ergänzen: MySQL-Integration für Locks/Fulltext, echte Workerprüfung für Zeitgrenzen, Browserfälle für Fokus/Livewire-Navigation und negative Lifecycle-Tests für Rechte-/Identitätsänderungen. Geschäftskritische Logik schrittweise aus großen Volt-Dateien in bereits etablierte analysierbare Services/Actions verlagern. Baseline nur begründet verändern; sie nicht als allgemeine Fehlerablage behandeln.

Abnahme: F01–F07 erhalten jeweils einen zuvor fehlschlagenden Regressionstest; mindestens ein produktionsnaher Lauf beweist die DB-/Queue-spezifischen Fälle. Eine Code-Coverage-Prozentzahl wurde nicht ermittelt und wird nicht aus der Testanzahl abgeleitet.

F27 — composer run dev startet standardmäßig keinen Vite-Server

Priorität: P3.

Fundstellen: composer.json, Script dev; package.json, Scripts dev, dev:portal, dev:web, dev:all.

Composer startet neben Server, Queue und Logs npm run dev. Dieses npm-Script gibt jedoch nur einen Hinweistext aus und beendet sich. Die tatsächlichen Vite-Server werden ausschließlich durch die spezialisierten Scripts gestartet.

Auswirkung: Der übliche Entwicklungsstart wirkt aktiv, liefert aber keine laufende Asset-Neukompilierung. Vorhandene Builddateien können den Fehler zunächst verdecken. Ohne Assets können zusätzlich Manifest-/Darstellungsfehler auftreten.

Empfehlung: Einen funktionierenden Standardstart definieren und die bewusste Auswahl einzelner Bundles zusätzlich anbieten. Den Bedarf an externem MySQL, Redis, Mailpit und Traefik im Startprozess klar dokumentieren: docker-compose.yml referenziert diese Infrastruktur, stellt sie selbst aber nicht vollständig bereit.

Abnahme: Aus einer vorbereiteten frischen Entwicklungsumgebung den dokumentierten Hauptbefehl starten; beide gewünschten Hotfiles/Ports und eine sichtbare Assetänderung prüfen. Kein Dependency-Upgrade dafür erforderlich.

F28 — Entwicklungsdokumentation enthält widersprüchliche aktuelle Zustände

Priorität: P3.

Fundstellen: docs/README.md:3–6,24–45; dev/roadmap.md:3–6; dev/README.md; resources/views/admin/README.md; resources/views/admin/BACKEND_STATUS.md.

docs/README.md bezeichnet den Stand vom 12.06.2026 als Einstieg und führt inzwischen vorhandene Funktionen als noch nicht umgesetzt. dev/roadmap.md ist als Stand vom 20.07.2026 formuliert, während Entwicklung und Berichte unter dev/2026-09-01 bis September weiterlaufen. Mindestens der Link auf dev/frontend/hub-flux/PROGRESS.md verweist auf die alte, inzwischen archivierte Struktur. Admin-Statusdateien enthalten ebenfalls ältere TODO-Stände.

Auswirkung: Neue Entwickler können vorhandene Funktionen nochmals planen, veraltete Entscheidungen als verbindlich ansehen oder wichtige offene Betriebsaufgaben übersehen. Umfangreiche Dokumentation hilft nur, wenn Gegenwart, historische Entscheidung und Archiv eindeutig unterscheidbar sind.

Empfehlung: Einen einzigen aktuellen Einstieg mit Datum, Verantwortlichkeit und Links auf Code-/Betriebsreferenzen festlegen. Historische Dokumente als solche markieren und verschobene Verweise korrigieren. Für jede neue Entscheidung den betroffenen bisherigen Entscheidungsstand ausdrücklich ersetzen.

Abnahme: Ein neuer Entwickler kann aus dem Einstieg Architektur, lokalen Start, Prüfkommandos, aktuellen Backlog und Deploymentablauf ohne widersprüchliche Statusangaben finden. Links automatisiert auf vorhandene lokale Ziele prüfen.

9.1 Entwicklungsverlauf

Die betrachtete lokale Git-Historie zeigt seit April 2026 folgende Commitverteilung: Mai 7, Juni 110, Juli 222, September 14. Diese Zahlen sind nur die sichtbare Historie des Checkouts, keine Produktivitätsmessung oder vollständige Teamhistorie.

Die jüngsten Änderungen sind fachlich fokussiert: Firmenkonsolidierungs-Trockenlauf, Konversionsmessung, API-Portalauflösung, Newsletter-Bounces, E-Mail-Index, Wiederherstellung der statischen Analyse und Entfernung mit 410. Das spricht für eine aktive Nachbesserungsphase nach dem Launch.

Das wiederkehrende Entwicklungsrisiko ist teilweise Zentralisierung: Neue Services und abgesicherte Pfade existieren, alte Alternativwege bleiben gleichzeitig aktiv. Beispiele sind der sichere pending_email-Dialog neben der sofortigen Fortify-Änderung, 2FA im Magic-Login neben dem direkten Google-Login und ein korrekter 410-Cacheflush neben normalem delete(). Künftige Änderungen sollten deshalb immer alle Einstiegspfade einer fachlichen Operation erfassen.

10. Weitere Optimierungen und gezielt offene Prüfungen

Die folgenden Punkte sind bewusst nicht als zusätzliche bestätigte Sicherheitslücken gezählt. Sie benötigen Messung, eine fachliche Entscheidung oder einen produktionsnahen Nachweis.

Bereich Beobachtung Nächster sinnvoller Schritt
Authentifizierungsarchitektur Mehrere Login- und Profiländerungspfade sind auseinanderentwickelt Gemeinsame Identitäts-/Loginabschlusslogik und einheitlicher E-Mail-Änderungsservice
Google-Identität SocialAuthService verknüpft primär per E-Mail und überschreibt Providerdaten Umgang mit stabiler Provider-ID, verifizierter Adresse, Providerwechsel und Account-Linking ausdrücklich definieren; nicht als nachgewiesene Google-Tokenfälschung einstufen
Rollen/Presets Preset-Routen sind nicht mit einer eigenen Managementpermission eingeschränkt Entscheiden, ob Editoren globale Voreinstellungen ändern dürfen; dann Route und Actions symmetrisch absichern
MAN-Perioden addMonth() / addYear() können an Monats-/Schaltjahresgrenzen überlaufen Vertraglichen Periodenanker definieren; 31. Januar und 29. Februar testen
MAN-Steuerdaten MAN-Resolver und Checkout nutzen unterschiedliche Prüftiefe bei USt-IDs Technischen Datenvalidierungsvertrag angleichen und fachlich freigeben; keine Steuerrechtsbewertung aus diesem Audit ableiten
Legacy-Aufrufzahlen Reimport schreibt absolute Legacy-hits, neuer Aggregator addiert eigene Events Bei weiteren Delta-Läufen Eigentümerschaft des Zählers definieren; Verlust bereits aggregierter neuer Aufrufe verhindern
Mediencache Öffentliche Medien erhalten public, max-age=86400 Anforderungen an Entfernungsfristen, CDN-Purge und versionierte URLs prüfen; gespeicherte Browserkopien sind nicht durch DB-Änderung widerrufbar
Bildverarbeitung MIME-/Bytegrenzen vorhanden, GD-Verarbeitung im Uploadpfad Maximale Pixelmaße und Dekompressions-/Speicherbudget prüfen; Tests mit großen stark komprimierten Bildern
Newsletter-Anmeldung API erzeugt unbestätigte Einträge; Sync-Provider ist teilweise Platzhalter End-to-End bestätigen, wer Bestätigungsmail, DOI-Abschluss und externen Sync tatsächlich übernimmt; keine funktionierende Integration aus einem 201 allein ableiten
Queue-Priorisierung Klassifikation und Default-Jobs teilen laut Runbook Worker Bei Last separate Kapazität für Transaktionsmails/Newsletter/KI erwägen; Wartezeiten zuerst messen
Retention Events/Magic Links besitzen eigene Bereinigung; weitere Protokolle wachsen Aufbewahrungs- und Archivregeln pro Datentyp festlegen, einschließlich API-Logs, KI-Audits und Versandprotokollen
Abhängigkeiten npm i statt npm ci in Test-CI; kein Dependency-Auditgate in den betrachteten Workflows Reproduzierbare Installation und gesonderte Security-Prüfung einrichten; Updates nur nach Kompatibilitätsprüfung
Sicherheitsheader Grundheader vorhanden; keine CSP in SecurityHeaders Eine zu Livewire/Alpine und bewusst erlaubten Footer-Scripts passende CSP zunächst beobachten; fehlende CSP allein ist kein XSS-Nachweis
Proxy/Hosts trustProxies('*') und Host-Whitelist vorhanden, einschließlich IP-/localhost-Ausnahmen Reale Proxykette und Erreichbarkeit des Appports prüfen; Schutz vor manipulierten Forwarded-Headern nicht allein aus lokaler Config ableiten
Formulare/Assets Zwei Vite-Bundles und teilweise historische Kommentare zu Alpine Browser-Smokes für Bundlewechsel, Livewire-Navigation, Fehlerscroll und erneute Initialisierung
Große Volt-Komponenten UI und Geschäftslogik eng gekoppelt Schrittweise entlang tatsächlicher Aktionen zerlegen: Teilen, Boost, Nachweis, Korrektur, DSGVO-Anfrage, Abrechnung
PDF-Erzeugung Umfangreicher eigener Renderer Repräsentative Unicode-, Umbruch-, Mehrseiten- und Randfall-PDFs visuell vergleichen; Parser-/Layouttests ergänzen
Betriebliche Nachweise Runbook vorhanden, tatsächliche Cron-/Worker-/Backupzustände nicht geprüft Wiederherstellungstest und Alarmtest mit dokumentiertem Ergebnis; RTO/RPO und Zuständigkeit festlegen

11. Priorisierter Umsetzungsplan

Alle folgenden Schritte sind Empfehlungen für Folgearbeit. In diesem Audit wurde keiner dieser Eingriffe umgesetzt.

Paket A — Identitäts- und Veröffentlichungsgrenzen schließen

Reihenfolge: F01 → F02/F03 → F04 → F07.

  1. Unverifizierte E-Mail als Grundlage für Berechtigungen ausschließen; alten Fortify-Profilpfad konsolidieren.
  2. Google in die gleiche 2FA-Abschlusslogik führen; Sicherheitsaktionen mit erneutem Identitätsnachweis schützen.
  3. Portalübergreifende Abfragen von Soft-Delete-Ausnahmen trennen; Medien-/Vorschauzugriff mitprüfen.
  4. KI-Ergebnisse und Freigaben an eine Inhaltsversion binden.

Abnahmekriterium: Die jeweiligen negativen Regressionstests schlagen vor dem Fix fehl und danach fehlerspezifisch nicht mehr an. Bestehende reguläre Passwort-, Magic-, Google- und Veröffentlichungsflüsse funktionieren weiter. Für F01 zusätzlich eine gezielte Bestandsprüfung automatisch verliehener Rechte planen.

Paket B — Versand und Rechnungslogik stabilisieren

Reihenfolge: F05/F06 → F16 → F14/F15.

  1. Newsletter-Chunking und Eindeutigkeit korrigieren, bevor größere Kampagnen erneut gestartet werden.
  2. Vertragsende im MAN-Auswahl- und Schreibpfad beachten.
  3. Rechnungslauf gegen blockierende unvollständige Altfälle absichern.
  4. Versandstart und Zustellung wiederaufnehmbar machen; Zustellunsicherheit offen behandeln.

Abnahmekriterium: Über 1.000 Empfänger werden vollständig und eindeutig eingeplant; beendete Verträge erzeugen keine unzulässige Folgeperiode; Fehler in einem Datensatz blockieren keine anderen. Keine Tests mit echten Mailadressen oder echten Zahlungen ausführen.

Paket C — Rechtewechsel, Cache und Eingabegrenzen

Umfang: F08–F13 sowie F23.

Rechte erneut pro Action prüfen, Superadmin-Deaktivierung wirksam machen, automatischen Rechteentzug definieren, API-Parameter validieren, Melde-URL korrekt auflösen, öffentliche Caches konsistent invalidieren und Uploadfehler sauber behandeln.

Abnahmekriterium: Tests ändern Rechte, Datenzustand und Storageverfügbarkeit zwischen zwei Requests. Ein ausschließlich erfolgreicher Erstdurchlauf genügt nicht.

Paket D — Scheduler, Queue und Importwiederanlauf

Umfang: F17–F21.

Zeitbudgets harmonisieren, Monatsverbrauch periodisieren, Aggregation gegen gleichzeitige Einstiege schützen, technische Publikationsfehler alarmierbar machen und Importtransaktionen/Batchspeicher korrigieren.

Abnahmekriterium: Wiederholung und Unterbrechung sind ausdrücklich getestete Normalfälle. Ein versäumter oder doppelt gestarteter Hintergrundlauf darf keine stillen fachlichen Mehrfachwirkungen verursachen.

Paket E — Schnelle Optimierungen und Qualitätsgates

Umfang: F22, F24–F28 und ausgewählte Punkte aus Abschnitt 10.

Maßnahme Erwarteter Nutzen Relativer Aufwand / Risiko
Direkter indizierter Firmen-E-Mail-Vergleich nach Kollationsprüfung Konkrete Vermeidung eines Vollscans Klein; Normalisierungsregeln testen
Unter-/Obergrenze für API-Pagination Begrenzter Speicher- und Queryaufwand Klein; API-Fehlervertrag beachten
Technische Schedulerfehler mit Fehlerstatus melden Vorhandene Alarme werden aussagekräftig Klein; fachliche Ablehnungen trennen
Gutschriften erneut autorisieren Rechteentzug sofort wirksam Klein; Lifecycle-Test erforderlich
Lintjob mit verbindlichem Diff-Gate Formatierung des tatsächlichen Commits gesichert Klein
Funktionierender Standard-Devstart Weniger Asset-/HMR-Verwirrung Klein; beide Bundles prüfen
Dokumentationseinstieg aktualisieren Schnellere Orientierung und weniger Doppelarbeit Klein
MySQL-/Worker-/Browserchecks ergänzen Produktionsrelevante Regressionen auffindbar Mittel; gezielt beginnen
Große Volt-Dateien fachlich zerlegen Bessere Prüfbarkeit und statische Analyse Mittel bis groß; kein pauschaler Umbau

Die Aufwandsklassen sind relative Planungswerte, keine belastbaren Zeitschätzungen. Sicherheits- und Datenbestandsfolgen können den Aufwand wesentlich erhöhen.

12. Konkreter Test- und Abnahmeplan

12.1 Höchste Priorität

Testpaket Prüffälle Passende vorhandene Anknüpfung
Identität/Firmenrechte Unverifizierte E-Mail, Rückwechsel, legitime Verifikation, Kontaktentzug Auth/ContactAccessTest.php, CustomerProfileSecurityTest.php, Auth/AccessGatesTest.php
Login/2FA Google mit TOTP, Magic mit TOTP, Passwort mit TOTP, inaktive Konten Auth/GoogleLoginTest.php, Auth/TwoFactorLoginTest.php, Auth/MagicLinkLoginTest.php
Sensitive Actions Recovery-Propertymutation, Disable/Regenerate ohne erneuten Nachweis, Rechteentzug CustomerProfileSecurityTest.php, Admin/AdminGrantTest.php
Löschen/Sichtbarkeit Soft Delete versus Force Delete; PM/Firma/Kontakt; Vorschau/Medien/API PressReleasePreviewTest.php, SecurityHardeningTest.php, Api/V1/PressReleaseApiTest.php
Newsletter Mehrere Chunks, Eindeutigkeit, Parallelstart, Teilabbruch, SMTP-/DB-Fehler Admin/NewsletterCampaignTest.php
MAN-Rechnung Vertragsende, Blockade durch 50 ungültige Fälle, Monatsende/Schaltjahr Billing/ManualInvoiceGenerationTest.php
KI-Inhaltsversion Textänderung während Prüfung, alte Antwort nach neuer Antwort PressReleaseClassificationJobTest.php, PressReleaseContentScoreTest.php

12.2 Produktionsnahe Ergänzungen

  • MySQL: Parallelität mit zwei Verbindungen, echte Zeilensperren, Unique-Konflikte, Slug-/E-Mail-Kollation, Fulltext und EXPLAIN. SQLite-Tests parallel behalten.
  • Queue: Reale Worker mit den vorgesehenen Timeouts und retry_after, künstliche Treiberverzögerung, Prozessabbruch und Wiederanlauf; keine echten externen Kosten verursachen.
  • Browser: Login-/Kontowechsel, Domain-/Bundlewechsel, Mobilnavigation, Fokus im Dialog, Validierungsanzeige und Tastaturbedienung.
  • Betrieb: Testalarm bei technischem Schedulerfehler; Nachweis, dass ein älterer Pending-/Sending- oder Abrechnungsrückstand sichtbar wird.
  • Dateien: Fehlgeschlagene Writes, DB-Abbruch nach Write, sehr große Pixelmaße, abgelaufene/gelöschte Medienlinks.

12.3 Reihenfolge der Prüfungen nach Korrekturen

Zuerst die jeweils betroffenen konkreten Pest-Dateien über Sail ausführen. Danach die betroffenen Fachgruppen, PHPStan und schließlich vor Freigabe die gesamte Suite sowie die ergänzten MySQL-/Worker-/Browserprüfungen. Formatierung nur im Rahmen einer autorisierten Codeänderung durchführen.

Beispielhafte vorhandene Dateiaufrufe:

vendor/bin/sail artisan test --compact tests/Feature/Auth/GoogleLoginTest.php
vendor/bin/sail artisan test --compact tests/Feature/Auth/ContactAccessTest.php
vendor/bin/sail artisan test --compact tests/Feature/CustomerProfileSecurityTest.php
vendor/bin/sail artisan test --compact tests/Feature/PressReleasePreviewTest.php
vendor/bin/sail artisan test --compact tests/Feature/Admin/NewsletterCampaignTest.php
vendor/bin/sail artisan test --compact tests/Feature/Billing/ManualInvoiceGenerationTest.php
vendor/bin/sail bin phpstan analyse --memory-limit=1G --no-progress
vendor/bin/sail composer audit --locked --format=json
vendor/bin/sail npm audit --json

Diese Liste ist ein Folgeplan und keine Behauptung, dass die Befehle hier erfolgreich liefen.

13. Durchgeführte Prüfungen und Ausführungsprotokoll

Prüfung Tatsächliches Ergebnis
Git-Ausgangszustand git status --short zu Beginn leer; geprüfter HEAD oben festgehalten
Dateiinventar und Größen Erfasst; siehe Abschnitt 3.2
Boost application-info Erfolgreich; tatsächliche Paketversionen erfasst
Boost database-schema, Summary Erfolgreich; Tabellen und Spalten lesend betrachtet
Index-/Kollationsmetadaten Erfolgreich; u. a. Empfängertabelle, PM-Slugindex und Firmen-E-Mail
Zwei Firmen-E-Mail-EXPLAINs Erfolgreich; Fullscan versus Indexzugriff, siehe F22
InnoDB-Tabellenschätzungen Erfolgreich; keine exakten Nutzdatencounts, siehe 8.1
Framework-/Livewire-/Fortify-Dokumentation Über Boost abgefragt; sicherheitsrelevante Aussagen zusätzlich am installierten Framework-Code geprüft
Bestehende relevante Tests Lesend geprüft, nicht als bestanden gewertet
Audit-Dokumentprüfung 28 eindeutige Detailbefunde und passende Prioritätszählung; Dateireferenzen und Zeilengrenzen geprüft. Der in F28 bewusst beanstandete historische Pfad ist erwartungsgemäß nicht vorhanden.
Abschließender Git-Zustand Ausschließlich dev/2026-09-11-audit/ neu; keine Änderung an versionierten Anwendungsdateien
vendor/bin/sail artisan route:list --except-vendor --json --no-interaction Exit 1: Docker/Podman nicht verfügbar
vendor/bin/sail up -d Exit 1: Docker/Podman nicht verfügbar
vendor/bin/sail artisan test --compact --filter='GoogleLogin|TwoFactorLogin|ContactAccess|PressReleasePreview|NewsletterCampaign|ManualInvoice' Exit 1 vor Teststart: Docker/Podman nicht verfügbar
vendor/bin/sail bin phpstan analyse --memory-limit=1G --no-progress Exit 1 vor Analyse: Docker/Podman nicht verfügbar
vendor/bin/sail composer audit --locked --format=json Exit 1 vor Advisory-Prüfung: Docker/Podman nicht verfügbar
vendor/bin/sail npm audit --json Exit 1 vor Advisory-Prüfung: Docker/Podman nicht verfügbar
Frontend-Build / Browserlauf Nicht durchgeführt; kein erfolgreicher Laufzeit-/Darstellungsnachweis
Formatter / Migration / Versand / Abrechnung Nicht ausgeführt; kein Anwendungscode verändert

Interpretation: Die Umgebung verhindert dynamische Validierung, nicht die oben beschriebenen statischen Befunde. Umgekehrt beweisen ein großer Testbestand, vorhandene CI-Dateien und installierte Pakete keine aktuelle fehlerfreie Ausführung. Weder „alle Tests grün“ noch „keine Sicherheitslücken in Abhängigkeiten“ wäre auf Basis dieser Sitzung eine zulässige Aussage.

14. Quellen und Nachvollziehbarkeit

14.1 Primäre Projektquellen

  • Routen: bootstrap/app.php, routes/domains.php, routes/auth.php, routes/admin.php, routes/customer.php, routes/api.php, routes/legacy-api.php, routes/web.php, routes/legal.php, routes/console.php.
  • Zugriffsgrenzen: app/Authorization/Permission.php, app/Policies/*, Auth-/Panel-/API-Middleware, Auth-Services und Login-/Verifizierungslistener.
  • Geschäftslogik: app/Services/PressRelease/*, app/Services/Billing/*, app/Services/Newsletter/*, app/Services/Import/*, Jobs und zugehörige Commands.
  • Oberfläche: kritische Volt-Komponenten unter resources/views/livewire, öffentliche Layouts/Komponenten, Vite-Konfiguration, JavaScript und CSS-Einstiegspunkte.
  • Datenmodell: betreffende Eloquent-Modelle, Migrationsdateien und lesend abgefragte MySQL-Metadaten.
  • Entwicklungsqualität: composer.json, package.json, Lockdateien als Bestand, phpunit.xml, phpstan.neon, phpstan-baseline.neon, tests/, .github/workflows/, Git-Historie.
  • Projektentwicklung: dev/README.md, dev/roadmap.md, dev/2026-09-01/, dev/betrieb/deploy-runbook.md, docs/README.md und einschlägige Statusreferenzen.

Datei-/Zeilenverweise gelten für den oben genannten Commit. Historische Kommentare und frühere Auditnummern im Code wurden nicht ungeprüft als aktuelle Tatsachen übernommen.

14.2 Dokumentation zur Gegenprüfung

Die Boost-Suche lieferte bei einer Fortify-Abfrage einen als latest bezeichneten Auszug aus dem 13.x-Dokumentationszweig. Dieser wurde nicht als Beleg einer Laravel-12-spezifischen API übernommen; entscheidend waren für F01/F03 die tatsächlich installierten Fortify-Routen und Actions. Entsprechend wurden auch Pagination und Chunking direkt am installierten Laravel-Code gegengeprüft.

15. Freigabebild nach diesem Audit

Anwendungscode unverändert. Optimierungen wurden ausschließlich beschrieben.

Für eine belastbare technische Freigabe fehlen die Korrektur bzw. explizite Bearbeitung der P1-Befunde und die anschließend erfolgreichen Regressionen in einer ausführbaren Umgebung. Die größte Verbesserung ist nicht zusätzliche Funktionsmenge, sondern ein einheitlicher Vertrag pro fachlicher Operation: eine Identitätsgrundlage, eine Berechtigungsentscheidung, eine Inhaltsversion, eine Löschsemantik und ein wiederholbarer Hintergrundprozess.

Die vorhandenen zentralen Services, Datenbankstrukturen und Tests bieten dafür eine gute Ausgangsbasis. Der empfohlene Weg ist eine Reihe kleiner, fachlich abgegrenzter Änderungen mit jeweils konkretem Fehlernachweis und Regressionstest.