presseportale/docs/weiteres/Sicherheit & Deployment-Hinweise (Auth, Rollen, Verifizierung).md
Kevin Adametz 512057cbfd Security: 2FA-Bypass beheben & Login-Pfade konsolidieren
Befund (Review 16.06.): Der Volt-Login machte direkt Auth::attempt() und umging
Fortifys 2FA-Pipeline (2FA-Bypass); zusätzlich existierte der Fortify-POST /login
parallel mit schwächeren Post-Login-Regeln.

Fix (Volt-nativ):
- Volt-Login prüft Credentials ohne sofortiges Login; bei aktivem 2FA wird der
  Session-Vertrag login.id/login.remember gesetzt und auf eine neue Volt-
  2FA-Challenge-Seite (/two-factor-challenge) geleitet, die an Fortifys
  bestehenden Controller postet (TOTP + Recovery-Code).
- Gemeinsame Post-Login-Logik in App\Support\LoginRedirect (rollengerechtes
  Home + 403-sicherer intended-Redirect), genutzt von Volt-Login UND Response.
- RoleAwareLoginResponse implementiert jetzt LoginResponse UND
  TwoFactorLoginResponse und erzwingt einheitlich: unverifiziert → Notice,
  verifiziert-inaktiv → Logout+Fehler, sonst 403-sicherer Redirect. Damit ist
  auch der direkte Fortify-POST-Pfad gehärtet.

Tests: 2FA-Übergabe, Challenge-Guard, voller TOTP-Flow, Fortify-POST blockt
inaktive User und hält Customer aus dem Admin-Bereich.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 10:00:15 +00:00

8 KiB
Raw Blame History

Version: Juni 2026 · Datum: 16.06.2026 · Status: Umgesetzt Scope: Sicherheits- und deployment-kritische Punkte aus der Auth-/Rollen-/Verifizierungs-Phase (WS-6 & WS-2). Dieses Dokument bündelt, was beim Go-Live zwingend beachtet werden muss. Feed für die spätere Live-Deployment-Checkliste (WS-7).


1. ⚠️ SICHERHEIT — Legacy-Rollen-Überberechtigung (behoben)

Befund: Der Legacy-Import (app/Services/Import/UserImporter.php) mappte die Alt-Gruppe 2 auf die neue Rolle editor. Die Alt-Gruppe 2 war jedoch die normale Self-Publisher-Masse (65.950 von 66.151 Usern) – keine Redaktions-Mitarbeiter. Im neuen Rollenmodell gewährt editor Admin-Panel-Zugriff (User::canAccessAdmin() prüft hasRole(['admin','editor'])).

Auswirkung: Praktisch jeder Legacy-Kunde konnte sich – insbesondere über den Magic-Link-Login – ins komplette Admin-Panel einloggen.

Fix:

  • Mapping korrigiert: Alt-Gruppe 2 → customer (statt editor). editor wird ab jetzt ausschließlich manuell an echtes Personal vergeben.
  • Daten-Migration 2026_06_16_080913_downgrade_legacy_editor_users_to_customer stuft die bestehenden Legacy-editor (alle mit legacy_portal) auf customer herab. Chunked (Placeholder-Limit), nicht umkehrbar.
  • Ergebnis nach Migration: editor = 0, customer ≈ 65.952.

Deployment / Nachkontrolle:

  • Migration MUSS auf Prod laufen.
  • Es bleiben 4 Legacy-Admins (Alt-Gruppe 1) – manuell prüfen, ob das wirklich Personal ist.
  • Kontrolle nach Deploy: Spatie\Permission\Models\Role::findByName('editor')->users()->whereNotNull('legacy_portal')->count() muss 0 sein.

2. ⚠️ DEPLOYMENT-KRITISCH — E-Mail-Verifizierung scharf

User implementiert jetzt MustVerifyEmail; die verified-Middleware auf den Panel-Routen greift damit real (war vorher wirkungslos).

Lockout-Gefahr: Ohne Vorsorge würden ALLE Bestands-User mit email_verified_at = NULL (alle 66.151, davon ~59.403 aktiv) ausgesperrt.

Absicherung: Migration 2026_06_15_101337_backfill_email_verified_at_for_existing_users setzt email_verified_at für alle bestehenden User.

Deployment: Diese Migration MUSS mit dem Deploy laufen, bevor echter Traffic auf die geschützten Routen kommt. Neue Registrierungen (nach Deploy) bleiben absichtlich NULL, bis verifiziert.

Registrierungs-Verhalten (Entscheidung 15.06.):

  • Selbst-Registrierung legt ein inaktives, rollenloses Konto an → Danke-/Notice-Seite → Verifizierungsmail.
  • Bestätigter Link → is_active = true + customer-Rolle (App\Listeners\ActivateUserAfterVerification).
  • Ausnahme-Kanäle (gelten als verifiziert, kein E-Mail-Schritt): Magic-Link, Kontaktzugang (WS-2), Google-Login (geplant).

3. VERHALTENSÄNDERUNG — Firmen-Scope für Pressemitteilungen

