
Inhalt (15 Abschnitte)
- Der Angriff: Ihr Formular ist die Waffe, nicht das Ziel
- Ohne Access-Log keine Ermittlung
- Chronologie einer Kampagne
- Anatomie des Bots: ein echter Browser, der über seinen Namen lügt
- Warum die klassischen Abwehrmaßnahmen nichts gesehen haben
- Die Abwehr: die Waffe wegnehmen
- Messen statt deuten
- Die Eingabedauer, gemessen mit einer vertrauenswürdigen Uhr
- Das Köderfeld
- Der unstimmige Browser, erkannt an Epochenmerkmalen
- Das Kontrollbild: eine Anwesenheit feststellen statt einer Abwesenheit
- Was wir nicht getan haben, und warum
- Wie es weitergeht
- Die Checkliste für Formulare mit Double Opt-in
- Zum Schluss
An einem Dienstagmorgen, beim Durchsehen der Logs einer Website, die wir hosten, passte eine Zahl nicht: dreizehn Newsletter-Anmeldungen in einer Nacht, bei einer Website, die zwei oder drei pro Woche bekommt. Keine davon war bestätigt worden. Alle trugen Adressen, die nie einen Fuß auf die Website gesetzt hatten: Postfächer bei Gmail, Yahoo, Hotmail, mail.ru, eine Domain eines amerikanischen Unternehmens. Die Website wurde nicht angegriffen: Sie wurde benutzt. Hier lesen Sie, was wir beim Zurückverfolgen der Logs gefunden haben, was wir über den Bot auf der anderen Seite verstanden haben und warum die Abwehr, die am Ende gehalten hat, nicht die ist, die man zuerst einbaut.
Der Angriff: Ihr Formular ist die Waffe, nicht das Ziel #
Der Mechanismus hat einen Namen, Subscription Bombing oder List Bombing, und einen Eintrag bei OWASP (OAT-017, Spamming). Der Angreifer will nichts von Ihrer Website. Was er will, ist die Bestätigungsmail, die Ihre Website verschickt, wenn sich jemand anmeldet. Er nimmt die Adresse seines Opfers (jemand, dessen Bankkonto er gekapert hat oder den er mit E-Mails überschwemmen will) und meldet sie bei Hunderten von Websites mit Double Opt-in an. Jede Website verschickt eine einzige höfliche E-Mail, hält sich für tadellos und hat technisch nichts falsch gemacht. Erst die Summe überwältigt: mehrere Hundert „Bestätigen Sie Ihre Anmeldung“ in einer Stunde, zwischen denen die Warnung der Bank untergeht.
Die Folgen für die als Relais missbrauchte Website sind real und unsichtbar: Jede unerwünschte E-Mail geht von Ihrer Domain aus, signiert mit Ihrem DKIM. Die Empfänger klicken auf „Als Spam melden“. Ihre Absenderreputation sinkt bei Gmail und Microsoft, und eines Tages landen Ihre echten E-Mails (Rechnungen, Bestellbestätigungen) im Spamordner, ohne dass Sie verstehen, warum. Die Beschwerdeschwelle, ab der Gmail beginnt, Sie abzustrafen, liegt bei 0,3 %. Vierzehn unerwünschte E-Mails bei einer Liste von einigen Hundert Adressen reichen dafür bereits aus.
Ohne Access-Log keine Ermittlung #
Erste, etwas peinliche Feststellung: Als das erste Signal auftauchte (zwei Tage zuvor, beim Kontaktformular), hatten wir auf dieser Maschine kein HTTP-Log. Traefik aktiviert es standardmäßig nicht. Wir hatten die Datenbanktabelle der Anwendung (wer hat was wann abgeschickt), mehr nicht: Unmöglich herauszufinden, wie oft der Bot die Seite geladen hatte, ob er die Anti-Spam-Challenge angefordert hatte, ob er das JavaScript lud. Die erste Maßnahme war daher, das Access-Log im JSON-Format mit Rotation zu aktivieren, und dabei festzustellen, dass es auf diesem VPS weder cron noch logrotate gab, eine Falle, in die wir schon auf unseren Hypervisoren getappt waren.
# traefik.yml - Access-Log als JSON, Header ausgeblendet außer dem User-Agent
accessLog:
filePath: /var/log/traefik/access.log
format: json
fields:
headers:
defaultMode: drop
names:
User-Agent: keepAlles Folgende stammt aus dieser Datei. Achtundvierzig Stunden Log, ~220 MB pro Tag (Schwachstellenscanner fallen ins Gewicht), 45 % fehlerhafte Anfragen: das normale Grundrauschen einer öffentlichen IP-Adresse. Und mittendrin zwei klar unterscheidbare Bots.
Chronologie einer Kampagne #
- Tag X-2, 11 Uhr → Mitternacht. Sechs Einsendungen über das Kontaktformular, von Tor-Exit-Knoten. Nachricht in Kauderwelsch, Gmail-Adressen mit zufällig eingefügten Punkten. Die Nachricht interessiert ihn nicht: Es geht um die Eingangsbestätigung, die das Formular an die eingegebene Adresse zurückschickte. Das Kontaktformular war ein offenes Versandrelais.
- Tag X-1, tagsüber. Eingangsbestätigung abgeschafft (das Formular schreibt nur noch an das Büro), Inhaltsfilter, Obergrenze für E-Mails pro Empfänger, HTTP-Log. Der Kontakt-Bot kommt am Abend elfmal zurück: elfmal als Spam eingestuft, null E-Mails. Dieser Angriffsweg ist geschlossen.
- Tag X-1, 17:34 Uhr. Eine Adresse aus einem „Privatanschluss“ (dazu später mehr) lädt die Website, fordert im Newsletter-Banner eine Anti-Spam-Challenge an… und schickt nichts ab. Aufklärung. Sie späht das Formular aus, den Captcha-Typ, die erwartete Antwort.
- Tag X-1, 20:17 Uhr → Tag X, 9:18 Uhr. Zwanzig Newsletter-Einsendungen, eine alle zehn bis dreißig Minuten, von acht verschiedenen IP-Adressen, verteilt auf zwei /16-Blöcke, die ein großer europäischer Telekommunikationsanbieter im Routing bekannt gibt, die aber in /22-Blöcken an einen Dritten mit exotischem Registrierungsland weitervergeben sind: der Fingerabdruck eines gemieteten Pools von Residential Proxies. Bei erweiterter Suche haben fünfundvierzig Adressen aus diesen beiden Blöcken die Website in dreiundvierzig Stunden besucht, mit tausend Anfragen pro Stunde: ein Drittel des gesamten Traffics. Vierzehn Bestätigungsmails gehen an Unbekannte. Fünf werden von der Obergrenze blockiert, weil der Bot eine Adresse wiederverwendet hat.
- Tag X, 9:30 Uhr. Inbetriebnahme der Schleuse (dazu gleich). Der Bot klopft weiter an. Er bekommt nichts mehr.
Anatomie des Bots: ein echter Browser, der über seinen Namen lügt #
Die Frage, die man sich sofort stellt: Ist das ein Skript, das die API aufruft, oder ein Browser? Die Antwort steckt in Menge und Art der Anfragen. Ein direkter Aufruf des Formular-Endpunkts käme mit drei Anfragen aus: Challenge holen, lösen, absenden. Jede Sitzung des Bots erzeugt sechzig bis siebzig, und es sind Anfragen, für die ein Skript weder einen Grund noch ein Mittel hätte.
20:17:16 GET / Startseite
20:17:16 GET /_next/static/chunks/…js ×18 JS- und CSS-Bundles
20:17:17 GET /api/cours-statut von einem useEffect der Seite aufgerufen
20:17:17 GET /tarifs?_rsc=5CB68i4… ┐
20:17:17 GET /planning?_rsc=5CB68i4… │ 41 Prefetches in < 1 s,
20:17:17 GET /contact?_rsc=5CB68i4… │ mit ZWEI verschiedenen _rsc-Tokens
… GET /…?_rsc=tXyZJ52U… ┘
20:17:19 GET /api/altcha/challenge vom Anti-Spam-Widget angefordert
20:17:21 POST /api/newsletter Proof of Work gelöst, gültigVier Indizien, nach abnehmender Beweiskraft. Zuerst die Prefetches ?_rsc=: Der Client-Router von Next.js lädt die Seiten sichtbarer Links vorab, indem er ihren Payload als React Server Components anfordert, mit einem Token, das die Laufzeitumgebung selbst aus dem Komponentenbaum erzeugt. Wir sehen einundvierzig davon, mit zwei Tokens (zwei Layout-Bäume), in genau der Reihenfolge, in der Next sie ausgibt. Ein Skript kennt weder diesen Mechanismus noch diese Tokens. Dann die Anti-Spam-Challenge: Sie wird vom Element <altcha-widget> angefordert, sobald es eingebunden ist (bei diesem Banner nur beim Fokus auf das E-Mail-Feld), und die eingereichte Lösung ist kryptografisch gültig. Die Bundles /_next/static/, nutzlos für jeden, der die Seite nicht rendert. Und /api/cours-statut, aufgerufen von einem useEffect: Der Bot des Kontaktformulars, der nach dem Absenden zwanzig Seiten crawlt, löst ihn auf jeder davon aus, zum richtigen Zeitpunkt, und nur dort, wo der Code der Seite ihn aufruft.
Dann der Vergleich mit den einzigen beiden Menschen im Zeitraum: einer Besucherin mit iPhone und dem Administrator der Website mit Firefox. Das Bild ist eindeutig.
- Null Bilder in den fünf Bot-Sitzungen, sechzehn bis zwanzig in den beiden menschlichen. Vorsicht vor dem voreiligen Schluss: Ein Headless-Browser lädt Bilder standardmäßig, darauf beruhen die Screenshots von Playwright. Null Bilder bedeutet, dass das Skript sie absichtlich blockiert (
page.route('**/*.{png,jpg,webp}', r => r.abort())), die Optimierung Nr. 1 jedes Scrapers. Das ist eine Entscheidung des Angreifers, also ein verräterisches Merkmal. - Kein Favicon. Der alte Headless-Modus von Chromium (und die
chromium-headless-shell, die Playwright seit Version 1.49 standardmäßig nutzt) fordert nie das Favicon an; der neue Headless-Modus (seit Chrome 132 der einzige verbliebene, der von Puppeteer) fordert es an, und zwar so zuverlässig, dass sich Nutzer darüber wie über einen Bug beschweren. Das spricht für Playwright, ohne es zu beweisen: Beide Werkzeuge hinterlassen denselben HTTP-Fingerabdruck. - Kein Cache von einer Sitzung zur nächsten. Dieselbe IP-Adresse kam in fünfzehn Stunden achtmal wieder und lud die achtzehn JavaScript-Dateien jedes Mal neu, ohne ein einziges 304. Das ist die Signatur eines
browser.newContext()pro Durchgang: frischer Kontext, leere Cookies, leerer Cache. - HTTP/2 und TLS 1.3 überall. Der Netzwerkstack eines echten Chromium. Ein
python-requestswürde HTTP/1.1 sprechen. - Ein User-Agent, der lügt. „MSIE 6.0“, „AOL 9.5 / Windows 98“, „PPC Mac OS X“, „Arora/0.4 auf WebKit 523“, zufällig aus einer Liste gezogen, in jeder Sitzung ein anderer. Ein Browser von 2005 kann weder Next.js ausführen noch einen Proof of Work in einem Web Worker lösen. Das ist ein echtes Chromium, das über seinen Namen lügt, kein Skript, das ein Chromium nachahmt.
Ein letztes Detail, aufschlussreicher als alles andere: Der Bot tastet sich heran. Seine ersten drei Einsendungen kommen 1,5 Sekunden nach der Challenge. Dann, im Lauf der Nacht, geht er auf vierzehn, achtzehn Sekunden: eine „vermenschlichte“ Verzögerung, im laufenden Betrieb hinzugefügt. Am nächsten Morgen fällt er auf 0,9 Sekunden zurück. Er hat keinerlei Rückmeldung erhalten, die ihm gesagt hätte, was funktioniert, also probiert er. Merken Sie sich diesen Punkt: Wir haben ihm nie etwas gesagt, und genau deshalb verbrennt er weiter nutzlos seine IP-Adressen.
Warum die klassischen Abwehrmaßnahmen nichts gesehen haben #
Die Website war nicht ungeschützt. Sie hatte ein Captcha mit Proof of Work (ALTCHA, dessen Wahl wir hier erklärt haben), eine Obergrenze für E-Mails pro Empfänger, eine Ratenbegrenzung pro IP-Adresse und CrowdSec im Beobachtungsmodus. Jede dieser Schichten wurde umgangen, nicht durch List, sondern weil keine für diese Angriffsform ausgelegt war.
- Der Proof of Work ist eine Mengen-Barriere: Er macht zehntausend Einsendungen pro Minute teuer. Gegen eine Einsendung alle zwanzig Minuten richtet er nichts aus, und ein ferngesteuerter Browser löst das Rätsel genau wie Ihr eigener. Den Rechenaufwand zu erhöhen, würde nur Ihre Besucher bestrafen: Der Bot hat Zeit.
- Die Obergrenze pro Empfänger (eine Bestätigungsmail pro Adresse und pro 24 h) schützt ein bestimmtes Opfer vor Wiederholung. Der Bot verwendet bei jedem Versuch eine andere Adresse. Sie hat nur seine fünf Wiederholungen blockiert.
- Das Rate-Limit pro IP (fünf Anfragen pro Viertelstunde) setzt voraus, dass der Angreifer von einem Ort kommt. Er kommt von acht, mit je einer Anfrage.
- CrowdSec sucht nach Scans, Ausnutzungsversuchen, Brute Force. Es hat in siebzehn Stunden 115 Warnungen gesehen (alle von echten Scannern) und null zu den acht Adressen des Bots. Ein sauberer
POST, der mit 200 antwortet, einmal pro IP, ähnelt nichts, was es kennt. Seine Community-Liste mit 17.000 Adressen enthielt keine der acht: Residential-Adressen, nie gemeldet.
Die Abwehr: die Waffe wegnehmen #
Wir haben uns für die dritte Möglichkeit entschieden. Seit jenem Morgen verschickt eine Anmeldung über das öffentliche Formular keine E-Mail mehr. Sie wird in einem Zwischenzustand gespeichert („zu prüfen“), mit allen Spuren, die die Einsendung hinterlassen hat, und eine Person im Büro entscheidet im Backoffice, ob die Bestätigungsmail freigegeben wird. Zwei nicht verhandelbare Regeln in diesem Konzept:
- Die Person im Büro bestätigt nie anstelle des Besuchers. Sie löst nur den Versand aus. Als Einwilligung gilt immer der Klick des Empfängers auf den Link: Der DSGVO-Nachweis bleibt unangetastet. Ein Bestätigungstoken ist nichts wert, solange die E-Mail nicht verschickt ist: Taucht es vorher auf, ist es durchgesickert.
- Die HTTP-Antwort ändert sich nicht. Der Bot erhält dasselbe
202 Acceptedwie vorher. Nichts verrät ihm, dass er in einer Schleuse steckt. Siehe oben: Ein Angreifer ohne Rückmeldung passt nichts an.
// Anmelde-Handler (Auszug). Der Modus ist eine Einstellung, kein Umbau:
// Newsletter__ValidationManuelle=false stellt den sofortigen Versand unter Obergrenze wieder her.
sub.RenseignerSignaux(request.Ip, request.UserAgent,
request.DelaiSoumissionMs, request.PiegeRempli);
if (parametres.ValidationManuelle)
{
sub.MettreEnAttenteValidation(); // Status AValider, keine E-Mail
await uow.SaveChangesAsync(ct);
return Unit.Value; // dasselbe 202 wie sonst
}
// … bisheriger Pfad: Obergrenze pro Empfänger, dann VersandDrei Umsetzungsdetails, die wichtiger sind, als sie aussehen. Die Einstellung ist standardmäßig aktiv, auch ohne die Umgebungsvariable: Ein versehentlich ausgerolltes Image wird nicht wieder zum Relais. Die E-Mail geht vor der Zustandsänderung raus, und der Zustand ändert sich nur, wenn die E-Mail verschickt wurde: Schlägt der Versanddienstleister fehl, bleibt die Anfrage mit angezeigtem Fehler in der Schleuse, statt dass eine Zeile behauptet, eine E-Mail sei verschickt worden. Und eine erneute Einsendung derselben Adresse erzeugt weder neue Tokens noch verschickt sie etwas: Sie aktualisiert nur die Spuren. Ein Architekturtest erfasst per Reflection die zwanzig Klassen, die eine E-Mail verschicken können, und verlangt, dass jede als „öffentlich“ (also mit Obergrenze) oder „intern“ deklariert ist: Wer einen Versand hinzufügt, ohne sich festzulegen, lässt die CI scheitern.
Messen statt deuten #
Eine Schleuse nützt nur, wenn die Person, die sie bedient, in zwei Sekunden entscheiden kann. Sonst sieht sie nur Gmail-Adressen und gibt am Ende die E-Mail des Bots selbst frei. Also mussten bei jeder Einsendung Signale erfasst werden, und nicht irgendwelche: Signale, die der Server prüfen kann, keine Angaben des Browsers.
Die Eingabedauer, gemessen mit einer vertrauenswürdigen Uhr #
Das ALTCHA-Widget liefert ein Feld took (Lösungsdauer). Es wird vom Client angegeben: wertlos. Aber die Challenge selbst ist vom Server datiert und signiert: expiresAt = Ausgabe + 30 min, unter HMAC. Beim Prüfen der Lösung erhält man den Ausgabezeitpunkt zurück und damit eine Dauer „Challenge → Einsendung“, die der Client nicht rückdatieren kann. Ein Mensch braucht mehrere Sekunden, um eine Adresse einzutippen, nachdem das Widget seine Challenge angefordert hat; der Bot hat bei vier seiner Versuche in 0,9 bis 1,7 Sekunden abgeschickt.
// Serverseitige ALTCHA-Prüfung (Next.js Route Handler, Auszug)
const { error, payload } = await server.verify(altcha, deriveKey, secret, keySecret, usedChallenges);
const expiresAt = payload?.challenge?.parameters?.expiresAt; // Unix-Sekunden, signiert
const emisA = Number.isFinite(expiresAt) ? expiresAt * 1000 - CHALLENGE_TTL_MS : null;
const delaiSoumissionMs = emisA ? Math.max(0, Date.now() - emisA) : null;Das Köderfeld #
Ein Feld site_web, außerhalb des Bildschirms, außerhalb der Tabulatorreihenfolge, unsichtbar für Screenreader. Ein Mensch sieht es nie; ein Bot, der „alle Felder ausfüllt“, trägt dort etwas ein. So alt wie das Web, kostenlos und gegen generische Werkzeuge immer noch wirksam, unwirksam dagegen gegen ein Skript, das das E-Mail-Feld gezielt über seine ID anspricht, was hier wahrscheinlich der Fall ist. Wir setzen es trotzdem ein: Es kostet nichts und sortiert einen Teil des Rauschens aus.
Der unstimmige Browser, erkannt an Epochenmerkmalen #
Erste Version: eine Liste von Namen (MSIE, AOL, BonEcho). Sie ließ zwei Zeilen des Bots mit „Arora/0.4 auf WebKit 523“ durch. Zweite Version: Wir suchen nicht mehr nach Namen, sondern nach Epochenmerkmalen: ein WebKit unter 536 (2011; ein modernes Chrome liegt bei 537.36, Safari bei 605), ein Gecko 1.x, das 2011 aufgegebene Token ; U;. Ohne Fehlalarm geprüft mit Firefox 154, Chrome 140, Safari iOS 18, Android und macOS. Das Prinzip: Ein Browser, der Ihre Anwendung ausführt, kann nicht der sein, der er zu sein behauptet, wenn dieser älter ist als die APIs, die er gerade benutzt hat.
const UA_ARCHAIQUE =
/MSIE [5-9]\.|AOL \d|Windows 98|Windows NT [45]\.|PPC Mac OS X|BonEcho|Acoo Browser|Arora\/|FunWebProducts
|AppleWebKit\/(?:[1-4]\d\d|5[0-2]\d|53[0-5])\b|rv:1\.\d|; U;/i;Das Kontrollbild: eine Anwesenheit feststellen statt einer Abwesenheit #
Der Bot lädt keine Bilder. Man wäre versucht, daraus ein Blockierkriterium zu machen, das wäre ein Fehler: Ein Besucher, der wiederkommt, hat die Bilder im Cache und fordert ebenfalls keine an, genau wie der Bot; ganz zu schweigen vom Datensparmodus, von Textbrowsern, von Linkvorschauen. Das Fehlen einer Anfrage beweist nichts. Der richtige Ansatz dreht das Problem um: Wir geben dem Browser ein Bild zum Laden, dessen Identität wir kennen. Ein unsichtbares Bild von einem Pixel, dessen Adresse die ID der Anti-Spam-Challenge trägt: eindeutig, also nie im Cache. Ein echter Browser lädt es, ohne darüber nachzudenken; ein Bot, der Bilder blockiert, wird es nie anfordern. Bei der Einsendung prüft der Server, ob das Kontrollbild dieser Challenge geladen wurde. Falls nicht: Signal „Bilder nicht geladen“, neben den anderen. Serverseitig prüfbar, an das konkrete Formular gebunden, unsichtbar für den Bot, dreiundvierzig Byte. Und passt sich der Bot an, indem er Bilder lädt, zahlt er zwanzig Dateien mehr pro Sitzung: genau das, was er vermeiden wollte.
Was wir nicht getan haben, und warum #
- Anhand des Favicons sperren. Browser cachen es sehr lange; Firefox fordert es von einem Besuch zum nächsten nicht neu an. Der Administrator der Website an jenem Morgen: kein Favicon. Er wäre als Erster gesperrt worden.
- Den Aufwand des Proof of Work erhöhen. Das vervielfacht die Rechenzeit jedes legitimen Besuchers, für einen Bot, der die ganze Nacht Zeit hat.
- ALTCHA durch Turnstile ersetzen. Seien wir ehrlich: Turnstile hätte wahrscheinlich genau diesen Bot gestoppt, so wie er läuft: Ein Chromium, das sich als MSIE 6 ausgibt, ist der Lehrbuchfall dessen, was es erkennt. Aber nicht diese Art von Angriff: Ein Bot, der seine Verzögerungen schon im Lauf der Nacht anpasst, passt seinen User-Agent mit einer Zeile an,
playwright-stealthverschleiert den Rest, und Lösungsfarmen verkaufen gelöste Turnstile-Challenges für einen Dollar pro tausend. Er hätte eine Nacht verloren, nicht die Kampagne, und wir hätten Cloudflare als Auftragsverarbeiter auf allen Seiten wieder eingeführt, was wir bewusst entfernt hatten. - Eine globale Obergrenze am Edge. Vor der Schleuse war das die richtige Maßnahme: ab drei Anmeldungen pro Stunde über alle Quellen hinweg ablehnen. Mit der Schleuse geht keine E-Mail mehr raus, eine Begrenzung der Einsendungen schützt also nichts mehr, und es ist der einzige Hebel, der am Abend einer Ankündigung echte Besucher abweisen kann.
- Den CrowdSec-Bouncer unverändert aktivieren. Siebzehn Stunden Beobachtung zeigten 115 berechtigte Warnungen… und einen gravierenden Fehlalarm: Das Prefetching
?_rsc=der Listen im Backoffice, als Crawl gewertet, hätte den Administrator im Lauf des Vormittags zweimal gesperrt. Einen Bouncer richtet man ein, nachdem man gelesen hat, was er getan hätte, nie vorher.
Wie es weitergeht #
Die Schleuse neutralisiert den Schaden. Bleibt dafür zu sorgen, dass die Versuche gar nicht mehr bis zu ihr gelangen. Zwei sofortige Hebel: das stille Verwerfen bei starken Signalen (Köderfeld ausgefüllt oder unstimmiger Browser: dasselbe 202, nichts gespeichert, eine Logzeile, kein Fehlalarm möglich, keine Rückmeldung an den Bot) und das Blockieren am Edge der beiden /16-Blöcke, nur auf den Formularrouten: fünf Minuten YAML, wirksam, solange er nicht den Proxy-Pool wechselt. Dann, als eigenes Vorhaben, der CrowdSec-Bouncer, beschränkt auf die POST-Anfragen der Formulare, nachdem das Crawl-Szenario eingestellt und das Nötige auf die Allowlist gesetzt ist. Und mittelfristig ein lokaler Risiko-Score, der diese Signale kombiniert, um die sauberen Anfragen selbstständig zu sortieren, für den Tag, an dem die Schleuse überläuft.
Die Checkliste für Formulare mit Double Opt-in #
- Vor allem ein HTTP-Access-Log. Ohne Log werden Sie nie erfahren, was passiert ist. JSON, Rotation, und prüfen Sie, ob tatsächlich etwas die Rotation ausführt.
- Behandeln Sie jeden E-Mail-Versand an eine Adresse, die ein Unbekannter eingegeben hat, als potenzielles Relais. Eingangsbestätigung beim Kontakt, Newsletter-Bestätigung, Magic Link, Warenkorb-Erinnerung: Listen Sie sie auf, begrenzen Sie sie, und lassen Sie die CI scheitern, wenn jemand einen hinzufügt, ohne ihn zu deklarieren.
- Eine globale Obergrenze für Anmeldungen über alle Quellen hinweg, oder eine Schleuse, wenn das Volumen es zulässt.
- Gemessene Signale: Zeitspanne seit einer signierten Challenge, Köderfeld, Kontrollbild. Einem Menschen angezeigt, nicht in automatische Sperren verwandelt.
- Sagen Sie dem Bot nie, dass er gescheitert ist. Gleicher Antwortcode, gleicher Inhalt, gleiche Verzögerung. Schweigen ist Ihr bester Verbündeter: Ein Angreifer ohne Rückmeldung verschwendet seine Ressourcen weiter ins Leere.
- Überwachen Sie Ihre Beschwerdequote bei Google Postmaster und Microsoft SNDS. Dort zeigt sich, was Subscription Bombing Sie kostet, Wochen später.
Zum Schluss #
Niemand hat diese Website „gehackt“. Ein gewöhnliches Formular, gebaut nach bewährten Praktiken, hat genau das getan, wofür es gebaut war (eine Bestätigungsmail verschicken), vierzehnmal, im Auftrag eines anderen. Das ist der häufigste Fall in der Websicherheit und der unspektakulärste: eine legitime Funktion, genutzt von der falschen Person, im falschen Maßstab. Die Abwehr war kein Produkt. Sie bestand darin, die Logs bis zum Ende zu lesen, zu verstehen, worauf der Angreifer aus war, und es ihm wegzunehmen, ohne es ihm zu sagen.
Genau solche Nächte bestimmen, was wir unter ernsthaften Managed Services verstehen: kein grünes Dashboard, sondern jemand, der dreizehn Anmeldungen bemerkt, wo es zwei sein sollten. Ein Formular, das Ihnen Sorgen macht, eine Domain, deren Zustellbarkeit ohne erkennbaren Grund nachlässt? Sprechen wir darüber.



