From b3bcc43c4c3a1fa27282e398cb77525001372292 Mon Sep 17 00:00:00 2001 From: Kevin Adametz Date: Fri, 9 Oct 2026 08:49:58 +0000 Subject: [PATCH] dev: Entwicklungsplan Spamschutz, Wiedervorlage der Nacharbeiten nach dem Deploy Co-Authored-By: Claude Opus 5.5 --- .../ENTWICKLUNGSPLAN-SICHERHEIT-2026-10-07.md | 2 + .../ENTWICKLUNGSPLAN-SPAMSCHUTZ-2026-10-09.md | 197 ++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 dev/sicherheit/ENTWICKLUNGSPLAN-SPAMSCHUTZ-2026-10-09.md diff --git a/dev/sicherheit/ENTWICKLUNGSPLAN-SICHERHEIT-2026-10-07.md b/dev/sicherheit/ENTWICKLUNGSPLAN-SICHERHEIT-2026-10-07.md index 20e89f1b..56903dc2 100644 --- a/dev/sicherheit/ENTWICKLUNGSPLAN-SICHERHEIT-2026-10-07.md +++ b/dev/sicherheit/ENTWICKLUNGSPLAN-SICHERHEIT-2026-10-07.md @@ -12,6 +12,8 @@ | **ab 15.10.2026** | CSP-Meldungen auswerten (Melde-Modus seit `v1.6.0`). | Auf Live `tail -n 200 storage/logs/csp-*.log`; fehlende Quellen in `App\Support\ContentSecurityPolicy` ergänzen, danach über das Scharfschalten entscheiden (`'unsafe-inline'`/`'unsafe-eval'` bleiben bis zu einer Nonce-Lösung). | | nächstes Release | Übergangs-Fallback der Sperrmarke entfernen. | `SessionRevocation::legacyCachedMark()` und den Test „a revocation from before v1.6.0 …“ streichen. | | nächstes Release | Liste und Kennzahl „Aktive Sessions“ auf der Sicherheitsseite lesen die Tabelle `sessions`, Live speichert Sessions in Redis — dort immer leer. | Korrigieren oder entfernen; der Knopf „Andere Geräte abmelden“ ist davon unabhängig. | +| nach Deploy Spamschutz | Altbestand und Bot-Konten aufräumen, Datenschutztext freigeben. | Schritte in [ENTWICKLUNGSPLAN-SPAMSCHUTZ-2026-10-09.md](ENTWICKLUNGSPLAN-SPAMSCHUTZ-2026-10-09.md), Abschnitt 0: beide Befehle erst mit `--dry-run`, dann `USERS_PRUNE_BOT_REGISTRATIONS=true`; Abschnitt 6a der Datenschutzerklärung juristisch prüfen lassen. | +| 7 Tage nach Deploy Spamschutz | Wirkung auswerten. | Spam-Filter der Kontakt-Queue, `storage/logs/spam-*.log`, Betriebsüberwachung „Formulare ohne Bot-Prüfung“; Regeln nachschärfen oder lockern. | | bei Gelegenheit | Außerhalb des Audits: eigene MySQL-Benutzer je Projekt auf `global-mysql`, MySQL-Root-Passwort ändern (steht in der Git-Historie), lokalen `APP_KEY` reparieren. | – | **Ausführung:** Befehle direkt im Dev-Container (`php artisan …`, ohne Sail – siehe `.ai/rules/general.md`) diff --git a/dev/sicherheit/ENTWICKLUNGSPLAN-SPAMSCHUTZ-2026-10-09.md b/dev/sicherheit/ENTWICKLUNGSPLAN-SPAMSCHUTZ-2026-10-09.md new file mode 100644 index 00000000..1f76c018 --- /dev/null +++ b/dev/sicherheit/ENTWICKLUNGSPLAN-SPAMSCHUTZ-2026-10-09.md @@ -0,0 +1,197 @@ +# Entwicklungsplan – Spamschutz für öffentliche Formulare + +**Anlass:** Über das Kontakt-Widget kommen täglich Spam-Mails an die Brand-Adressen. Ziel ist, dass Spam gar nicht erst im System landet oder Mails auslöst. + +**Datengrundlage:** lokale Datenbank, Stand 07.10.2026, Zeitraum der letzten 30 Tage. + +**Stand:** 09.10.2026 – Phasen 1 bis 3 umgesetzt (noch nicht released), Entscheidungen am 09.10.2026 getroffen (Abschnitt 4). Umsetzung und Schritte nach dem Deploy: Abschnitt 0. + +## 0. Umsetzungsstand + +| Baustein | Umsetzung | Tests | +| --- | --- | --- | +| B1 Turnstile | `App\Services\Spam\TurnstileVerifier`, Komponente ``, Config `services.turnstile`, CSP um `challenges.cloudflare.com` (script-src, frame-src) ergänzt | `tests/Feature/Web/ContactSpamProtectionTest.php`, `tests/Feature/Auth/RegistrationSpamProtectionTest.php` | +| B2 Inhaltsfilter | `App\Services\Spam\ContactSpamFilter`: fremde Links (Links auf eigene Seiten erlaubt, weil Korrekturwünsche sie nennen), Floskel mit Seitentitel, Zeichensalat, Sperrliste, Absenden unter 3 Sekunden. Nur für Gäste und unbestätigte Konten | wie oben, `tests/Unit/GibberishDetectorTest.php` | +| B3 Quarantäne | Status `ContactMessageStatus::Spam`: keine Mail, nicht im Zähler, kein Verlaufseintrag, Löschung nach 30 Tagen (`model:prune`). Admin: Filter „Spam“, Sammelaktion, „Als Spam markieren“, „Kein Spam“ (holt die Mail einmal nach) | `tests/Feature/Admin/ContactMessagesAdminTest.php` | +| B4 Registrierung | Turnstile und Namensregel (`GibberishDetector::isRandomPersonName`, 0 Fehltreffer unter rund 67.000 Personennamen); Befehl `users:prune-bot-registrations` mit Probelauf, Zeitplan täglich 03:45, aber aus bis `USERS_PRUNE_BOT_REGISTRATIONS=true` | `tests/Feature/Auth/PruneBotRegistrationsTest.php` | +| Altbestand | Befehl `contact-messages:quarantine-spam` mit Probelauf | `tests/Feature/Web/QuarantineContactSpamTest.php` | +| B6 Beobachtung | Log-Kanal `spam` (Grund, Formular, IP-Präfix, nie Inhalte); Betriebsüberwachung: „Spamschutz (Cloudflare Turnstile)“ und „Formulare ohne Bot-Prüfung“ (7 Tage) | `tests/Feature/Admin/SystemHealthTest.php` | +| Datenschutz | Abschnitt 6a „Schutz vor automatisierten Anfragen (Cloudflare Turnstile)“ und Hinweis zur Quarantäne in Abschnitt 6 — **Vorschlag, juristische Freigabe steht aus** | – | + +**Ausfall von Cloudflare (Entscheidung 1, angepasst):** Es wird nie blockiert. Formulare, bei denen Cloudflare nicht erreichbar war, werden angenommen und markiert: +- im Aktivitätsverlauf als Typ „Ohne Bot-Prüfung“; +- in der Kontakt-Queue als Badge „ohne Bot-Prüfung“; +- in der Betriebsüberwachung als Warnung. + +Das gilt in drei Fällen: Siteverify antwortet nicht, Cloudflare lehnt unseren Secret-Key ab, oder das Widget lud im Browser nicht. Den letzten Fall meldet der Browser selbst und könnte deshalb missbraucht werden. Sollte das passieren, schaltet `TURNSTILE_FAIL_OPEN=false` auf Ablehnen um. + +**Messung am Altbestand (lokal, Stand 07.10.):** +- `contact-messages:quarantine-spam` markiert mit den Live-Hostnamen 125 offene Gast-Anfragen. Die echten Korrekturanfragen KT-32, KT-33 und KT-161 bleiben offen. +- Was der Filter nicht erkennt, wird über die Sammelaktion markiert. Das sind vor allem die Preisfragen in wechselnden Sprachen und die Newsletter-Bitten. +- `users:prune-bot-registrations --dry-run` findet 392 Konten. 379 davon haben einen Zufallsnamen, 13 sind echt wirkende, aber seit über 14 Tagen unbestätigte und ungenutzte Konten. + +**Nach dem Deploy auf Live:** +1. Im Cloudflare-Dashboard prüfen, dass das Turnstile-Widget alle drei Live-Hostnamen kennt. Danach einmal das Kontakt-Widget öffnen und absenden. +2. `php artisan contact-messages:quarantine-spam --dry-run` ausführen, die Liste prüfen, dann ohne `--dry-run` ausführen. +3. `php artisan users:prune-bot-registrations --dry-run` ausführen. Nach Freigabe der Liste einmal ohne `--dry-run` ausführen und `USERS_PRUNE_BOT_REGISTRATIONS=true` in die `.env` setzen (`config:cache` läuft im Deploy-Skript). +4. Lokal die Cloudflare-Testkeys in die `.env` eintragen, weil die echten Keys an die Live-Hostnamen gebunden sind. +5. Nach 7 Tagen auswerten: Spam-Filter in der Kontakt-Queue, `storage/logs/spam-*.log` und die Betriebsüberwachung. + +## 1. Befund + +### Kontakt-Widget (`contact_messages`) + +- 141 Anfragen in 30 Tagen, das sind 2 bis 9 pro Tag auf allen drei Portalen. Jede löst eine Mail aus. +- Davon kommen 5 von angemeldeten Kunden (`user_id` gesetzt). Nach Durchsicht der letzten 70 sind das die einzigen echten. Der Rest ist Spam. +- Es gibt fünf Spam-Familien: + +| Muster | Anzahl (30 T.) | Beispiel | +| --- | --- | --- | +| Link im Text (telegra.ph, tinyurl, ``, SEO-Angebote) | 56 | „2026 Rolls-Royce Phantom for $15,000 https://telegra.ph/…“ | +| Floskel plus Meta-Beschreibung der Seite | 43 | „Ich wünsche weitere Informationen. Bitte kontaktieren Sie mich per E-Mail — aktuelle pressemitteilungen aus der deutschen wirtschaft.“ | +| Preisfrage in wechselnden Sprachen | regelmäßig | „Kaixo, zure prezioa jakin nahi nuen.“ (Name „RobertHix“, „Robertfielt“) | +| Zeichensalat | vereinzelt | Name „NAEWTRER2720160NEYRTHYT“, Text „METRYTRE2720160MAWRERGTRH“ | +| Newsletter-Bitte auf Englisch | vereinzelt | „Hi! I'd like to subscribe to your newsletter.“ | + +- **Herkunft:** wenige Netze. `212.30.36.x` allein bringt 32 Anfragen, `158.173.79.x` 13 und `77.90.185.x` 11. Das Limit pro IP (5 pro Stunde) greift nicht, weil die Absender innerhalb des Netzes die IP wechseln. +- **Vorhandener Schutz:** zwei Honeypot-Felder (`website`, `homepage`), ein Limit pro IP und `throttle:10,1`. Das hält diese Bots nicht auf. Sie füllen die Honeypots offenbar nicht aus und laden die Seite vorher (`page_url` ist immer gesetzt). + +### Registrierung – größer als das Kontaktformular + +- 452 neue Konten in 30 Tagen. 389 davon sind Bot-Konten: Name als Zufallsbuchstaben („ruLrlotwaCoVStmZetSxD“), E-Mail nie bestätigt, keiner Firma zugeordnet. Das sind 6 bis 21 pro Tag. +- Echte Neukunden im selben Zeitraum: 59 (bestätigt). +- **Jedes Bot-Konto löst eine Bestätigungsmail an eine fremde Adresse aus.** Darunter sind SMS-Gateways von US-Mobilfunkanbietern (`tmomail.net`, `vtext.com`). Das ist bekanntes Sign-up-Bombing: + - Die Täter nutzen unseren Mailversand, um Postfächer zu fluten. + - Das schadet unserer Absender-Reputation und damit der Zustellung echter Mails. +- Vorhandener Schutz: nur ein Limit pro IP (10 pro Stunde). + +### Übrige öffentliche Formulare + +Diese Formulare haben einen Honeypot und ein Limit. Spam fällt dort noch nicht auf (z. B. 14 Rechtsanfragen in 30 Tagen). Sie verschicken aber Mails an Adressen, die der Absender angibt: + +- Widerruf +- Kündigung +- Meldeformular +- Rechtsanfrage + +## 2. Bausteine + +### B1 – Cloudflare Turnstile (Kern) + +**Konfiguration** +- Neuer Eintrag `services.turnstile` mit `site_key`, `secret_key` und `enabled`. Grund: Wegen `config:cache` darf `env()` nur in `config/` stehen. +- Die Keys stehen bereits in der `.env`. Es sind echte Keys (`0x…`), keine Testkeys. + +**Serverseitige Prüfung** +- Ein Dienst `TurnstileVerifier` ruft `POST https://challenges.cloudflare.com/turnstile/v0/siteverify` auf. +- Mitgesendet werden `secret`, `response` und `remoteip`. +- Geprüft werden `success`, `hostname` (gegen die drei Portal-Domains) und `action` (pro Formular). +- Timeout 3 Sekunden. +- Ein Token gilt nur einmal und höchstens 300 Sekunden. + +**Blade-Komponente ``** +- Das Skript wird nur dort geladen, wo ein Formular sichtbar ist. +- Im Kontakt-Widget lädt es erst beim Öffnen des Panels (`render=explicit`). Normale Seitenaufrufe stellen so keine Verbindung zu Cloudflare her. +- Modus „Managed“: meist unsichtbar, nur bei Verdacht eine Checkbox. + +**Livewire (Registrierung)** +- Den Container mit `wire:ignore` ausnehmen. +- Ein Callback setzt das Token in ein Property. +- Nach einem Validierungsfehler `turnstile.reset()` aufrufen. Sonst scheitert der zweite Versuch am verbrauchten Token. + +**CSP:** `https://challenges.cloudflare.com` muss in `script-src` und `frame-src` aufgenommen werden (`App\Support\ContentSecurityPolicy`). Das gehört vor das Scharfschalten der CSP und deckt sich mit der Wiedervorlage ab 15.10. + +**Tests und lokale Entwicklung** +- `phpunit.xml` leert die Keys. Die Tests nutzen `Http::fake`. +- Lokal kommen die Cloudflare-Testkeys in die `.env` (`1x…` = besteht immer, `2x…` = scheitert immer). Die echten Keys sind an die Live-Hostnamen gebunden. + +**Cloudflare-Dashboard:** Beim Widget müssen alle Hostnamen eingetragen sein: `pressekonto.com`, `presseecho.de`, `businessportal24.com` sowie weitere Live-Domains aus `config/domains.php`. + +### B2 – Inhaltsfilter fürs Kontaktformular (zweite Linie) + +Diese Regeln gelten nur für **nicht angemeldete** Absender. Kunden bleiben ungefiltert. + +- **Links:** URL, `www.` oder `` kommt in diese Formulare: + - Widerruf + - Kündigung + - Meldeformular + - PM-Meldung (Link anfordern und Anliegen) + - Rechtsanfrage + - Newsroom verwalten +- **Kündigung und Widerruf sind gesetzlich vorgeschrieben** (§ 312k BGB, Widerrufsrecht). Ist Cloudflare nicht erreichbar, werden sie dort **trotzdem angenommen** (fail-open) und nur als „ohne Bot-Prüfung“ im Log vermerkt. + +### B6 – Beobachtung + +- Jede abgewiesene Anfrage wird mit Formular, Grund und IP-Präfix gezählt, ohne Inhalt. Dafür ist ein eigener Log-Kanal `spam` vorgesehen, analog zu `csp`. +- Die Betriebsüberwachung bekommt einen Check: In Produktion sind die Turnstile-Keys gesetzt. +- Nach 7 Tagen wird ausgewertet, wie viel durchkommt und wie viel in der Quarantäne liegt. + +## 3. Reihenfolge + +| Phase | Inhalt | Wirkung | +| --- | --- | --- | +| 1 | B1 + B2 + B3 fürs Kontakt-Widget, CSP-Ergänzung | Die täglichen Spam-Mails hören auf. | +| 2 | B4: Turnstile und Namensregel bei der Registrierung | Keine Bestätigungsmails mehr an fremde Postfächer. | +| 3 | B4: Aufräumbefehl für Bot-Konten (erst Probelauf) | Rund 390 Bot-Konten weg, saubere Kennzahlen. | +| 4 | B5: übrige Formulare | Gleicher Schutz überall. | +| 5 | B6: Auswertung nach 7 Tagen | Schwellen nachschärfen oder lockern. | + +Phasen 1 und 2 passen in ein Release. Es ist **keine Migration** nötig: `contact_messages.status` ist eine String-Spalte (`string(40)`), der Status `spam` ist nur ein neuer Enum-Wert. + +## 4. Entscheidungen (09.10.2026) + +Alle wie empfohlen, mit einer Anpassung bei Nr. 1: Bei nicht erreichbarem Cloudflare wird **überall** durchgelassen und markiert, also auch bei Kontaktformular und Registrierung. Nr. 4: Ein Textvorschlag ist eingebaut. + +1. **Cloudflare nicht erreichbar.** Empfehlung: + - Kontaktformular und Registrierung: fail-closed, mit der Meldung „bitte später erneut“. + - Kündigung und Widerruf: fail-open (siehe B5). +2. **Altbestand im Kontakt-Postfach.** Rund 140 offene Spam-Anfragen der letzten 30 Tage nachträglich als Spam markieren? Empfehlung: ja, per einmaligem Befehl mit Probelauf, nach denselben Regeln wie B2. +3. **Bot-Konten löschen** (B4, Phase 3). Empfehlung: ja, aber erst nach deiner Freigabe der Probelauf-Liste. +4. **Datenschutzerklärung.** Turnstile überträgt die IP und Browsermerkmale an Cloudflare (USA, EU-US Data Privacy Framework). Ein Absatz in `datenschutz.blade.php` ist nötig, Rechtsgrundlage Art. 6 Abs. 1 lit. f DSGVO (Missbrauchsschutz). Den Text sollte jemand juristisch freigeben. +5. **Links von Gästen.** Komplett sperren oder nur ab einem Link in Kombination mit einem weiteren Signal? Empfehlung: Gäste dürfen keine Links senden. Echte Anfragen kamen in der Stichprobe ohne Link oder von angemeldeten Kunden. + +## 5. Weitere Ideen (nicht eingeplant) + +- Ein Limit pro /24-Netz zusätzlich zum Limit pro IP. Das würde `212.30.36.x` direkt bremsen, Turnstile macht das aber weitgehend überflüssig. +- Statt einer Mail pro Anfrage eine tägliche Sammelmail für das Kontakt-Postfach. +- Einwegadressen-Domains wie `notboxletters.com` und `belettersmail.com` als kleine Sperrliste. Sie wechseln aber ständig. +- KI-Klassifizierung der Nachricht. Für diese Mengen ist das überdimensioniert. Sinnvoll erst, wenn nach Turnstile noch nennenswert Spam durchkommt.