PM-Zugriff war hart an user_id (Autor) gebunden. Jetzt additiv: Zugriff = Autor ODER Mitglied der zugeordneten Firma (Owner via owner_user_id oder company_user-Pivot).

  • Betroffen: PressReleasePolicy::canManage() (view/update/submitForReview/delete) sowie die Queries der Kunden-Komponenten (Liste/Show/Edit) und User::accessibleCompanyIds().
  • Solo-Owner: keine sichtbare Änderung (sie sind Autor ihrer PMs).
  • Firmen mit mehreren Mitgliedern: Mitglieder sehen/bearbeiten jetzt alle PMs der Firma (gewollt – „gesamte Firma").

Öffentliches Formular /pressekontakt-zugang (auth.contact-access):

  • Enumeration-sicher (neutrale Antwort unabhängig vom Treffer), Honeypot + Rate-Limit.
  • Eine hinterlegte Kontakt-E-Mail → lazy angelegter, per E-Mail de-duplizierter customer-Account (aktiv, verifiziert), der den Firmen seiner Kontakte als Mitglied zugeordnet wird.
  • Versand über den bestehenden Login-Magic-Link (MagicLinkGenerator + MagicLinkConsumeController) – keine Schema-Änderung.

Design-Nuance (bewusst): Der Account wird beim Absenden der Anfrage angelegt (nicht erst beim Klick), um eine fragile magic_links-Schema-Änderung (ENUM/FK über SQLite-Tests + MySQL-Prod) zu vermeiden. Er ist „lazy" (nur für real existierende Kontakt-Mails) und ohne den per Mail versendeten Token wertlos.


5. Guest-Redirect rollenbewusst (403-Sackgasse behoben)

bootstrap/app.php → redirectUsersTo: eingeloggte User auf Gast-Routen (/login, /register) werden rollen-/verifizierungsbewusst geleitet (Admin → /dashboard, Customer → /admin/me, unverifiziert → Notice). Verhindert, dass ein Customer auf das feste /dashboard (403) läuft und festsitzt.

Ergänzend: Magic-Link-Consume nutzt einen rollensicheren Redirect ohne intended() (eine als Gast besuchte Admin-URL darf einen Customer nicht in den 403-Bereich schicken).


Aus einer gezielten Auth-Prüfung umgesetzt:

  • Magic-Link-Versand rate-limited (vorher ungedrosselt): pro E-Mail+IP (3/h) und zusätzlich pro IP (15/h) – verhindert Mail-Fluten aktiver Accounts und das laufende Entwerten alter Links. Der Pressekontakt-Zugang war bereits limitiert.
  • Inaktive (aber verifizierte) User werden beim Passwort-Login zentral blockiert (Auth::logout() + Fehler), analog zum Magic-Link-Consume. Schließt nur-auth+verified-Routen ohne zusätzlichen is_active-Check ab.
  • Rollensicherer Login-Redirect: eine gemerkte intended-Admin-URL (z. B. /admin/users) schickt einen Customer nicht mehr in den 403 – inaktzessible Admin-Pfade fallen auf das rollengerechte Ziel zurück.
  • ContactAccess mutiert keine deaktivierten Bestands-Accounts mehr: der is_active-Check läuft vor jeder Firmenzuordnung/jedem Linkversand.
  • Magic-Link-Verbrauch ist atomar (konditionales UPDATE … whereNull(consumed_at)): Single-Use auch bei parallelen Requests garantiert.

Captcha (dokumentiert, vorerst NICHT eingebaut): Beim Pressekontakt-Zugang greifen aktuell Honeypot + Rate-Limit. Ein echtes Captcha (Cloudflare Turnstile / hCaptcha) ist sinnvoll, wird aber erst eingebaut, falls im Livebetrieb zu viel Missbrauch auftritt (Entscheidung 16.06.) – braucht Provider-Entscheidung, Keys und ein Paket.

6b. 2FA-Bypass behoben & Login-Pfade konsolidiert (Review 16.06., Teil 2)

Befund: Der sichtbare Volt-Login machte direkt Auth::attempt() und umging damit Fortifys 2FA-Pipeline (RedirectIfTwoFactorAuthenticatable) – für Accounts mit aktivem 2FA faktisch ein 2FA-Bypass. Zusätzlich existierte der Fortify-POST /login parallel mit schwächeren Post-Login-Regeln (kein is_active-Block, resolveTarget neutralisierte nur / und /dashboard).

Fix (Volt-nativ):

  • Der Volt-Login prüft die Zugangsdaten jetzt OHNE sofortiges Einloggen; bei aktivem 2FA (hasEnabledTwoFactorAuthentication()) wird der Session-Vertrag login.id/login.remember gesetzt und auf eine neue Volt-2FA-Challenge-Seite (/two-factor-challenge) geleitet. Diese postet an Fortifys bestehenden two-factor-challenge-Controller (TOTP + Recovery-Code).
  • Gemeinsame Post-Login-Logik in App\Support\LoginRedirect (rollengerechtes Home + 403-sicherer intended-Redirect), genutzt von Volt-Login UND der Fortify-Response.
  • RoleAwareLoginResponse implementiert jetzt beide Contracts (LoginResponse und TwoFactorLoginResponse) und erzwingt einheitlich: unverifiziert → Notice, verifiziert-inaktiv → Logout+Fehler, sonst 403-sicherer Redirect. Damit ist auch der direkte Fortify-POST-Pfad gehärtet.

Hinweis: fortify.views => false bleibt; die Challenge wird vom Volt-Frontend bereitgestellt, die Verifizierung übernimmt Fortify.


7. Deployment-Reihenfolge (Migrationen dieser Phase)

  1. 2026_06_15_101337_backfill_email_verified_at_for_existing_users — verhindert Lockout.
  2. 2026_06_16_080913_downgrade_legacy_editor_users_to_customer — schließt die Admin-Überberechtigung.

Nach Deploy prüfen:

  • editor-Rolle hat keine Legacy-User mehr.
  • Niemand ungewollt ausgesperrt (Stichprobe aktiver Legacy-User → Login/Panel erreichbar).
  • Die 4 verbliebenen Legacy-Admins sind legitimes Personal.