Warum Spam mein ratenbegrenztes Kontaktformular besiegte
Warum kommt Spam durch ein Kontaktformular mit Rate Limit? Weil ein Rate Limit Anfragen pro Adresse zählt und nichts darin fragt, wer sie geschickt hat. Am 30. Oktober und am 2. November durchkämmten zwei Schwachstellenscanner meine Website, luden wie jeder Besucher die Startseite, auf der das Formular steht, holten sich ein gültiges CSRF-Token und schickten das Formular ab, bis sie weiterzogen oder gesperrt wurden. Am ersten Tag gingen 101 Übermittlungen durch, am zweiten 31, jede davon eine 200 der App, die sie für einen Versand zurückgibt und ebenso für einen fehlgeschlagenen; diese Zahlen sind also die Höchstzahl an E-Mails, die dabei entstehen konnten.12 Der Limiter meiner App, eingestellt auf fünf Übermittlungen pro Minute und Adresse, wies 830 beziehungsweise 31 ihrer Übermittlungen ab und ließ 101 und 31 Versände durch, 47 und 9 mehr, als ein exakter Limiter bei dieser Einstellung erlaubt hätte.3 Das Rate Limit an der Edge griff bei keinem der beiden Scanner: Ich legte es etwa 12 Minuten nach der letzten Formularübermittlung der ersten Flut an, und die Übermittlungen des zweiten Scanners kamen über zwei Cloudflare-Rechenzentren, die getrennte Zähler führen. Diese Aufteilung ist meine stärkste Erklärung für das zweite Schweigen: Sie passt zu den Minutensummen, die sie nicht beweisen können.456 Auf der Website meines Studios fing ein Honeypot-Feld in acht Tagen 42 von 52 Übermittlungen ab und ließ die 10 durch, deren Absender den Honeypot leer gelassen hatten.78 Die Edge dieser Website hatte durchaus eine Regel, die fragen kann, ob ein Mensch da ist, eine Managed Challenge auf dem Pfad des Formulars, doch sie greift nur bei einem gelisteten User-Agent-String, und beide Scanner schickten den String eines Browsers; eine zweite Challenge, an einen hohen Threat Score geknüpft, konnte nie auslösen, weil Cloudflare diesen Score inzwischen auf null setzt, und so stellte nichts diese Frage an jede Übermittlung.910411 Seit dem Morgen des 3. November stellt Turnstile sie auf beiden Formularen jeder Übermittlung, die mein Postfach erreichen könnte.12
Kurzfassung
- Zwei Scanner, ein Postfach. Eine Adresse schickte meiner Website am 30. Oktober 76.429 Anfragen in 53 Ein-Minuten-Buckets, eine andere am 2. November 10.907 in 22. Beide durchkämmten die gesamte Website, beide schickten SQL-Injection-Strings in Pfaden und im User-Agent-Header, und beide schickten das Kontaktformular ab: 1.065-mal am ersten Tag und 500-mal am zweiten.131412
- Ein gültiges CSRF-Token beweist nur, dass ein Client ein Token besaß, das meine Website ausgegeben hat. Jeder Versand an beiden Tagen trug eines, denn meine Tokens sind zustandslos, und jeder Client, der die Seite abruft, bekommt eines. Die CSRF-Prüfung wies in zwei Fluten vier Übermittlungen ab.1516
- Mein eigener Limiter war undicht. Er liest einen Zähler aus Cloudflare KV, vergleicht ihn, liest ihn dann erneut und schreibt in einer separaten Anfrage den Zählerstand plus eins; ein fehlgeschlagener zweiter Lesevorgang schreibt eins, und ein fehlgeschlagener KV-Aufruf fällt auf einen Zähler im Speicher eines einzelnen Prozesses zurück. Jeder dieser Pfade kann mehr als fünf durchlassen, und die Logs können nicht sagen, welcher es war: In einer Minute gingen 18 Versände hinaus, bei einem Limit von fünf.171
- Das Edge-Rate-Limit zählt pro Adresse und pro Rechenzentrum. Am 2. November kamen die 85 Formularübermittlungen des Scanners zwischen 18:56 und 19:05:00 Uhr UTC, also in den Minuten rund um die Sperre, über Amsterdam und London, 44 und 41, und die geschäftigste Minute teilte sich 18 zu 14 zwischen beiden auf. Diese Aufteilung ist der wahrscheinlichste Grund, warum die Regel still blieb, auch wenn Minutensummen das nicht beweisen können. Eine Messung, die ich am nächsten Morgen durchführte, bewies, dass die Regel auslöst und dass Cloudflare das protokolliert.518
- Die Sperren waren Adresssperren, und beide kamen zu spät, um die E-Mails fernzuhalten. Am 30. Oktober sperrte ich die Adresse 3,9 Minuten, nachdem ich es bemerkt hatte, von Hand, etwa 50 Minuten nach ihrer ersten Formularübermittlung. Am 2. November sperrte mein Monitor sie 8 Minuten nach der ersten Minute des Scans. An keinem der beiden Tage kam ein Versand nach der Sperrminute: Am 30. Oktober kam der letzte mehr als 12 Minuten vor der Sperre, und am 2. November kamen 26 in den Minuten davor und 5 in der Minute der Sperre selbst, die sich anhand von Minutendaten nicht vor oder nach der Sekunde der Sperre einordnen lassen.101912
- Der Honeypot fängt Bots, die jedes Feld ausfüllen. Auf der Website meines Studios beantwortete er 42 von 52 Übermittlungen, als wären sie erfolgreich gewesen, und speicherte nichts; nach der Stunde ihres Eintreffens gehörte jede Übermittlung von einem Tor-Exit dazu. Die 10, die gespeichert wurden, und damit höchstens 10 E-Mails, stammten von Absendern, die den Honeypot leer gelassen hatten.720
- Was sich geändert hat. Beide Formulare tragen jetzt ein Cloudflare-Turnstile-Widget im Managed-Modus, und der Server speichert oder versendet nichts, bevor die siteverify-API von Cloudflare das Token der Übermittlung akzeptiert; ein fehlendes, übergroßes oder nicht verifizierbares Token weist er ab. Turnstile führt Challenges im Browser des Besuchers aus und verarbeitet Signale wie IP-Adresse, TLS-Fingerprint und User-Agent; laut Cloudflare berührt es keine Formulareingaben. Es hält keinen Menschen auf, der Spam von Hand tippt, und es ersetzt die Rate Limits nicht, die weiterhin das Volumen begrenzen.122122
1. Die Frage: „Bist du das?“
Am 2. November füllte sich mein Postfach mit Nachrichten aus dem Kontaktformular, und meine erste Frage an die Agenten-Session, die meine Infrastruktur betreibt, lautete, ob meine eigene Automatisierung sie verschickte. Das tat sie nicht. Ein Scanner tat es, über das Kontaktformular dieser Website, und das Formular auf der Website meines Studios, 941apps.com, bekam ein eigenes langsames Tröpfeln ab, auch wenn davon an jenem Tag nichts im Postfach ankam.238 Alle drei Mailströme, die Alarme meines Monitors und die beiden Formulare, versenden von einer Adresse auf meiner eigenen Maildomain, im Postfach sahen sie also gleich aus.23
Die Betreffzeilen verrieten alles. Der Kontakt-Handler dieser Website schickt mir jede Übermittlung mit dem Betreff „Contact Form:“, gefolgt von dem, was der Absender in die Namensfelder getippt hat, und so wird die Payload eines Scanners zur Betreffzeile.2423 Das Formular der Studio-Website speichert jede Nachricht in einer Datenbank und mailt sie dann an dasselbe Postfach.25
Die Frage, die ich zuerst stellte, war die falsche, und die richtige zu beantworten kostete einen Tag voller Abfragen: Welche der Kontrollen an diesen Formularen hätte einen Scanner von einem Menschen unterscheiden können, und warum ließ jede den Rest durch? Es gab fünf Arten von Kontrollen. Auf dieser Website eine Edge-Firewall mit Custom Rules, ein Edge-Rate-Limit, ein CSRF-Token und einen Rate Limiter in der App. Auf der Studio-Website dieselbe Art von Edge-Regeln, ein Edge-Rate-Limit und einen eigenen App-Limiter, der keine der beobachteten Formularübermittlungen abwies, sowie ein Honeypot-Feld.9257 Der Rest dieses Beitrags nimmt sich die beiden Fluten und das langsame Tröpfeln einzeln vor, dann die fünf Schichten einzeln, jeweils mit dem Regeltext oder dem Verhalten des Codes.
Woher die Zahlen stammen
Jede Zahl im Folgenden stammt aus Cloudflares Request-Analytics und Firewall-Events für die beiden Zonen, abgefragt am 3. November über bereits abgeschlossene Zeitfenster, dazu aus der Nachrichtentabelle der Studio-Website und dem eigenen Log meines Monitors.16 Ein Skript machte aus diesen gespeicherten Abfrageergebnissen ein Ledger von einigen hundert Zeilen, jede mit der Regel, die sie erzeugt hat, und der Beitrag bezieht seine Zahlen aus diesem Ledger und seinem Nachtrag und nirgendwo sonst. Wo meine Notizen vom Tag dem Ledger widersprechen, gilt das Ledger: Die Notizen vom 30. Oktober schrieb ich wenige Minuten nach der Flut, als Cloudflare die letzten Minuten noch einspielte, und sie zählen zu wenig.1626 Cloudflare bewahrt diese Datensätze für „at least 31 days of data on every plan“ (mindestens 31 Tage an Daten in jedem Tarif) auf, ich konnte eine Flut also nachträglich zählen, aber nicht beliebig lange.27
Zwei Arten von Zahlen brauchen einen Warnhinweis. Firewall-Events sind eine Stichprobe, daher nutze ich sie dafür, welche Regel wann gehandelt hat, und die Request-Datensätze dafür, wie viele es waren.16 Und eine Formularübermittlung, die auf dieser Website mit 200 beantwortet wurde, ist ein Versand oder ein Fehlschlag, den der Handler ebenfalls mit 200 beantwortet (er hat zwei solche Fehlerpfade), keine zugestellte E-Mail, und das Log meines Mailanbieters habe ich nicht geprüft. Jede E-Mail-Zahl in diesem Beitrag ist daher eine Obergrenze.2428
Wie der Normalzustand aussah
Vor den Fluten war es um das Formular ruhig. In den acht Tagen, die die Abfragen abdecken, schickten drei andere Adressen als die beiden Scanner das Formular erfolgreich ab, jede genau zweimal, eine Adresse pro Tag am 27., 28. und 29. Oktober.29 Ich weiß nicht, ob das Menschen waren. Zwei Übermittlungen von einer Adresse passen zu einem Doppelklick, und sie passen zu einem Bot, der es erneut versucht. Diese Ungewissheit ist das ganze Problem im Kleinen: Nichts in den Logs oder den Kontrollen konnte es sagen.
2. Flut eins: 30. Oktober, von Hand gesperrt
Die erste Flut kam von einer Adresse in einem Netz, das die regionale Internet-Registry unter BULLIONET FBW NETWORKS SAS führt, registriert in Frankreich.30 Ihre erste Anfrage fiel in die Minute 17:59 Uhr UTC, 10:59 Uhr pazifischer Sommerzeit, ihre letzte in die Minute 18:51 Uhr, 11:51 Uhr PDT: 53 Ein-Minuten-Buckets und 76.429 Anfragen an diese Website. Zwei unabhängige Abfragen, eine pro Minute für die Adresse und eine über alle Zonen meines Kontos, stimmen in dieser Summe überein, und die Adresse schickte davor und danach nichts.13
Die Gestalt des Scans
Das Formular war nur ein kleiner Teil davon. Vor der Sperre kam die Adresse im Schnitt auf 1.389,7 Anfragen pro Minute, mit einem Spitzenwert von 3.219 in der Minute 18:36, und sie lief die gesamte Website ab: mindestens 2.881 verschiedene Pfade, eine Untergrenze, weil die Pfadabfrage an ihr Zeilenlimit stieß.1331 Über den gesamten Scan verteilten sich die Ergebnisse so:13
| Ergebnis | Anfragen | Anteil |
|---|---|---|
| Ausgeliefert (2xx, 3xx) | 49.513 | 64,8% |
| Von der App abgewiesen (4xx), darunter 10.019 vom Agenten-Gate der Website | 18.990 | 24,8% |
| Origin-Fehler (520) | 2.339 | 3,1% |
| An der Edge blockiert (403) | 5.587 | 7,3% |
Die 7,3% der Edge sind fast vollständig die Sperre: 5.234 ihrer 5.587 Abweisungen kamen nach der Sperrminute.13 In den fünfzig Minuten davor handelte nur die Exploit-Pfad-Regel. Diese Regel wirkte ab der ersten Minute, mit dem ersten Firewall-Event um 17:59:53 Uhr UTC, doch sie prüft auf Pfad-Teilstrings wie .php und /wp-, und dieser Scan fragte größtenteils echte Seiten ab.109
Was ihn zu einem Scan statt zu einem bloßen Crawl machte, war das, was er in die Anfragen packte. Fast der gesamte Traffic, mindestens 69.322 Anfragen, nutzte einen einzigen schlichten Chrome-141-User-Agent für Windows. Mindestens 2.755 weitere gaben sich als Googlebot aus, und die Verified-Bot-Regel der Edge behandelte sie nicht als Google.32 Mindestens 3.154 Anfragen trugen eine SQL-Injection-Payload im User-Agent-Header selbst, in mindestens 396 verschiedenen Strings, und mindestens 2.680 Anfragen trugen eine im Pfad, verteilt auf mindestens 810 verschiedene Pfade.3231 Die Payloads gehören zu dokumentierten Familien. PG_SLEEP(15), waitfor delay '0:0:15' und XOR(if(now()=sysdate(),sleep(15),0)) sind zeitbasierte Sonden: Das Cheat Sheet von PortSwigger listet die Verzögerungssyntax für Oracle, Microsoft SQL Server, PostgreSQL und MySQL, und die eigenen Payload-Definitionen von sqlmap enthalten dieselben Funktionen.3334 PortSwigger nennt den Grund, warum sie funktionieren: „As SQL queries are normally processed synchronously by the application, delaying the execution of a SQL query also delays the HTTP response.“ (Da die Anwendung SQL-Abfragen normalerweise synchron verarbeitet, verzögert eine verzögerte Ausführung der Abfrage auch die HTTP-Antwort.)35 Eine einzelne langsame Antwort beweist wenig; eine Verzögerung von etwa 15 Sekunden, die jedes Mal auftritt, wenn die Payload sie verlangt, und bei Kontrollanfragen ohne Payload ausbleibt, ist ein Indiz für eine mögliche Injection.
Die Mal-App auf dieser Website hat einen Share-Endpunkt, und der Scanner fand ihn. Er schickte 320 POSTs an den Share-Pfad, als einzige Adresse, die in den acht abgefragten Tagen dorthin postete, und der Endpunkt wies alle 320 ab und speicherte nichts. Drumherum postete er an Varianten des Pfads mit eingebauter Payload, etwa /paint/share'||DBMS_PIPE.RECEIVE_MESSAGE(CHR(98)||CHR(98)||CHR(98),15)||' und /paint/(select 198766*667891 from DUAL), die die Website mit 404 beantwortete.31 Die Adresse berührte außerdem eine weitere Zone meines Kontos, mit 387 GETs an kiradex.app, alle mit 200 beantwortet, und keine sonst.13 Meine Lesart ist, dass sie Links von dieser Website zu jener folgte; die Links habe ich nicht geprüft.
Der OWASP-Katalog automatisierter Bedrohungen hat einen Namen für diese Aktivität: OAT-014, Vulnerability Scanning, „Crawl and fuzz application to identify weaknesses and possible vulnerabilities.“ (Die Anwendung crawlen und fuzzen, um Schwächen und mögliche Schwachstellen zu finden.)36 Das Kontaktformular war nur eine weitere Eingabe zum Fuzzen.
Das Formular, Minute für Minute
Die Adresse schickte das Kontaktformular 1.065-mal ab, und jede Übermittlung erreichte die App; die Edge hielt keine davon auf.1 Die erste Übermittlung und der erste Versand fielen in die Minute 18:00 Uhr UTC, 11:00 Uhr PDT. Hier ist jede Minute, in der eine Formularübermittlung die App erreichte, nach dem Status, den die App zurückgab:1
| UTC | PDT | Versendet (200) | CSRF abgewiesen (403) | Feld fehlt (422) | Limiter abgewiesen (429) |
|---|---|---|---|---|---|
| 18:00 | 11:00 | 8 | |||
| 18:01 | 11:01 | 12 | 133 | ||
| 18:02 | 11:02 | 9 | 148 | ||
| 18:03 | 11:03 | 4 | 4 | 200 | |
| 18:04 | 11:04 | 9 | 89 | ||
| 18:05 | 11:05 | 16 | 164 | ||
| 18:06 | 11:06 | 18 | 85 | ||
| 18:07 | 11:07 | 7 | 11 | ||
| 18:08 | 11:08 | 2 | |||
| 18:09 | 11:09 | 4 | |||
| 18:10 | 11:10 | 9 | |||
| 18:11 | 11:11 | 1 | |||
| 18:12 | 11:12 | 1 | |||
| 18:35 | 11:35 | 1 | |||
| 18:36 | 11:36 | 1 | |||
| 18:37 | 11:37 | 129 |
Die Summen: 101 versendet, 830 vom Limiter der App abgewiesen, 130 abgewiesen, weil ein Pflichtfeld fehlte, und 4 von der CSRF-Prüfung abgewiesen, alle vier in der Minute 18:03.1 Die 429er stammten von der App. Die Rate-Limit-Regel an der Edge existierte noch nicht, für die Adresse gibt es kein Rate-Limit-Firewall-Event, und wo Cloudflare für diese Übermittlungen den Status des Origin aufzeichnete, kam die 429 vom Origin.1037 Die 422er kamen aus der Formularvalidierung von FastAPI, die vor dem Limiter läuft, sie verbrauchten also nie einen Platz im Limiter; die Reihenfolge prüfte ich mit einer zehnzeiligen FastAPI-App, in der eine Übermittlung ohne Pflichtfeld 422 zurückgab, ohne dass der Wrapper je aufgerufen wurde.38 Der Schwall von 129 um 18:37 sieht aus, als hätte der Scanner die Formularfelder gefuzzt, indem er sie wegließ.
Lesen Sie die Spalte „Versendet“ gegen den Limiter. Der Limiter erlaubt fünf Übermittlungen pro Adresse und Uhrminute, und in acht dieser Minuten gingen mehr als fünf hinaus, um 18:06 sogar 18.13 Abschnitt 4 erklärt, wie das kam.
Die Entdeckung und die Sperre
Ich bemerkte es um 11:46 Uhr PDT und notierte, dass etwas immer wieder das Kontaktformular der Website traf.26 Zu diesem Zeitpunkt lag die erste Formularübermittlung etwa 46 Minuten zurück, und die Formularflut selbst hatte um 11:37 Uhr PDT geendet.1 Ich setzte die Adresse von Hand auf die kontoweite Sperrliste, die die erste Custom Rule der Zone bereits abgleicht, und das erste Event dieser Regel kam um 18:49:53 Uhr UTC, 11:49:53 Uhr PDT.10 Die Sperre kam 3,9 Minuten, nachdem ich es bemerkt hatte, etwa 50 Minuten nach der ersten Formularübermittlung und 50,9 Minuten nach der ersten Minute des Scans.113 Alle 101 Versände waren vorher hinausgegangen; die letzte Formularübermittlung, die die App erreichte, kam etwa 12 Minuten vor der Sperre.1 Nach der Sperrminute schickte die Adresse weitere 5.234 Anfragen, und die Edge wies jede einzelne ab.13
In derselben Sekunde, 18:49:53 Uhr UTC, legte ich die einzige Rate-Limiting-Regel der Zone an: Formular-POSTs an die Pfade für Kontakt und Mal-Share, 3 pro 10 Sekunden pro Adresse, mit einer Sperre von 10 Sekunden.9 Um 18:52:38 Uhr UTC testete ich sie von meinem eigenen Rechner aus. Über sechs Übermittlungen im Abstand von einer Sekunde, mit einem Browser-User-Agent und ohne gültiges CSRF-Token, heißt es in meinen Notizen zu diesem Test, sie „returned 403 403 403 429 429 429 (the app’s CSRF rejection, then the edge’s rate limit)“ (403 403 403 429 429 429, also erst die CSRF-Abweisung der App, dann das Rate Limit der Edge), und schlichte curl-Übermittlungen erreichten die Regel gar nicht, weil eine bestehende Custom Rule ihnen stattdessen eine Managed Challenge auslieferte.26 Meine Notizen sagen, die Übermittlungen hätten kein Token getragen, doch eine Übermittlung ohne dieses Feld erhält eine 422, bevor der Handler läuft, wie meine Messung am 3. November zeigte; die 403er bedeuten also ein Token, das vorhanden, aber ungültig war.18 Die Firewall-Events stimmen damit überein: drei Rate-Limit-Events von meiner Testadresse, das erste in genau jener Sekunde, und 24 Managed Challenges für meine curl-Übermittlungen.39 Keiner meiner Test-Requests steckt in irgendeiner der obigen Zahlen.
Mein Monitor lief an jenem Tag nicht. Ich nahm ihn am 31. Oktober wieder in Betrieb, und sein Traffic-Detektor kam erst nach dieser Flut hinzu.19 Die Sperre vom 30. Oktober war ein Mensch, der sein Postfach las.
30. Oktober: 76.429 Anfragen von einer Adresse, pro Minute; bis ich die Adresse von Hand sperrte, wies die Edge fast keine davon ab.1310
3. Flut zwei: 2. November, von meinem Monitor gesperrt
Die zweite Adresse gehört zu NovoServe B.V., einem Hosting-Anbieter in den Niederlanden.40 Sie schickte um 07:49 Uhr UTC, 23:49 Uhr pazifischer Standardzeit am 1. November, zwei GETs und dann 11,1 Stunden lang nichts. Der eigentliche Scan lief von der Minute 18:56 Uhr UTC bis zur Minute 19:17 Uhr, 10:56 bis 11:17 Uhr PST: 22 Ein-Minuten-Buckets und 10.907 Anfragen, mit den beiden frühen GETs 10.909. Vier unabhängige Abfragen stimmen in der Summe überein.14
Es sah nach demselben Werkzeug aus. Die Adresse nutzte für 9.725 Anfragen denselben Chrome-141-Windows-User-Agent-String wie die Adresse vom 30. Oktober, für 942 denselben Googlebot-String, dieselben Injection-Familien, und sie berührte dieselben zwei Zonen und keine sonst, mit 368 GETs an kiradex.app.414214 Zwei Adressen in zwei Netzen in zwei Ländern beweisen nichts darüber, wer sie betrieb, also belasse ich es bei „sah danach aus“.
Der Ablauf, in beiden Zeitzonen
| UTC | PST | Was geschah |
|---|---|---|
| 18:56 | 10:56 | Der Scan beginnt: 29 Anfragen in dieser Minute.1419 |
| 18:57:00 | 10:57:00 | Ein Lauf meines Monitors startet. Sein Zeitfenster schließt um 18:57:22, er sieht also etwa eine Minute des Scans und findet nichts.19 |
| 18:57 | 10:57 | Die geschäftigste Minute vor der Sperre, 637 Anfragen. Die Exploit-Pfad-Regel der Edge beginnt um 18:57:11 zu blockieren.144 |
| 18:58 | 10:58 | Die ersten drei Formularübermittlungen. Die App versendet alle drei.2 |
| 19:00 | 11:00 | Vier weitere Formularübermittlungen, alle vier versendet.2 |
| 19:02 | 11:02 | 32 Formularübermittlungen erreichen die App: 9 versendet, 23 vom Limiter der App abgewiesen.2 |
| 19:02:46 | 11:02:46 | Der nächste Lauf des Monitors startet.19 |
| 19:03 | 11:03 | 10 versendet, 8 abgewiesen.2 |
| 19:03:53 | 11:03:53 | Mein Monitor meldet einen kritischen Befund zu einer Anfrageflut: 1.914 Anfragen von der Adresse.19 |
| 19:04:01 | 11:04:01 | Mein Monitor setzt die Adresse auf die Kontosperrliste.19 |
| 19:04:03 | 11:04:03 | Das erste Event der Sperrlisten-Regel.4 |
| 19:04 | 11:04 | Die letzten fünf Versände und 22 an der Edge abgewiesene Formularübermittlungen, alle in der Minute der Sperre selbst; der Minuten-Bucket verrät nicht, was zuerst kam.2 |
| 19:07 | 11:07 | Die geschäftigste Minute des Scans, 1.107 Anfragen, alle blockiert.14 |
| 19:17 | 11:17 | Die letzte Minute, 300 Anfragen, alle blockiert.14 |
Die acht Minuten meines Monitors
Mein Monitor meldete seinen ersten Befund 7,9 Minuten nach der ersten Minute des Scans, und die Sperre griff 8,0 Minuten danach, also 6,8 Minuten nach dem ersten Firewall-Event.194 Bis dahin waren 26 Versände in den Minuten vor der Sperre hinausgegangen, und die letzten 5 fielen in die Minute der Sperre selbst, die sich anhand der Minutendaten nicht vor oder nach der Sekunde der Sperre einordnen lassen; in den folgenden Minuten kam keiner mehr.2 Acht Minuten sind schnell für eine Sperre und langsam für ein Postfach.
Der Lauf, der um 10:57:00 Uhr PST startete, hatte den Scan etwa eine Minute lang im Blick, und in seinem Zeitfenster stellte die Adresse irgendwo zwischen 29 und 666 Anfragen; der obere Wert zählt die gesamte Minute 18:57, von der das Zeitfenster nur 22 Sekunden sah. Der Detektor protokollierte keinen Befund.19 Der nächste Lauf, um 11:02:46 Uhr PST, fand die Flut und sperrte sie, und genau dieser Lauf zeigt, warum der erste sie verpassen konnte. Sein Zeitfenster endete in dem Moment, in dem er Cloudflare abfragte, bevor die neuesten Events eingetroffen waren: Sein Log verzeichnet 1.914 Anfragen von der Adresse, während meine Wiederholung desselben Fünf-Minuten-Fensters am 3. November auf vollständigen Daten 2.143 zählt; die Wiederholung ist mit ihren Befehlen und der vollständigen Ausgabe gespeichert, und ihre Zahlen sind Zeilen im Nachtrag des Ledgers.1943 Die Wiederholung löst außerdem einen Befund zu einer Formularflut aus, den der Live-Lauf nie meldete, aus 39 Übermittlungen, davon 23 abgewiesen, mit hohem Schweregrad, was eine Zeile im täglichen Digest gewesen wäre und weder eine E-Mail noch eine Sperre.43 Ich habe das Zeitfenster des zweiten Laufs wiederholt, nicht das des ersten, für den Lauf um 10:57 Uhr PST ist dieselbe Verzögerung also die wahrscheinliche Ursache und keine gemessene. Noch am selben Tag änderte ich die Zeitfenster des Monitors so, dass sie 90 Sekunden vor jedem Lauf enden und dort beginnen, wo das vorige endete.43 Insgesamt lösten sechs Befunde zu der Adresse aus, und mein Monitor leitete jeden davon in seinen täglichen Digest um, statt mir zu mailen, weil die Adresse bereits in seinem Sperr-Ledger stand.19 Wie mein Monitor alarmieren sollte, und dieser Scanner aus seiner Sicht, ist Thema eines separaten Beitrags; für diesen hier zählt, dass eine Sperre eine Reaktion ist, und die Reaktion kam nach mindestens 26 der 31 Versände.
Das Formular, nach Status
Die Adresse schickte das Formular 500-mal ab.2
| UTC | PST | Versendet (200) | Limiter abgewiesen (429) | Edge blockiert (403) |
|---|---|---|---|---|
| 18:58 | 10:58 | 3 | ||
| 19:00 | 11:00 | 4 | ||
| 19:02 | 11:02 | 9 | 23 | |
| 19:03 | 11:03 | 10 | 8 | |
| 19:04 | 11:04 | 5 | 22 | |
| 19:05 | 11:05 | 39 | ||
| 19:06 | 11:06 | 19 | ||
| 19:07 | 11:07 | 10 | ||
| 19:08 | 11:08 | 13 | ||
| 19:09 | 11:09 | 31 | ||
| 19:10 | 11:10 | 41 | ||
| 19:11 | 11:11 | 47 | ||
| 19:12 | 11:12 | 97 | ||
| 19:13 | 11:13 | 29 | ||
| 19:14 | 11:14 | 23 | ||
| 19:15 | 11:15 | 21 | ||
| 19:16 | 11:16 | 28 | ||
| 19:17 | 11:17 | 18 |
Die Summen: 31 versendet, 31 vom Limiter der App abgewiesen und 438 an der Edge abgewiesen, alle in oder nach der Sperrminute. 62 Übermittlungen erreichten also die App.2 Jeder Versand beweist, dass der Scanner ein gültiges CSRF-Token besaß, weil der Handler das Token vor dem Versand prüft. Die 429er beweisen in keine Richtung etwas, weil der Limiter eine Übermittlung abweist, bevor der Handler ihr Token liest.2416 Wo Cloudflare für diese 429er den Status des Origin aufzeichnete, war es der des Origin, also des App-Limiters, und für die Adresse gibt es kein Rate-Limit-Firewall-Event.374
Der Rest des Scans
Nach Ergebnis verteilte sich der Scan vom 2. November so:14
| Ergebnis | Anfragen | Anteil |
|---|---|---|
| Ausgeliefert (2xx, 3xx) | 1.388 | 12,7% |
| Von der App abgewiesen (4xx), darunter 420 vom Agenten-Gate | 1.473 | 13,5% |
| Origin-Fehler (520) | 116 | 1,1% |
| An der Edge blockiert (403) | 7.930 | 72,7% |
Die 72,7% der Edge sind größtenteils die Sperre: 2.986 Anfragen kamen vor der Sperrminute und 7.445 danach, und von den späteren lieferte die Website keine aus.14 Die Pfadabfrage war an diesem Tag vollständig: 2.173 verschiedene Pfade, und 778 Anfragen auf 542 davon trugen eine SQL-Injection-Payload.44 Auch die User-Agent-Abfrage war vollständig, 73 verschiedene Strings. Neben dem schlichten Chrome-String und dem Googlebot-Anspruch schickte der Scanner 54 verschiedene SQL-Payloads in 149 Anfragen, drei anführungszeichenbrechende Strings in 78 und 12 Marker-Tokens, jedes einmal.41
Einige dieser Strings lohnen einen genaueren Blick, weil sie zeigen, was der Scanner fragte. (select 198766*667891 from DUAL) ist ein arithmetischer Ausdruck in einem Select ohne Tabelle; FROM dual ist die Oracle-Syntax dafür, wie das Cheat Sheet von PortSwigger zeigt.33 if(now()=sysdate(),sleep(15),0) hat dieselbe Form IF(condition, SLEEP(n), x) wie die bedingte Zeitverzögerung auf demselben Cheat Sheet.33 @@qkLpP ist ein @@-Präfix, dasselbe, das das Cheat Sheet in SELECT @@version für Microsoft und MySQL verwendet, gefolgt von fünf Zeichen, und der Scanner schickte ein Dutzend ähnlicher.3341 Der Testleitfaden von OWASP benennt die beiden Techniken, denen diese Strings dienen: fehlerbasierte Injection, die „forces the database to generate an error“ (die Datenbank zwingt, einen Fehler zu erzeugen), und Zeitverzögerungen, die „use database commands (e.g. sleep) to delay answers in conditional queries.“ (Datenbankbefehle wie sleep nutzen, um Antworten in bedingten Abfragen zu verzögern.)45 Keine der Seiten, die ich abgerufen habe, dokumentiert genau diese Strings, also beschreibe ich sie und schreibe sie keinem bestimmten Werkzeug zu.
2. November: 10.907 Anfragen von einer Adresse, pro Minute, mit den beiden Läufen meines Monitors und der Sperre, die er um 11:04:01 Uhr PST setzte.1419
4. Die fünf Schichten und was jede unterscheiden konnte
Fünf Arten von Kontrollen standen zwischen diesen Bots und meinem Postfach. Jeder stellte ich dieselbe Frage: Konnte sie einen Bot von einem Menschen unterscheiden? Eine Managed Challenge in den Edge-Regeln kann diese Frage einem Besucher stellen, aber nur Anfragen, deren User-Agent-String auf ihrer Liste steht, und die der Scanner standen nicht darauf; eine zweite, an einen Threat Score geknüpft, konnte nie auslösen, und so stellte nichts diese Frage an jede Übermittlung. Zwei der Kontrollen zählen, eine gleicht Namen und Listen ab, eine beweist, dass ein Client ein Token besitzt, das die Website ausgegeben hat, und eine fängt nur die Bots, die ein verstecktes Feld ausfüllen. Hier ist jede einzelne: was sie prüft, was sie aufhielt und der genaue Grund, warum sie den Rest durchließ.
Edge-Custom-Rules
Was sie prüfen. Auf dieser Website blockiert die erste Custom Rule jede Adresse auf einer kontoweiten Liste sowie sieben Hosting-Netze per ASN. Eine weitere blockiert Exploit-Pfad-Teilstrings wie .php, /wp- und /.env. Zwei weitere liefern auf dem Kontaktformular und dem Admin-Pfad eine Managed Challenge aus: die eine bei einem leeren User-Agent oder einem, der curl, wget, python-requests, httpx, scrapy, HeadlessChrome, PhantomJS oder Selenium enthält, die andere bei einem hohen Cloudflare-Threat-Score. Die Studio-Website hat dieselben Arten von Regeln, mit beiden Challenges websiteweit und einer Sperre auf API-Pfaden, die das Kontaktformular ausnimmt.9 Die Threat-Score-Challenges, eine auf jeder Website, waren tote Regeln: Cloudflares Feldreferenz sagt, der Score habe einst von 0 bis 100 gereicht, und „Now, the threat score is always 0 (zero).“ (Inzwischen ist der Threat Score immer 0.) Eine Regel, die einen hohen Score verlangt, trifft auf nichts zu, und ich deaktivierte beide am 3. November, dem Tag, an dem ich das las; die User-Agent-Challenges bleiben.1146 Der Free-Tarif von Cloudflare erlaubt fünf Custom Rules, die „work as customized web application firewall (WAF) rules that you can use to perform actions like Block or Managed Challenge on incoming requests.“ (als angepasste WAF-Regeln funktionieren, mit denen Sie auf eingehende Anfragen Aktionen wie Block oder Managed Challenge anwenden können.)47
Was sie aufhielten. Die Exploit-Pfade, an beiden Tagen ab der ersten Minute.104 Am 30. Oktober keine einzige Formularübermittlung.110 Am 2. November die 438 Formularübermittlungen nach der Sperre durch meinen Monitor.2 Auf der Studio-Website keine einzige Formularübermittlung: Die Firewall protokollierte in den acht Tagen kein Event zu einem POST an das Formular.7
Warum der Rest durchkam. Keine Regel schaut sich an, was eine Anfrage ist. Die Listenregel greift bei einer Adresse erst, nachdem etwas sie auf die Liste gesetzt hat, am 30. Oktober ein Mensch und am 2. November mein Monitor.91019 Die User-Agent-Challenge gleicht einen String ab, und beide Scanner schickten einen Browser-String, mindestens 69.322-mal am ersten Tag und 9.725-mal am zweiten.3241 Gegen Werkzeuge, die sich zu erkennen geben, wirkt die User-Agent-Challenge durchaus: Mein eigener curl-Test am 30. Oktober zog 24 Managed Challenges nach sich.39 Die Threat-Score-Challenge konnte bei niemandem auslösen, und die Firewall protokollierte für keine der beiden Adressen eine Challenge auf dem Formular.11104 Tor-Exits und Hosting-Netze außerhalb der sieben genannten ASNs stehen auf keiner Liste.
Eine Edge-Regel kann eine bekannte Adresse, ein bekanntes Netz oder ein ehrliches Werkzeug vom Rest unterscheiden, und ihre Challenge fragt nur bei den Anfragen, die ihr Ausdruck auswählt, ob ein Mensch da ist; einen Browser-String, den ein Scanner schickt, kann sie nicht von einem Browser unterscheiden.
Edge-Rate-Limit
Was es prüft. Der Regeltext ist kurz:9
(http.request.method eq "POST" and http.request.uri.path in {"/api/contact" "/paint/share"})
Sie zählt passende Anfragen nach Cloudflare-Rechenzentrum und Client-Adresse, erlaubt 3 pro 10 Sekunden und sperrt für 10 Sekunden. Gleichmäßig verteilt entspricht diese Obergrenze nominell 18 pro Minute pro Adresse und Rechenzentrum; die Regel selbst zählt in 10-Sekunden-Perioden.916 Der Free-Tarif legt Periode, Timeout und Zählschlüssel fest und gibt der Zone genau eine solche Regel: eine Zählperiode von 10 Sekunden, ein Mitigation-Timeout von 10 Sekunden und Zählung nach IP.48 Auch das Rechenzentrum ist nicht optional. „Every rate limiting rule includes the Cloudflare data center ID (cf.colo.id) as a mandatory characteristic“ (Jede Rate-Limiting-Regel enthält die ID des Cloudflare-Rechenzentrums (cf.colo.id) als verpflichtendes Merkmal), heißt es in Cloudflares Dokumentation, und „Counters are not shared across data centers, with the exception of data centers associated with the same geographical location.“ (Zähler werden nicht zwischen Rechenzentren geteilt, mit Ausnahme von Rechenzentren am selben geografischen Standort.)6
Was es aufhielt. An keinem der beiden Tage eine einzige Anfrage eines Scanners. Seine einzigen Events im Vorfallzeitraum waren die drei aus meinem eigenen Test.10439 Die Studio-Website hat eine eigene websiteweite Rate-Limiting-Regel, auf dieselbe Weise gezählt über jede Anfrage außer denen von Clients, die Cloudflare als verifizierte Bots führt, und sie wies keine der Formularübermittlungen des Tröpfelns ab: Ihre Firewall protokollierte in den acht Tagen kein Event zu einer Übermittlung an das Formular. Diese Regel zählt jede solche Anfrage, die eine Adresse an die Website schickt, und meine Daten zählen nur Formularübermittlungen, sie messen also nicht, wie viel Spielraum der Zähler noch hatte.97
Warum der Rest durchkam. Am 30. Oktober existierte die Regel erst ab 18:49:53 Uhr UTC, in derselben Sekunde wie das erste Event der Handsperre und etwa 12 Minuten, nachdem die letzte Formularübermittlung die App erreicht hatte.91 Die 1.065 Übermittlungen dieses Scanners kamen alle über ein einziges Rechenzentrum, Paris, eine bereits bestehende Regel hätte für sie also einen Zähler geführt.5 Am 2. November existierte die Regel, und die Formularübermittlungen des Scanners kamen zwischen 18:56 und 19:05:00 Uhr UTC über zwei Rechenzentren, Amsterdam mit 44 und London mit 41. In der geschäftigsten Minute, 19:02, trug London 18 und Amsterdam 14.5 Achtzehn in einer Minute entsprechen nominell der Obergrenze der Regel, doch meine Daten enthalten Minutensummen, keine Ankunftszeiten, und können deshalb nicht zeigen, ob irgendeine 10-Sekunden-Periode an einem Rechenzentrum mehr als drei enthielt. Cloudflare äußert sich zur Präzision unmissverständlich: „Rate limiting rules are not designed to allow a precise number of requests to reach your origin server. There may be a delay of up to a few seconds between detecting a request and updating rate counters.“ (Rate-Limiting-Regeln sind nicht dafür ausgelegt, eine exakte Zahl von Anfragen an Ihren Origin-Server durchzulassen. Zwischen dem Erkennen einer Anfrage und der Aktualisierung der Zähler können bis zu einige Sekunden vergehen.)48 Über die sieben Minuten von seiner ersten Übermittlung, die die App erreichte, bis zu seiner letzten kam der Scanner im Schnitt auf 8,86 pro Minute.2 Meine Notizen vom Tag nannten das „about nine a minute“ (etwa neun pro Minute) und hörten dort auf; die Aufteilung auf Rechenzentren ist der Teil, den sie übersahen.23
Ich wollte einen Beweis, dass die Regel überhaupt funktioniert, also testete ich sie am Morgen des 3. November von meinem eigenen Rechner aus. Um 06:36:49 Uhr PST schickte ich zehn POSTs im Abstand von 0,4 Sekunden ohne CSRF-Token: Vier kamen mit 422 zurück, weil ein Pflichtfeld fehlte und FastAPI sie abwies, dann sechs mit 429. Um 06:38:03 Uhr PST schickte ich acht mit allen Feldern: Drei kamen mit 403 zurück, der CSRF-Abweisung vom Origin, dann fünf mit 429 und dem Body „error code: 1015“ von Cloudflare. Die Firewall protokollierte in diesen Minuten 11 Events auf dem Formular, alle Sperren durch die Rate-Limiting-Regel.18 Die Regel löst aus, und Cloudflare protokolliert es. Meine Lesart des 2. November ist eine Hypothese: Auf zwei Zähler verteilt und auf jedem nahe an der nominellen Obergrenze, brachte der Scanner keinen der beiden zum Auslösen. Das ist die stärkste Erklärung, die zu den Minutensummen passt, kein Nachweis; die Summen können nicht zeigen, wie die Übermittlungen innerhalb jeder Minute verteilt waren, und Cloudflare räumt ein, dass überzählige Anfragen durchkommen können, während seine Zähler aktualisiert werden.548
Ein Rate Limit kann schnell von langsam unterscheiden, für eine Adresse an einem Rechenzentrum; wer sendet, kann es nicht unterscheiden.
CSRF-Token
Was es prüft. Das Formular dieser Website trägt ein verstecktes Token, in die Seite gerendert, mit drei Teilen: einem Zeitstempel, einem Zufallswert und einer HMAC-Signatur über die ersten beiden. Der Server akzeptiert es, wenn die Signatur passt und der Zeitstempel jünger als eine Stunde ist.15 Das Formular der Studio-Website hat kein CSRF-Token.25
Was es aufhielt. Vier Übermittlungen am 30. Oktober, alle in der Minute 18:03, und keine am 2. November.12
Warum der Rest durchkam. Das Token ist zustandslos. Es ist an keine Session, kein Cookie und keine Adresse gebunden, also bekommt jeder Client, der die Seite abruft, ein gültiges, und jeder Client, dem ein anderer ein Token überreicht, kann es vorlegen, solange es gilt. Beide Scanner riefen die Seiten der Website tatsächlich ab; das zeigen die Request-Logs. Was die Prüfung zeigt, ist enger gefasst: Jeder Versand an beiden Tagen trug ein gültiges, nicht abgelaufenes Token, das meine Website ausgegeben hatte, weil der Handler das Token vor dem Versand prüft.1513142416 Ein CSRF-Token soll verhindern, dass eine andere Website den Browser eines Besuchers dazu bringt, mein Formular abzuschicken. Es war nie dafür gedacht, einen Client aufzuhalten, der das Formular selbst abschicken will, und es tat es auch nicht.
Dieselbe Prüfung hatte einen zweiten Fehler, und der traf Menschen statt Bots. Schlug das Token fehl, löste der Handler eine JSON-403 aus, die das HTMX-Formular nie anzeigte; ein Mensch mit abgelaufenem Token hätte also auf „Senden“ geklickt und überhaupt nichts gesehen.12 Das habe ich behoben, auch wenn meine erste Korrektur ebenfalls nicht funktionierte; Abschnitt 6 erklärt, wie.
Ein gültiges CSRF-Token beweist, dass der Client ein nicht abgelaufenes Token besaß, das meine Website ausgegeben hat; es kann nicht beweisen, wer das Formular ausgefüllt hat, und nicht einmal, dass dieser Client die Seite geladen hat.
App-Rate-Limiter
Was er prüft. Der Handler dieser Website sitzt hinter einem Decorator, der Formularübermittlungen pro Client-Adresse und Uhrminute in Cloudflare KV zählt, mit einem Fallback im Speicher, und ab der sechsten mit 429 abweist.17 Die Studio-Website hat für anonyme Clients einen speicherinternen Limiter pro Prozess, gezählt pro Adresse und pro Client-Klassifizierung, der Klasse, die der Limiter anhand von User-Agent und Adresse vergibt, über jeden Pfad außer Health-Checks und statischen Dateien.25
Was er aufhielt. 830 Übermittlungen am 30. Oktober und 31 am 2. November. Auf der Studio-Website keine.127
Warum der Rest durchkam. Konstruktionsbedingt lässt er jedem fünf pro Minute durch, und weil das Zeitfenster die Uhrminute ist, bekommt ein Schwall, der eine Minutengrenze überspannt, zwei Kontingente.17 So viel wusste ich. Was ich nicht wusste, bis ich die Zahlen gegen den Code las: Er ist undicht, und der Code hat mindestens drei Wege, undicht zu sein. Der Limiter liest den Zähler per HTTP aus KV, vergleicht ihn mit dem Limit und schreibt dann in einer separaten Anfrage den Zählerstand plus eins; zwei Übermittlungen, die beide lesen, bevor eine von beiden schreibt, kommen also beide durch. Das Inkrement liest den Zähler vor dem Schreiben ein zweites Mal, und schlägt dieser zweite Lesevorgang fehl, setzt der Schreibvorgang den Zähler auf eins zurück, und die Minute beginnt von vorn. Und immer wenn der KV-Lesevorgang nichts zurückgibt oder der Schreibvorgang fehlschlägt, fällt die Entscheidung auf eine Liste im Speicher eines einzelnen Prozesses zurück, die andere Prozesse nicht teilen.17
Die Zahlen zeigen das Leck. Am 30. Oktober enthielten acht Minuten mehr als fünf Versände, und die Minute 18:06 enthielt 18.1 Ein exakter Limiter mit fünf pro Minute hätte über die Minuten, in denen der Scanner postete, höchstens 54 Versände erlaubt, und 101 gingen hinaus, 47 über der Obergrenze. Am 2. November ließen die Minuten 19:02 und 19:03 9 und 10 durch; ein exakter Limiter hätte von den 62 Übermittlungen, die er sah, höchstens 22 erlaubt, und 31 gingen hinaus, 9 über der Obergrenze.3 Welcher Pfad leckte, zeigen sie nicht. Meine Lesart ist, dass Übermittlungen denselben Zählerstand lasen, bevor eine von ihnen schrieb, oder dass KV-Aufrufe unter der Last fehlschlugen und den Zähler zurücksetzten oder die Entscheidung an prozesslokale Listen übergaben, oder eine Mischung davon; die Logs, die ich habe, verzeichnen den Status, den die App zurückgab, nicht welcher Pfad entschied, und können die Pfade deshalb nicht auseinanderhalten.173 Die Abhilfe ist für alle dieselbe, und sie ist keine Einzeilenänderung: ein Zählerspeicher mit atomarem Inkrement, den ein KV-Lesevorgang mit anschließendem separatem Schreibvorgang nicht bietet. Das Leck ist mein Bug. Es schärft zugleich das Argument dieses Beitrags mehr, als ich es absichtlich hätte tun können: Eine Kontrolle, die fünf sagt und achtzehn durchlässt, sollte nie diejenige sein, die entscheidet, wer mir schreiben darf.
Auf der Studio-Website wies der Limiter der App keine der beobachteten Formularübermittlungen ab, und die aktivste Adresse schickte 13 in einer Stunde. Seine Zähler erfassen auch die Anfragen einer Adresse an jeden anderen Pfad unter derselben Client-Klassifizierung, und meine Daten decken nur Formularübermittlungen ab, die Zahlen messen also nicht, wie viel Spielraum diese Zähler hatten.725
Ein Rate Limiter kann fünf von sechs unterscheiden, wenn die Anfragen nacheinander kommen; einen Menschen von einem Skript kann er bei keiner Anzahl unterscheiden.
Honeypot
Was er prüft. Das Formular der Studio-Website hat ein Texteingabefeld namens website, das ein Mensch nie sieht: Es trägt tabindex="-1" und aria-hidden, sodass weder die Tastatur noch ein Screenreader darauf landet. Kommt es mit irgendeinem Inhalt zurück, antwortet der Handler mit derselben Erfolgsmeldung, die ein Mensch sehen würde, und speichert und mailt nichts.25 Der Zweig steht im Handler, seit ich das Formular am 5. Juni gebaut habe.25 Das Formular dieser Website hat keinen Honeypot.24
Ned Batchelder beschrieb die Technik 2007: „These fields are called honeypots and are validated when the form data is posted. If they contain any text, then the submitter must be a bot“ (Diese Felder heißen Honeypots und werden beim Absenden der Formulardaten geprüft. Enthalten sie Text, muss der Absender ein Bot sein).49 Sein Grund, warum es funktioniert: Formular ausfüllende Bots „cannot distinguish invisible fields from visible ones.“ (können unsichtbare Felder nicht von sichtbaren unterscheiden.)49
Was er aufhielt. 42 der 52 Übermittlungen in den acht Tagen, 80,8%, darunter nach der Stunde ihres Eintreffens jede Übermittlung von einem Tor-Exit und, nach Stunde und Nachrichtenmuster, alle 13 aus einem Schwall eines selbsternannten SQL-Spiders.720
Warum der Rest durchkam. Ein Bot, der nur die sichtbaren Felder ausfüllt, berührt website nie, und die Absender, deren Übermittlungen gespeichert wurden und so mein Postfach erreichen konnten, ließen es leer.825 Dass sie absichtlich nur die sichtbaren Felder wählten, ist meine Lesart; der Handler verzeichnet nur, dass das versteckte Feld leer zurückkam. Batchelder sah es kommen: „In theory, it is possible for a spambot to defeat any of these measures. But in practice, bots are very stupid“ (Theoretisch kann ein Spambot jede dieser Maßnahmen aushebeln. In der Praxis sind Bots aber sehr dumm).49 Manche sind weniger dumm als andere, und die, die Leadgenerierung verkaufen, am wenigsten dumm.
Ein Honeypot beweist nur, dass der Client ein Feld ausgefüllt hat, das ein Mensch nicht sehen kann; ein leerer beweist nichts.
Wie weit jeder Strom kam: die Übermittlungen der beiden Scanner auf dieser Website und acht Tage Tröpfeln auf der Studio-Website.1278
Was die fünf gemeinsam hatten
Liest man die fünf Sätze zusammen, liegt die Lücke offen. Rate Limits begrenzen das Volumen pro Adresse. Ein CSRF-Token beweist, dass ein Client ein Token besaß, das die Website ausgegeben hat. Ein Honeypot beweist, dass der Client ein verstecktes Feld ausgefüllt hat. Die Edge-Regeln kennen Namen und Listen, und die eine, die fragen konnte, ob am anderen Ende ein Mensch sitzt, die User-Agent-Challenge, fragt nur die Anfragen, deren User-Agent-String auf ihrer Liste steht; die Threat-Score-Challenge daneben konnte nie auslösen. Keiner der Scanner passte, also stellte nichts diese Frage an jede Übermittlung, und die Scanner taten nichts, was ein Browser nicht auch tun kann: Sie riefen die Seite ab, behielten ihr Token und posteten in einem Tempo, das die Limits teilweise durchließen. Die OWASP-Leitlinie zu Credential Stuffing macht denselben Punkt für ein anderes Formular und rät zu Abwehrmaßnahmen, die sich „not rely on a single predictable volume limit“ (nicht auf eine einzige vorhersehbare Volumengrenze verlassen).50
5. Das langsame Tröpfeln auf der Studio-Website
Das Formular der Studio-Website erlebte nie eine Flut. Es erlebte ein Tröpfeln, und ein Tröpfeln ist ein anderes Tier. Das Zeitfenster reicht hier von der ersten Stunde mit einer Formularübermittlung in der Acht-Tage-Abfrage, 17:00 Uhr UTC am 26. Oktober, bis zu dem Moment, in dem ich die Daten speicherte, 13:59:50 Uhr UTC am 3. November; der erste und der letzte Tag sind unvollständig.7
Übermittlungen gegen gespeicherte Zeilen
Das Formular nahm in diesem Zeitfenster 52 POSTs entgegen und beantwortete jeden mit 200. Die Nachrichtentabelle wuchs im selben Zeitraum um 10 Zeilen. Da der Honeypot-Zweig der einzige Pfad im Handler ist, der mit 200 antwortet und nichts speichert, fing der Honeypot die anderen 42 ab, also 80,8%.7825 Nach UTC-Tag:78
| Datum (UTC) | Übermittlungen (200) | Gespeicherte Zeilen | Vom Honeypot gefangen |
|---|---|---|---|
| 26. Okt. (unvollständig) | 4 | 2 | 2 |
| 27. Okt. | 5 | 0 | 5 |
| 28. Okt. | 3 | 2 | 1 |
| 29. Okt. | 5 | 1 | 4 |
| 30. Okt. | 16 | 1 | 15 |
| 31. Okt. | 8 | 2 | 6 |
| 1. Nov. | 4 | 0 | 4 |
| 2. Nov. | 5 | 0 | 5 |
| 3. Nov. (unvollständig) | 2 | 2 | 0 |
Die Spitze vom 30. Oktober ist eine Adresse in Großbritannien, die in einer einzigen Stunde 13 Übermittlungen mit dem User-Agent Mozilla/5.0 (compatible; SQLSpider/1.5) schickte. Nach Stunde und Nachrichtenmuster speicherte die Tabelle keine davon: Die eine Zeile, die in jener Stunde gespeichert wurde, ist ein Leadgenerierungs-Pitch, das Muster der amerikanischen Adresse, die in derselben Stunde ebenfalls postete.7820
Wer sendete
Cloudflare kennzeichnet Anfragen aus dem Tor-Netz mit einem besonderen Ländercode, T1. Seine Header-Referenz sagt „T1 - Used for clients using the Tor network“ (T1: für Clients, die das Tor-Netz nutzen), und seine Seite zu IP Access rules sagt „T1: Tor exit nodes (country name: Tor)“ (T1: Tor-Exit-Nodes, Ländername: Tor).5152 Nach diesem Code kamen 23 der 52 Übermittlungen aus Tor, 44,2%, und 23 der 35 verschiedenen Adressen waren Tor-Exits, 65,7%.7 Zur Gegenprobe glich ich jede Adresse, die das Formular abgeschickt hatte, mit der Bulk-Exit-Liste des Tor Project ab, die das Projekt „a nearly real time authoritative source for Tor exits that allow contacting your server on port 80“ nennt (eine nahezu in Echtzeit maßgebliche Quelle für Tor-Exits, die Verbindungen zu Ihrem Server auf Port 80 erlauben). Alle 23 Adressen, die Cloudflare mit T1 kennzeichnete, standen auf der Liste, die ich am 3. November abrief, und keine der anderen 12.5354 Die beiden Quellen stimmen überein. Diese Liste zeigt allerdings die Exits vom 3. November, sie bestätigt den Code also für diesen Tag und beweist nicht, welche Adressen an dem Tag, an dem jede Übermittlung eintraf, Exits waren. Das CollecTor-Archiv des Projekts bewahrt frühere Exit-Listen auf und würde die Zugehörigkeit am Tag des Ereignisses klären; ich habe es nicht abgefragt.55
Die meisten Adressen in diesem Zeitfenster posteten einmal: 32 der 35, 91,4%, und jeder Tor-Exit darunter.7 Die drei, die mehr als einmal posteten, waren der SQL-Spider mit 13, eine Adresse in Polen mit zwei in einer Stunde und eine Adresse in den Vereinigten Staaten mit fünf. Die fünf Übermittlungen dieser letzten fallen in dieselben fünf Stunden wie die fünf Leadgenerierungs-Pitches, die die Tabelle im Zeitfenster speicherte, und sie wechselte jedes Mal ihren User-Agent: Chrome 135 unter Linux, dann Firefox 128, 137, 138 und wieder 137.720
Die User-Agents teilten sich entlang derselben Linie. Von den Tor-Übermittlungen nannten 18 ein Android-Gerät und 5 ein Desktop-Chrome, und 21 der 23 nannten Chrome 81 oder älter. Die Übermittlungen ohne Tor nannten Desktop-Browser, die weit neuer waren, 10 Desktop-Chrome von 125 bis 135 und 6 Firefox von 128 bis 138, dazu die 13 des Spiders unter eigenem Namen.7
Wer gespeichert wurde
Die beiden Hälften trafen sich am Honeypot. Keine gespeicherte Zeile im Zeitfenster liegt in einer Stunde, die nur Tor-Übermittlungen hatte, und alle 10 liegen in einer Stunde mit einer Übermittlung ohne Tor. 22 der 23 Tor-Übermittlungen fielen in Stunden ganz ohne gespeicherte Zeile, und die 23. teilt ihre Stunde mit der amerikanischen Leadgenerierungs-Adresse.20 Ich habe nach Stunde abgeglichen, weil die Request-Logs und die Nachrichtentabelle keine gemeinsame Request-ID haben, der Abgleich ist also ein Schluss: Nach der Stunde ihres Eintreffens war jede Tor-Übermittlung im Zeitfenster ein Honeypot-Treffer.20 Was gespeichert wurde und so das Postfach erreichen konnte, kam von Adressen außerhalb von Tor, die neuere Desktop-Browser-Strings schickten und den Honeypot leer ließen.
Die Zahlen passen zum alten Muster von Formularspam. Batchelders Taxonomie von 2007 teilt Spam-Erzeuger in „Playback spambots, form-filling spambots, and humans.“ (Playback-Spambots, Formular ausfüllende Spambots und Menschen.)49 Das Tor-Tröpfeln verhält sich wie Formularausfüller, die jedes Feld füllen und in den Honeypot tappen. Die Absender, die durchkamen, verhalten sich eher wie ein Mensch an einer Tastatur oder wie ein Skript, das gebaut wurde, um wie einer auszusehen. OWASP führt die Aktivität unter OAT-017, Spamming: „Malicious or questionable information addition to public or private content, or messages.“ (Hinzufügen bösartiger oder fragwürdiger Informationen zu öffentlichen oder privaten Inhalten oder Nachrichten.)56 OWASP grenzt auch etwas aus: „The mass abuse of broken form-to-email and form-to-SMS functions, to send messages to unintended recipients, is not included in this threat event“ (Der massenhafte Missbrauch fehlerhafter Formular-zu-E-Mail- und Formular-zu-SMS-Funktionen, um Nachrichten an nicht vorgesehene Empfänger zu senden, ist in diesem Bedrohungsereignis nicht enthalten).56 Meine Formulare mailen nur an mich, sie sind also der eingeschlossene Fall, Spam an den Betreiber der Website, und nicht jener.
Das Tröpfeln hebelt Rate Limiting außerdem konstruktionsbedingt aus. Die OWASP-Leitlinie zu Credential Stuffing benennt den Effekt, Anfragen auf viele Adressen zu verteilen: „This may defeat both IP block-lists and rate limiting, as per IP request volume may remain relatively low“ (Das kann sowohl IP-Sperrlisten als auch Rate Limiting aushebeln, da das Anfragevolumen pro IP relativ niedrig bleiben kann).50 Eine Übermittlung pro Adresse ist so niedrig, wie Volumen nur sein kann.
Was die gespeicherten Nachrichten sagen
Die Nachrichtentabelle reicht bis zum 6. Juni zurück und enthielt 108 Zeilen, als ich sie las; die neueste kam um 07:24 Uhr UTC am 3. November an.8 Ich sortierte sie mit festen Regeln nach Muster, der erste Treffer gewinnt:57
| Muster | Gesamte Tabelle | Oktober |
|---|---|---|
| „Hi, I wanted to know your price“ (Hallo, ich wollte Ihren Preis wissen), eine Sprache pro Zeile, ein Absendername | 49 | 10 |
| Anbieter-Pitches: Web-Leads, Instagram-Wachstum, Erklärvideo, SEO, B2B-Daten, ein Verzeichnis, Design | 22 | 4 |
| Gewinnköder: Jackpot, Promo-Code, PlayStation, Lamborghini, Rolls-Royce-Links, ein Absendername | 16 | 3 |
| Ein „$50,000,000 per month“-Link (50 Millionen Dollar pro Monat) auf telegra.ph | 9 | 9 |
| Ein Leadgenerierungs-Pitch: „If I were a real customer, how soon would I get a call back?“ (Wenn ich ein echter Kunde wäre, wie schnell würde ich zurückgerufen?) | 6 | 5 |
| Sonstiges: zwei Zeichenketten Kauderwelsch, eine Mailinglisten-Anfrage, eine Notiz mit der Bitte, anderswo zu schreiben | 4 | 1 |
| Mein eigener Test des E-Mail-Benachrichtigers, 21. September | 1 | 0 |
| Eine echte Anfrage, nach meiner Lesart | 1 | 0 |
Einige davon verdienen einen genaueren Blick. Die neun Zeilen mit den $50,000,000 kamen unter einem Absendernamen mit neun verschiedenen Absenderadressen, jede eine kleine Variation derselben Schlagzeile.8 Die Preisfrage taucht über die gesamte Tabelle in 25 Sprachen auf: Afrikaans, Armenisch, Baskisch, Belarussisch, Bengalisch, Bulgarisch, Katalanisch, Kroatisch, Dänisch, Englisch, Georgisch, Griechisch, Ungarisch, Isländisch, Indonesisch, Irisch, Italienisch, Latein, Lettisch, Litauisch, Luxemburgisch, Spanisch, Vietnamesisch, Walisisch und Zulu. Allein im Oktober kam sie in neun: Armenisch, Baskisch, Dänisch, Indonesisch, Latein, Lettisch, Litauisch, Spanisch und Vietnamesisch.57 Zwei der Sprachzuordnungen stammen von mir und sind knapp: Der Satz, den ich als Kroatisch lese, könnte Bosnisch sein, und der, den ich als Indonesisch lese, könnte Malaiisch sein.57 Jemand arbeitet sich mit einem Sprachführer durch ein Kontaktformular, eine Sprache nach der anderen.
Die Rate ist die andere Signatur des Tröpfelns. Der Oktober speicherte 32 Zeilen, 1,03 pro Tag, an 21 seiner 31 Tage, und nie mehr als drei an einem Tag.8 Kein Rate Limit pro Adresse löst bei einer pro Tag aus.
Welche das Postfach erreichten
Das Formular mailt erst seit dem 21. September um 17:46:47 Uhr UTC an das Postfach des Studios. Seitdem hat es 43 Zeilen gespeichert und damit gemailt, eine Obergrenze, weil der Benachrichtigungsaufruf nach bestem Bemühen erfolgt und ich den Zeitpunkt des Deployments nicht überprüft habe; die 65 Zeilen, die vor diesem Datum gespeichert wurden, wurden nie gemailt.258 Jede Zeile im Belegzeitraum ist Spam.57
Neben meinem eigenen Test ist eine Zeile in der gesamten Tabelle keiner. Am 14. September schrieb ein Kunde eines der Produkte des Studios wegen eines konkreten Fehlers, im Ton eines Menschen, und die Nachricht lag in der Tabelle, ohne dass jemand sie las.57 Als ich das Formular eine Woche später so verdrahtete, dass es an das Postfach mailt, sagte die Commit-Nachricht in klaren Worten, warum: „a paying customer’s message sat unread in D1 for a week“ (die Nachricht eines zahlenden Kunden lag eine Woche lang ungelesen in D1).25 Diese Nachricht ist der Grund, warum es den Benachrichtiger gibt, und sie ist der Grund, warum ich Spam nicht beheben kann, indem ich den Benachrichtiger abschalte. Den Absender halte ich hier anonym; der Punkt ist, dass ein echter Mensch in derselben Tabelle steht wie der Sprachführer.
6. Was eine Bot-Prüfung ist, und was live ging
Turnstile, in Cloudflares Worten
Cloudflare nennt Turnstile seine „smart CAPTCHA alternative“ (intelligente CAPTCHA-Alternative) und sagt, es „can be embedded into any website without sending traffic through Cloudflare and works without showing visitors a CAPTCHA.“ (lässt sich in jede Website einbetten, ohne Traffic über Cloudflare zu leiten, und funktioniert, ohne Besuchern ein CAPTCHA zu zeigen.)21 Der Mechanismus ist der Teil, der meine Frage beantwortet. „First, we run a series of small non-interactive JavaScript challenges to gather signals about the visitor or browser environment“ (Zuerst führen wir eine Reihe kleiner, nicht interaktiver JavaScript-Challenges aus, um Signale über den Besucher oder die Browserumgebung zu sammeln), heißt es in der Übersicht, und „These challenges include proof-of-work (computational puzzles), proof-of-space, probing for web APIs, and various other challenges for detecting browser-quirks and human behavior.“ (Zu diesen Challenges gehören Proof-of-Work (Rechenrätsel), Proof-of-Space, das Abtasten von Web-APIs und verschiedene weitere Challenges, die Browser-Eigenheiten und menschliches Verhalten erkennen.)21 Ein Rate Limit fragt, wie viele. Ein CSRF-Token fragt, ob der Client ein Token besitzt, das die Website ausgegeben hat. Turnstile fragt, was für ein Client am anderen Ende ist. Meine User-Agent-Challenge konnte das auch fragen, aber nur bei Anfragen, deren User-Agent-String auf ihrer Liste stand, und meine Threat-Score-Challenge konnte nie auslösen; der Unterschied, auf den es hier ankommt, ist eine selektiv ausgelöste Challenge gegenüber einer Verifizierung, die jede Übermittlung bestehen muss.
Das Widget hat drei Modi. Ich nutze Managed, den Cloudflare als einen beschreibt, der „Automatically chooses between non-interactive or checkbox challenge based on visitor risk level. No images or text to decipher.“ (je nach Risikostufe des Besuchers automatisch zwischen nicht interaktiver Challenge und Checkbox-Challenge wählt. Keine Bilder oder Texte zum Entziffern.)58 Einfach gesagt: Ein Besucher sieht eine kurze Prüfung und sonst nichts, es sei denn, die Signale verlangen mehr, und dann bekommt er ein Kästchen zum Anhaken. Der unsichtbare Modus verbirgt selbst das, und Cloudflare knüpft ihn an eine Bedingung: Eine Website, die ihn aktiviert, muss in ihrer eigenen Datenschutzerklärung auf Cloudflares Turnstile Privacy Addendum verweisen.58 Turnstile hat einen eigenen kostenlosen Tarif mit bis zu 20 Widgets und unbegrenzten Challenges, und Cloudflare sagt, es „can be used independently without requiring other Cloudflare services.“ (kann eigenständig genutzt werden, ohne andere Cloudflare-Dienste zu benötigen.)59
Die Serverhälfte, die Hälfte, die zählt
Das Widget allein tut für ein Formular nichts, und Cloudflare sagt das in den ersten Zeilen seines serverseitigen Leitfadens: „You must call the Siteverify API to complete your Turnstile implementation. The client-side widget alone does not protect your forms.“ (Sie müssen die Siteverify-API aufrufen, um Ihre Turnstile-Implementierung abzuschließen. Das clientseitige Widget allein schützt Ihre Formulare nicht.) Und dann noch deutlicher: „Tokens can be forged. An attacker can submit any string to your form endpoint without completing a challenge.“ (Tokens lassen sich fälschen. Ein Angreifer kann jeden beliebigen String an Ihren Formular-Endpunkt schicken, ohne eine Challenge zu lösen.)60 Der Server schickt das Token mit seinem Secret Key an https://challenges.cloudflare.com/turnstile/v0/siteverify und kann die IP-Adresse des Besuchers als remoteip mitgeben.60
Zwei Eigenschaften des Tokens prägen den Code drumherum. „Each token is valid for 300 seconds (5 minutes) after generation.“ (Jedes Token ist nach seiner Erzeugung 300 Sekunden (5 Minuten) gültig.) Und „Tokens are single-use. Each token can only be validated once. A replayed token will be rejected with the timeout-or-duplicate error code.“ (Tokens sind nur einmal verwendbar. Jedes Token kann nur einmal validiert werden. Ein wiederverwendetes Token wird mit dem Fehlercode timeout-or-duplicate abgewiesen.)60 Ein Scanner, der ein Token abgegriffen hat, könnte damit das Formular nicht 1.065-mal abschicken. Cloudflare begrenzt das Token außerdem auf 2.048 Zeichen und weist den Server an: „Validate the action and hostname when specified.“ (Validieren Sie Action und Hostname, wenn sie angegeben sind.)60 Ein Vorbehalt zur selben Seite: Sie nennt an einer Stelle timeout-or-duplicate für ein abgelaufenes Token und beschreibt in ihrer Fehlertabelle invalid-input-response als „Token is invalid, malformed, or expired“ (Token ist ungültig, fehlerhaft oder abgelaufen); ein Server sollte daher jeden Fehlschlag als Abweisung behandeln, statt seine Logik an einen einzelnen Code zu knüpfen.60
Was es verarbeitet, in Cloudflares Worten
Cloudflare behauptet nicht, dass Turnstile keine personenbezogenen Daten nutzt, und ich behaupte es auch nicht. Sein Turnstile Privacy Addendum nennt die Signale: „Cloudflare Turnstile processes a variety of client-side signals (“Signals”) such as client IP address, TLS Fingerprint, User-Agent Header and Sitekey and associated origin.“ (Cloudflare Turnstile verarbeitet verschiedene clientseitige Signale, etwa die IP-Adresse des Clients, den TLS-Fingerprint, den User-Agent-Header sowie den Sitekey und den zugehörigen Origin.) Es nennt den Zweck: „The purpose of collecting these Signals is not to identify, profile or target any individuals but solely to detect and block bots.“ (Diese Signale werden nicht erhoben, um Personen zu identifizieren, zu profilieren oder gezielt anzusprechen, sondern ausschließlich, um Bots zu erkennen und zu blockieren.) Und es sagt, dass Cloudflare sie auch zur Verbesserung des Produkts nutzt: „Cloudflare is a data controller of Signals that we process to improve Turnstile’s bot detection capabilities.“ (Cloudflare ist Verantwortlicher für die Signale, die wir verarbeiten, um die Bot-Erkennung von Turnstile zu verbessern.)22 Was Cloudflare nach eigener Aussage unberührt lässt, ist der Inhalt: „Turnstile does not access, store, or transmit user communications, form entries, or other page inputs.“ (Turnstile greift nicht auf Kommunikation von Nutzern, Formulareingaben oder andere Seiteneingaben zu, speichert und überträgt sie nicht.)21 Die Produktseite fügt hinzu, dass „Turnstile never harvests data for ad retargeting.“ (Turnstile nie Daten für Ad-Retargeting sammelt.)61 Meine Lesart: Turnstile sieht dieselbe Art von Verbindungsdaten, die meine Edge ohnehin für jede Anfrage protokolliert, und sieht nie, was ein Mensch in mein Formular tippt.
Die CSP, die es braucht
Turnstile lädt vom Challenge-Origin von Cloudflare, und eine Content Security Policy muss das erlauben. Cloudflare nennt zwei Direktiven: „script-src: https://challenges.cloudflare.com“ und „frame-src: https://challenges.cloudflare.com“.62 Beide meiner Zonen setzen ihre Policy in einer Cloudflare-Response-Header-Regel. Auf dieser Website gab es die Frame-Liste bereits, und sie bekam den Challenge-Origin dazu. Die Studio-Website hatte überhaupt kein frame-src, ihr default-src 'self' hätte also das iframe des Widgets blockiert; sie bekam frame-src 'self' plus den Challenge-Origin, und die eigene Policy der App im Code bekam dasselbe.12
Warum keine Managed Challenge auf dem POST des Formulars selbst
Die naheliegende Abkürzung steckte bereits in meiner Zone: eine Custom Rule, die einen POST an das Formular mit einer Managed Challenge beantwortet. Meine bestehenden Regeln tun das für curl und seine Verwandten, und es funktioniert, weil kein Mensch curl benutzt, um mir eine Nachricht zu schicken.939 Das auf jeden POST auszudehnen, hätte das Formular für alle kaputtgemacht. Cloudflare dokumentiert die Einschränkung direkt: „Challenge Pages interrupt the request flow by returning a full HTML page for the user’s browser to render and solve. This mechanism fails when the browser expects a non-HTML response, such as an AJAX or XHR (fetch) request.“ (Challenge-Seiten unterbrechen den Anfragefluss, indem sie eine vollständige HTML-Seite zurückgeben, die der Browser rendern und lösen soll. Dieser Mechanismus versagt, wenn der Browser eine Nicht-HTML-Antwort erwartet, etwa bei einer AJAX- oder XHR-(fetch-)Anfrage.)63 Das Kontaktformular dieser Website ist ein HTMX-Formular, das mit hx-post sendet und die Antwort in die Seite einsetzt, sein POST ist also genau diese Art von Anfrage; die Dokumentation nennt HTMX nicht beim Namen, und die Verbindung ist meine Lesart.24 Ein POST mit Challenge käme als HTML-Challenge-Seite zurück, die Cloudflare mit einem cf-mitigated-Header kennzeichnet, „set to challenge“ (auf challenge gesetzt), und „Regardless of the requested resource-type, the content-type of a challenge will be text/html.“ (Unabhängig vom angeforderten Ressourcentyp ist der Content-Type einer Challenge text/html.)64 Das Formular würde eine Challenge-Seite in sein Antwortfeld einsetzen, oder gar nichts.
Cloudflares eigene Antwort für diesen Fall ist eine andere Funktion: „To ensure your API calls are protected without breaking single-page applications (SPAs) or API integrations, Cloudflare recommends using Turnstile Pre-clearance.“ (Damit Ihre API-Aufrufe geschützt sind, ohne Single-Page-Applications (SPAs) oder API-Integrationen zu beschädigen, empfiehlt Cloudflare Turnstile Pre-clearance.)63 Mit Pre-clearance stellt ein gelöstes Widget zusätzlich ein cf_clearance-Cookie aus, das „enables visitors to bypass WAF Challenges on all subsequent requests on the zone, including API requests, based on the security clearance level set by the customer.“ (Besuchern ermöglicht, WAF-Challenges bei allen weiteren Anfragen an die Zone zu umgehen, einschließlich API-Anfragen, abhängig von der vom Kunden festgelegten Freigabestufe.) Das Cookie ersetzt das Token nicht: Cloudflare sagt, es „is issued to the visitor in addition to the default Turnstile token“ (wird dem Besucher zusätzlich zum Standard-Turnstile-Token ausgestellt), und dieselbe Seite verlangt weiterhin die Prüfung durch den Server: „You must call Siteverify to complete your Turnstile configuration.“ (Sie müssen Siteverify aufrufen, um Ihre Turnstile-Konfiguration abzuschließen.)65 Was Pre-clearance hinzufügt, ist eine Edge-Challenge, die ein XHR-POST überstehen kann, für einen Besucher, dessen Cookie eine gültige Freigabe auf der Stufe der Challenge oder darüber hält. Ich habe den Standard genutzt: ein Widget im Formular und ein Token, das mein eigener Server prüft.
Was am 3. November live ging
Auf beiden Websites legte ich über Cloudflares API ein Widget im Managed-Modus an, beschränkt auf die eigene Domain der Website. Das Secret jedes Widgets ging im selben Prozess aus der API-Antwort in den Schlüsselbund meines Mac und in die Variablen des Hosting-Dienstes und landete nie auf einer Kommandozeile oder in einer Datei.12
Diese Website. Ein neuer Verifier schickt das Token zusammen mit der Adresse des Besuchers an siteverify. Ein fehlendes Token, ein Token mit mehr als 2.048 Zeichen, ein abgewiesenes Token oder ein Transportfehler liefern alle false; das Formular schlägt also geschlossen fehl (fail closed), sobald das Secret gesetzt ist, und ohne Secret, wie in der lokalen Entwicklung und der Testsuite, funktioniert das Formular wie zuvor.12 Ein Token, das Cloudflare akzeptiert, muss außerdem auf einem der eigenen Hostnamen dieser Website ausgeliefert worden sein, und wenn Cloudflare die Action des Widgets zurückgibt, muss es die des Formulars selbst sein, wie der serverseitige Leitfaden rät; eine Antwort ohne Hostname ist eine Abweisung. Ein gültiges Token, das auf einer anderen Website oder von einem anderen Widget erzeugt wurde, kommt also nicht durch. Diese Prüfung kam am selben Morgen als zweite Änderung hinzu, gebaut ab 07:20 Uhr PST; um 07:25 Uhr PST, nach einem Cache-Purge, kam eine Übermittlung mit einem gültigen CSRF-Token von der Live-Seite und ohne Turnstile-Token erneut mit 403 zurück. Das beweist, dass das Formular nach dem Deployment weiterhin geschlossen fehlschlägt; die Hostname-Prüfung selbst kann eine Übermittlung ohne Token nicht auslösen, sie stützt sich auf den Code und die Tests.6660 Bis zu einer dritten Änderung ließen beide Widgets und beide Verifier auch localhost zu, und weil ein Sitekey öffentlich ist, hätte ein Token, das irgendjemand auf seinem localhost mit meinem erzeugt, siteverify und die Hostname-Prüfung gleichermaßen bestanden. Cloudflares Testseite rät davon ab: „Cloudflare recommends that sitekeys used in production do not allow local domains“ (Cloudflare empfiehlt, dass in Produktion genutzte Sitekeys keine lokalen Domains erlauben), und sie nennt localhost unter ihnen. Um 08:12 Uhr PST nahm ich localhost aus beiden Widgets heraus, und beide Verifier lassen standardmäßig nur noch die Website und ihren www-Host zu, die Prüfung lässt also die eigenen Hostnamen dieser Website zu und sonst nichts; die lokale Entwicklung nennt ihre Hostnamen in einer Einstellung. Nach dieser Änderung bestanden beide Websites erneut dieselben Live-Prüfungen, um 08:15 und 08:18 Uhr pazifischer Zeit: das Widget auf der Seite und eine Übermittlung ohne Token mit 403 abgewiesen. Keine Live-Prüfung kann die Hostname-Regel selbst auslösen, da ein fehlendes oder unsinniges Token scheitert, bevor der Hostname gelesen wird; diese Regel stützt sich auf die Widget-Einstellungen, den Code und die Tests.4667 Die Reihenfolge der Prüfungen im Handler ist jetzt CSRF, dann Turnstile, dann der Versand, hinter demselben Limiter pro Adresse wie zuvor. Beide Abweisungen geben das eigene Fehlerfragment der Seite mit einer 403 zurück, und HTMX setzt eine 4xx-Antwort standardmäßig nicht in die Seite ein, das Formular braucht also einen Handler, der eine 403 durchlässt. Meine erste Version dieses Handlers funktionierte nicht. Ich hängte sie an das Formular, doch HTMX feuert das Event auf dem Element, das die Antwort empfängt, und auf dieser Seite ist dieses Element ein Geschwisterelement des Formulars, das Event erreichte den Handler also nie, und ein abgewiesener Mensch sah weiterhin nichts. Eine zweite Durchsicht des Entwurfs dieses Beitrags fand den Fehler, indem sie das gebündelte HTMX gegen das Template las. Die Korrektur, eine vierte Änderung an jenem Morgen, setzt den Listener auf den Body der Seite, beschränkt auf das Antwortfeld, und ein Test stellt sicher, dass er gerendert wird. Um 08:21 Uhr PST, nach diesem Deployment, ließ ich das Formular in Playwrights Chromium laufen, mit dem Token des Widgets durch einen unsinnigen String ersetzt: Das Antwortfeld zeigte „error: Verification failed. Reload the page and try again.“ (Fehler: Verifizierung fehlgeschlagen. Laden Sie die Seite neu und versuchen Sie es erneut.), und nichts wurde versendet, weil siteverify ein unsinniges Token abweist, bevor der Handler den Versand erreicht.68 Die alte CSRF-Abweisung, die JSON-403, die HTMX nie anzeigte, gibt es nicht mehr. Das Formular setzt sein eigenes Widget nach jeder HTMX-Anfrage zurück, wobei es den Container des Widgets so benennt, wie Cloudflares Reset-Aufruf ihn erwartet, sodass ein erneuter Versuch ein frisches Token trägt, denn jedes Token ist nur einmal verwendbar und lebt 300 Sekunden.126069 Cloudflares clientseitiger Leitfaden nennt den Grund und den Aufruf: „To reset the widget if the given widget timed out or expired, you can use the function:“ (Um das Widget zurückzusetzen, wenn es abgelaufen ist, können Sie diese Funktion verwenden:) turnstile.reset(widgetId);.69 Meine erste Version rief den Reset ohne Argument auf, was der Leitfaden nicht dokumentiert; eine vierte Prüfrunde fand das, der Aufruf nennt jetzt auf beiden Websites den Container, und eine Browserprüfung am 3. November schickte auf jedem Formular ein unsinniges Token ab, sah die Abweisung und fand danach ein neues Token im versteckten Eingabefeld: binnen acht Sekunden auf der Studio-Website, binnen fünfundzwanzig auf dieser, wo das Widget zuerst seine Challenge erneut ausführte.12 Das Widget-Div und das Skript werden nur gerendert, wenn der Site Key gesetzt ist. Elf Tests bestehen: Ohne Secret wird wie zuvor versendet; mit Secret weist der Handler ein fehlendes Token vor jedem Versand ab und weist ein abgewiesenes Token ab, ein akzeptiertes Token wird versendet, und CSRF läuft weiterhin zuerst; und der Verifier akzeptiert den Hostnamen dieser Website und weist einen anderen Hostnamen ab, standardmäßig localhost, eine andere Action oder keine.126646
Die Studio-Website. Der Honeypot bleibt vorne. Ein ausgefülltes verstecktes Feld beantwortet weiterhin die Erfolgsmeldung und speichert nichts, sodass die billigsten Bots nie einen Aufruf bei Cloudflare kosten. Dann kommt Turnstile, und ein fehlendes, abgewiesenes oder nicht verifizierbares Token bekommt eine 403 mit dem Fehlerfragment. Dann Validierung, das Einfügen in die Nachrichtentabelle und die Benachrichtigung.12 Ihr Verifier prüft den Hostnamen (jetzt nur noch die beiden eigenen Hostnamen der Website) und die Action auf dieselbe Weise; diese Änderung ging um 07:19 Uhr PST live, und um 07:22 Uhr PST trug die Live-Seite das Widget und den Site Key, eine Übermittlung ohne Token und eine mit unsinnigem Token kamen mit 403 zurück, und eine Honeypot-Übermittlung wurde weiterhin mit 200 beantwortet.66 Vor dem Deployment ließ ich den Handler lokal gegen Cloudflares dokumentierte Testschlüssel laufen. Mit dem Always-Pass-Secret kam eine Übermittlung ohne Token mit 403 zurück, und das Dummy-Token bestand Turnstile und erreichte die Validierung; mit dem Always-Fail-Secret kamen beide mit 403 zurück; der Honeypot antwortete mit 200.1267 Cloudflares Testseite sagt, warum ein Dummy-Token in Produktion nicht durchkommen kann: „Production secret keys: Only accept real tokens, reject dummy tokens.“ (Produktions-Secret-Keys: Nur echte Tokens akzeptieren, Dummy-Tokens abweisen.)67
Die Live-Prüfung. Früher an jenem Morgen, nachdem das Deployment jeder Website für sich erfolgreich war, und nach einem Cache-Purge trug um 06:35 Uhr PST jede Startseite ein Widget-Div, ein Turnstile-Script-Tag und den Site Key, und die Live-Policy jeder Zone führte den Challenge-Origin in frame-src. Eine Übermittlung an das Formular dieser Website mit einem unsinnigen CSRF-Token kam mit 403 zurück, doch das ist die CSRF-Prüfung, die sie abweist, und es beweist nichts über Turnstile. Die Turnstile-Prüfung selbst testete ich um 06:43 Uhr PST: Eine Übermittlung mit einem gültigen CSRF-Token von der Live-Seite und ohne Turnstile-Token kam mit 403 und der Meldung „error: Verification failed. Reload the page and try again.“ zurück, und dieselbe Übermittlung mit einem unsinnigen Turnstile-Token ebenfalls mit 403. Das Secret liegt also auf dem Dienst, und die Prüfung läuft nach CSRF. Auf der Studio-Website kamen eine Übermittlung ohne Token und eine mit unsinnigem Token beide mit 403 zurück, und eine Honeypot-Übermittlung kam mit 200 zurück, ohne dass etwas gespeichert oder gemailt wurde.70
Die Browserprüfung. In einem echten Chromium, gesteuert von Playwright, renderte das Widget auf beiden Seiten, das Turnstile-Skript lud, und das versteckte Eingabefeld cf-turnstile-response hielt auf beiden Seiten binnen sechs Sekunden ein Token mit 730 Zeichen.70 Das beweist weniger, als es scheint. Cloudflares Testseite warnt, dass „Automated testing suites (like Selenium, Cypress, or Playwright) are detected as bots by Turnstile“ (automatisierte Testsuiten wie Selenium, Cypress oder Playwright von Turnstile als Bots erkannt werden), und nur siteverify entscheidet, ob ein Token besteht.67 Was die Prüfung beweist, ist, dass das Widget unter beiden Policies läuft und sein Feld füllt. Was ich nicht getan habe, ist der eine Test, der für einen Leser dieses Beitrags am meisten zählt: ein Mensch, der das Formular ausfüllt und eine Antwort bekommt. Dieser Test verschickt eine E-Mail, also ist es an mir, ihn durchzuführen, und das habe ich noch nicht getan.
7. Wofür das Rate Limit noch da ist
Turnstile hat die Rate Limits als Tor ersetzt. Als Zaun hat es sie nicht ersetzt. Eine Bot-Prüfung beantwortet das Wer; etwas muss weiterhin das Wie-viele beantworten, denn jede Übermittlung, die die Prüfung mit einem nicht leeren Token gültiger Größe erreicht, kostet jetzt einen Aufruf der siteverify-API von Cloudflare, und jede Übermittlung, die durchkommt, kostet weiterhin einen Versand.12 Cloudflares eigener serverseitiger Leitfaden führt Rate Limiting unter seinen Empfehlungen für den siteverify-Aufruf selbst: „Rate limit to protect against validation flooding.“ (Rate Limiting zum Schutz vor Validierungsfluten.)60 Beide meiner Limits bleiben: die Edge-Regel mit 3 Übermittlungen pro 10 Sekunden pro Adresse und Rechenzentrum und der Limiter der App mit fünf Übermittlungen pro Minute und Adresse.917
Die Edge-Regel verdient eine ehrliche Beschreibung, jetzt, da ich weiß, wie sie zählt. Cloudflare beschreibt das Design unumwunden: „Cloudflare tracks request rates by maintaining separate counters for each unique combination of values in a rule’s characteristics“ (Cloudflare erfasst Anfrageraten, indem es für jede eindeutige Wertekombination der Merkmale einer Regel separate Zähler führt), und „Cloudflare does not support global rate limiting counters across the entire network.“ (Cloudflare unterstützt keine globalen Rate-Limiting-Zähler über das gesamte Netzwerk.)6 Eine Adresse, die meine Zone über zwei Rechenzentren erreicht, bekommt zwei Kontingente, und die Übermittlungen des Scanners vom 2. November kamen tatsächlich über zwei; das ist meine stärkste Erklärung dafür, warum die Regel still blieb, vereinbar mit den Minutensummen, aber nicht durch sie bewiesen.5 Nimmt man Cloudflares Vorbehalt zur Präzision hinzu, dass die Regeln „are not designed to allow a precise number of requests to reach your origin server“ (nicht dafür ausgelegt sind, eine exakte Zahl von Anfragen an Ihren Origin-Server durchzulassen), dann beschreibt man die Regel am besten als nominelles Budget, nicht als bewiesene Obergrenze: 3 Formularübermittlungen pro 10 Sekunden pro Adresse und Rechenzentrum, nominell 18 pro Minute, näherungsweise durchgesetzt.489 Im Free-Tarif ist sie außerdem die einzige Rate-Limiting-Regel, die ich bekomme, mit fester 10-Sekunden-Periode und Zählung allein nach IP.48
Ein Detail des Free-Tarifs ist wichtig, falls Sie, wie ich, versucht waren, das Rate Limit selbst zur Challenge zu machen. Cloudflares Parameterseite sagt, dass in den Tarifen Free, Pro und Business eine Rate-Limiting-Regel mit Challenge-Aktion immer drosselt, statt eine Dauer einzuhalten, und dass „When visitors pass a challenge, their corresponding request counter is set to zero.“ (Wenn Besucher eine Challenge bestehen, wird ihr zugehöriger Anfragezähler auf null gesetzt.)71 Eine Challenge auf dem XHR-POST des Formulars würde zudem aus dem in Abschnitt 6 genannten Grund scheitern.63 Die Edge-Regel bleibt also eine Sperre, und die Challenge lebt im Widget.
Der App-Limiter braucht die Korrektur, die sein Leck verlangt: einen Zählerspeicher mit atomarem Inkrement, sodass keine zwei Übermittlungen denselben Zählerstand lesen können, und einen Fehlerpfad, der den Zähler nicht auf Prozesse verteilt. Der aktuelle Code, ein KV-Lesevorgang mit anschließendem separatem Schreibvorgang und einem Fallback pro Prozess, bietet keines von beiden, das ist also ein Wechsel des Speichers und kein Einzeilen-Patch.173 Mit Turnstile als zusätzlicher Bedingung für jede Übermittlung, die das Postfach erreichen könnte, muss eine Übermittlung, die das Leck durchlässt, trotzdem ein Token tragen, das Cloudflare akzeptiert; das Leck kostet also Volumen und Geld, und die Entscheidung, wer posten darf, hängt nicht mehr am Zähler. Ein Limiter ist ein Budget. Ein Budget, das in einer einzigen Flut um 47 danebenliegt, ist ein schlechtes Budget, aber es war nie ein Wächter.
Die Authentifizierungsleitlinie von OWASP fasst dieselbe Schichtung in eigene Worte. Sie behandelt ein CAPTCHA „as a defense-in-depth control to make brute-force attacks more time-consuming and expensive, rather than as a preventative.“ (als Defense-in-Depth-Kontrolle, die Brute-Force-Angriffe zeitaufwendiger und teurer macht, statt als vorbeugende Maßnahme.)72 Und ihre Leitlinie zu Credential Stuffing hält fest: „Most tools used for these types of attacks will make direct POST requests to the server and read the responses, but will not download or execute JavaScript“ (Die meisten Werkzeuge für solche Angriffe schicken POST-Anfragen direkt an den Server und lesen die Antworten, laden aber kein JavaScript herunter und führen keines aus).50 Meine beiden Scanner riefen die Seiten der Website tatsächlich ab, der wahrscheinlichste Weg, auf dem sie an gültige CSRF-Tokens kamen. Ein Client, der das Formular abschickt, ohne das Widget auszuführen, hat kein Turnstile-Token zu senden, und beide Formulare weisen eine Übermittlung ohne eines jetzt ab: Auf dieser Website kam eine Übermittlung mit gültigem CSRF-Token und ohne Turnstile-Token mit 403 zurück, ebenso eine Übermittlung ohne Token auf der Studio-Website.70
Die Checkliste
Für ein Kontaktformular hinter Cloudflare, in der Reihenfolge, in der ich es heute angehen würde:
- Setzen Sie eine Bot-Prüfung vor das Formular und verifizieren Sie jedes Token auf dem Server. In Cloudflares eigenen Worten: „The client-side widget alone does not protect your forms.“ (Das clientseitige Widget allein schützt Ihre Formulare nicht.)60 Prüfen Sie auch Hostname und Action in der Antwort, wie dieselbe Seite rät, damit ein anderswo erzeugtes Token nicht durchkommt, und halten Sie lokale Domains vom Produktions-Sitekey fern, denn „Cloudflare recommends that sitekeys used in production do not allow local domains“ (Cloudflare empfiehlt, dass in Produktion genutzte Sitekeys keine lokalen Domains erlauben).606667
- Schlagen Sie geschlossen fehl. Ein fehlendes, übergroßes, abgewiesenes oder nicht verifizierbares Token ist eine Abweisung, denn „Tokens can be forged.“ (Tokens lassen sich fälschen.)6012
- Setzen Sie das Widget nach jeder Übermittlung zurück. Tokens sind nur einmal verwendbar und 300 Sekunden gültig, ein erneuter Versuch ohne Reset ist also ein Versuch mit einem verbrauchten Token.6069
- Erlauben Sie den Challenge-Origin in beiden CSP-Direktiven. Cloudflare verlangt
script-srcundframe-srcfürhttps://challenges.cloudflare.com, und eine Policy mit nurdefault-src 'self'blockiert das iframe.6212 - Setzen Sie keine Managed Challenge auf ein Formular, das per XHR oder fetch sendet. Cloudflare sagt, die Challenge-Seite „fails when the browser expects a non-HTML response“ (versagt, wenn der Browser eine Nicht-HTML-Antwort erwartet), und ein HTMX-POST ist genau diese Art von Anfrage.63
- Geben Sie einen Fehler zurück, den ein Mensch sehen kann. Meine CSRF-Abweisung war eine JSON-403, die das HTMX-Formular nie anzeigte, ein Mensch mit abgelaufenem Token hätte also auf „Senden“ geklickt und nichts gesehen, und meine erste Korrektur lauschte auf dem Formular, während HTMX das Swap-Event auf dem Ziel der Antwort feuert, einem Geschwisterelement des Formulars; testen Sie die Abweisung also in einem Browser.1268
- Lassen Sie den Honeypot vor der Bot-Prüfung. Er fing auf meiner Studio-Website 42 von 52 Übermittlungen ab, zum Preis eines versteckten Eingabefelds und eines
if.725 - Behalten Sie das Rate Limit und dimensionieren Sie es als Kostendeckel, nicht als Tor. Cloudflare zählt es pro Adresse und Rechenzentrum, und der Scanner vom 2. November kam über zwei, der wahrscheinlichste Grund, warum es nie auslöste, auch wenn Minutensummen das nicht beweisen können.65
- Stützen Sie den App-Limiter auf einen Speicher, der atomar inkrementiert. Meiner las den Zählerstand und schrieb ihn in einer separaten Anfrage zurück, setzte ihn auf eins zurück, wenn sein zweiter Lesevorgang fehlschlug, fiel auf den Speicher eines einzelnen Prozesses zurück, wenn KV versagte, und ließ 47 und 9 Versände über seine eigene Obergrenze hinaus durch; die Logs können nicht sagen, welcher Pfad leckte.173
- Verlassen Sie sich nicht darauf, dass eine User-Agent-Regel einen Scanner aufhält. Beide Scanner schickten einen schlichten Chrome-String, mindestens 69.322-mal am 30. Oktober und 9.725-mal am 2. November.3241
- Zählen Sie E-Mails beim Mailanbieter, nicht am Formular. Mein Handler antwortet auf seinen Fehlerpfaden mit 200, und Resends Event
email.sentbedeutet nur, dass „the API request was successful“ (die API-Anfrage erfolgreich war); das Feldlast_eventeiner abgerufenen E-Mail sagt, was danach geschah.242873 - Fragen Sie die Analytics innerhalb eines Monats ab. Cloudflares adaptive Datensätze bewahren „at least 31 days of data on every plan“ (mindestens 31 Tage an Daten in jedem Tarif) auf.27
- Zählen Sie nach, wenn das Zeitfenster geschlossen ist. Meine Notizen, wenige Minuten nach der Flut vom 30. Oktober geschrieben, zählten zu wenige Formularübermittlungen, weil Cloudflare die letzten Minuten noch nicht fertig eingespielt hatte.16
- Betrachten Sie eine Adresssperre als Aufräumarbeit. Meine kam am 30. Oktober etwa 50 Minuten nach der ersten Formularübermittlung und am 2. November 8 Minuten nach Beginn des Scans, und an keinem der beiden Tage kam ein Versand nach der Sperrminute: 101 davor am 30. Oktober, und am 2. November 26 davor und 5 in ihr.1192
Was sich ändern könnte
Das sind Bedingungen, keine Prognosen. Jede nennt, was geschehen müsste und was ich dann tun würde.
Wenn ein Scanner beginnt, Managed Challenges zu bestehen. Die Prüfungen von Turnstile laufen im Browser und suchen nach Automatisierung; ein Werkzeug, das einen vollständigen Browser gut genug betreibt, um sie massenhaft zu bestehen, würde das Widget für sich in einen weiteren Kostenfaktor verwandeln und nicht in eine Mauer. Das Anzeichen wären Formularversände mit Tokens, die siteverify akzeptiert, von Adressen, die zugleich Injection-Strings im User-Agent-Header schicken. Batchelder schrieb 2007 die ehrliche Fassung davon für Honeypots: „If and when they do, we can switch back to CAPTCHAs.“ (Falls und wenn sie das tun, können wir zu CAPTCHAs zurückkehren.)49 Mein Ausweg wäre im Geist derselbe, eine stärkere Challenge, und bis dahin stehen der Honeypot und die Rate Limits weiterhin.
Wenn ich eine Edge-Prüfung mit Pre-clearance hinzufüge. Cloudflare empfiehlt Turnstile Pre-clearance für Anfragen, die keine Challenge-Seite rendern können, und ein cf_clearance-Cookie lässt einen Besucher „bypass WAF Challenges on all subsequent requests on the zone, including API requests“ (WAF-Challenges bei allen weiteren Anfragen an die Zone umgehen, einschließlich API-Anfragen).6563 Wenn ich sie übernehme, könnte die Edge den POST des Formulars selbst mit einer Challenge belegen, ohne HTMX für einen Besucher zu beschädigen, dessen Cookie eine gültige Freigabe auf der Stufe der Challenge oder darüber hält, und Cloudflares CSP-Seite fügt für diesen Modus eine Anforderung hinzu: „your connect-src directive must include ‘self’.“ (Ihre connect-src-Direktive muss ‘self’ enthalten.)62 Das Cookie kommt „in addition to the default Turnstile token“ (zusätzlich zum Standard-Turnstile-Token), und siteverify bleibt Pflicht, die Edge-Challenge wäre also eine zweite Prüfung vor der meines Servers, kein Ersatz dafür.65 Ich habe das nicht umgesetzt, und der Beitrag beschreibt das Design aus Widget und siteverify, das live ist.
Wenn ich das Resend-Log für die beiden Zeitfenster exportiere. Jede E-Mail-Zahl in diesem Beitrag ist eine Obergrenze, weil eine 200 des Formulars ein Versand ist oder ein Fehlschlag, den mein Handler ebenfalls mit 200 beantwortet.24 Der Retrieve-Endpunkt von Resend gibt für jede E-Mail ein last_event zurück, etwa delivered, und seine Event-Liste unterscheidet „sent - The email was sent successfully.“ (sent: Die E-Mail wurde erfolgreich gesendet.) von „delivered - Resend successfully delivered the email to the recipient’s mail server.“ (delivered: Resend hat die E-Mail erfolgreich an den Mailserver des Empfängers zugestellt.)7374 Mit diesem Export würden aus 101 und 31 exakte Zahlen, und sie könnten nur sinken.
Wenn ein Mensch den Spam von Hand schickt. Turnstile fragt, ob ein Browser und mitunter ein Mensch anwesend ist. Es fragt nicht, warum. Ein Mensch, der dafür bezahlt wird, einen Pitch in Kontaktformulare zu kopieren, kann es bestehen, und das können auch die Leadgenerierungs-Absender auf meiner Studio-Website, falls ein Mensch hinter ihnen steht. Für diesen Fall sind die Abwehrmittel die alten: ein Rate Limit für das Volumen und mein eigenes Urteil im Postfach.
Wenn das Tor-Tröpfeln eine Antwort Tag für Tag braucht. Die Exit-Liste, die ich geprüft habe, ist die, die ich am 3. November abgerufen habe, und der CollecTor-Dienst des Tor Project bewahrt frühere Listen auf.5355 Frage ich ihn für die acht Tage ab, stützt sich die Tor-Zuordnung auf die Listen der Tage, an denen die Übermittlungen eintrafen, und nicht auf Cloudflares Code plus die Liste vom 3. November. Cloudflare bietet außerdem in jedem Tarif Onion Routing an, das Tor-Nutzer direkt über das Tor-Netz bedient; Cloudflare sagt, damit „Human Tor users and bots can be distinguished by our Onion services, such that interactive challenges are only served to malicious bot traffic.“ (können unsere Onion-Dienste menschliche Tor-Nutzer und Bots unterscheiden, sodass interaktive Challenges nur an bösartigen Bot-Traffic ausgeliefert werden.)75 Ich habe es nicht eingeschaltet, und bevor ich es täte, würde ich den Tor-Traffic der Studio-Website nach Turnstile messen wollen.
Zentrale Erkenntnisse
Wenn Sie ein Kontaktformular betreiben:
- Ein CSRF-Token und ein Rate Limit zusammen lassen einen Scanner trotzdem an Sie schreiben. Beide meiner Scanner besaßen gültige Tokens, und mein Limiter ließ 101 und 31 Versände durch.12
- Verifizieren Sie das Token einer Bot-Prüfung auf dem Server, schlagen Sie geschlossen fehl und setzen Sie das Widget nach jeder Anfrage zurück; das Widget allein „does not protect your forms.“ (schützt Ihre Formulare nicht.)60
- Testen Sie Ihren Limiter unter Last und im Fehlerfall, nicht mit einer Anfrage nach der anderen. Ein Zähler nach dem Muster Lesen-dann-Schreiben mit einem Fallback pro Prozess ließ 18 Versände in einer Minute durch, die auf fünf eingestellt war.117
- Behalten Sie den Honeypot. Er ist der billigste Filter im ganzen Stack, und er fing auf meiner Studio-Website über acht Tage 80,8% dieser Übermittlungen ab, bevor der Handler eine Adresse prüfte, eine Zeile speicherte oder eine Mail verschickte.725
Wenn Sie die Edge der Website betreiben:
- Wissen Sie, was Ihr Rate Limit zählt. Bei Cloudflare zählt jede Regel pro Rechenzentrum, und ein Scanner, der über zwei eintrifft, bekommt zwei Zähler.65
- Testen Sie Ihre eigenen Regeln und lesen Sie danach die Firewall-Events. Elf Firewall-Events aus meiner Messung verrieten mir mehr über die Regel als ein Tag des Grübelns darüber, warum sie still geblieben war.18
- Belegen Sie einen XHR- oder fetch-POST nicht mit einer Challenge-Seite; Cloudflare dokumentiert, dass das scheitert, und empfiehlt für diesen Fall Pre-clearance.63
- Rechnen Sie damit, dass User-Agent-Regeln nur ehrliche Werkzeuge fangen. Die Scanner hier schickten einen Browser-String; mein curl-Test zog Challenges nach sich.3239
Wenn Sie derjenige sind, der das Postfach liest:
- Wenn sich das Postfach füllt, prüfen Sie den Absender vor dem Betreff. Drei meiner Mailströme teilen sich eine Absenderadresse, und deshalb lag meine erste Vermutung falsch.23
- Behandeln Sie die 200er eines Formulars als Obergrenze für E-Mails, nicht als Zählung, bis das Log des Mailanbieters etwas anderes sagt.2428
- Lesen Sie die gespeicherten Nachrichten, bevor Sie den Benachrichtiger abschalten. Die Nachricht eines echten Kunden lag einmal eine Woche ungelesen, und diese Nachricht ist der Grund, warum das Studio-Formular mir überhaupt mailt.25
Häufige Fragen
Warum bekommt mein Kontaktformular trotz Cloudflare Spam?
Höchstwahrscheinlich, weil nichts vor dem Formular jede Übermittlung fragt, ob ein Mensch sie geschickt hat. Die Custom Rules von Cloudflare gleichen Adressen, Netze, Pfade und User-Agent-Strings ab, und seine Rate Limits zählen Anfragen pro Adresse und Rechenzentrum; ein Scanner, der einen Browser-User-Agent schickt, zuerst die Seite lädt und in moderatem Tempo postet, kommt an allen vorbei. Auf meiner Website taten zwei Scanner genau das und brachten 101 und 31 Übermittlungen durch, und der Spam auf meiner Studio-Website kam in den acht gemessenen Tagen überwiegend mit einer Übermittlung pro Adresse, was keine Zählung pro Adresse erfassen kann.96127
Hält ein Rate Limit Bots auf?
Ein Rate Limit begrenzt, wie schnell eine Adresse sein kann; einen Bot von einem Menschen unterscheidet es nicht. Cloudflares eigene Dokumentation sagt, seine Regeln „are not designed to allow a precise number of requests to reach your origin server“ (sind nicht dafür ausgelegt, eine exakte Zahl von Anfragen an Ihren Origin-Server durchzulassen) und dass Zähler nicht zwischen Rechenzentren geteilt werden. Meine Edge-Regel löste bei keinem der beiden Scanner aus, und mein App-Limiter, eingestellt auf fünf pro Minute, ließ in einer Minute 18 durch: Sein Zähler ist ein Lesevorgang mit anschließendem separatem Schreibvorgang, ein fehlgeschlagener zweiter Lesevorgang setzt ihn auf eins zurück, und ein Fallback pro Prozess übernimmt, wenn der Speicher versagt; jeder dieser Pfade kann zusätzliche Übermittlungen durchlassen.48641173
Reicht ein Honeypot-Feld?
Es reicht für die Bots, die das versteckte Feld ausfüllen, und auf meiner Studio-Website waren das 80,8% dieser Übermittlungen: Meiner fing in acht Tagen 42 von 52 ab, darunter, nach der Stunde ihres Eintreffens, jede Übermittlung von einem Tor-Exit. Gegen einen Absender, der das versteckte Feld leer lässt, richtet er nichts aus, und die 10, die er durchließ, gespeichert und damit höchstens 10 E-Mails, taten genau das.720 Ned Batchelder sagte das schon 2007, als er die Technik beschrieb: „In theory, it is possible for a spambot to defeat any of these measures.“ (Theoretisch kann ein Spambot jede dieser Maßnahmen aushebeln.)49
Turnstile oder CAPTCHA: Was ist der Unterschied?
Ein klassisches CAPTCHA verlangt vom Besucher, ein Rätsel zu lösen. Turnstile führt „a series of small non-interactive JavaScript challenges to gather signals about the visitor or browser environment“ (eine Reihe kleiner, nicht interaktiver JavaScript-Challenges, um Signale über den Besucher oder die Browserumgebung zu sammeln) aus, und im Managed-Modus zeigt es nur dann ein Kästchen, wenn das Risiko des Besuchers eines verlangt, mit „No images or text to decipher.“ (Keine Bilder oder Texte zum Entziffern.)2158 Cloudflare sagt, seine Challenges nutzen keine „CAPTCHA puzzles or visual tests like selecting objects or typing distorted characters.“ (CAPTCHA-Rätsel oder visuellen Tests wie das Auswählen von Objekten oder das Abtippen verzerrter Zeichen.)76 Beide brauchen weiterhin einen Server, der das Ergebnis verifiziert; bei Turnstile ist das der siteverify-Aufruf.60
Warum hält CSRF-Schutz keinen Spam auf?
Eine korrekt implementierte CSRF-Abwehr soll verhindern, dass eine andere Website den Browser eines Besuchers dazu bringt, Ihr Formular abzuschicken. Mein Token ist schwächer als das: Es ist zustandslos und signiert, eine Stunde gültig und an keine Session, kein Cookie und keine Adresse gebunden, es beweist also nur, dass der Client ein Token besitzt, das meine Website innerhalb der Stunde ausgegeben hat, nicht woher die Anfrage kam. Keine der beiden Arten hält einen Client auf, der Ihre Seite lädt und das Formular selbst abschickt, denn das Laden der Seite ist genau der Weg, auf dem ein Client ein gültiges Token bekommt, und beide Scanner sammelten gültige ein; die Prüfung wies über beide Fluten hinweg vier ihrer Übermittlungen ab.1512
Wie unterscheide ich in den Logs einen Scanner von einem Menschen?
Schauen Sie sich alles an, was die Adresse getan hat, nicht nur das Formular. Meine beiden Scanner durchkämmten Tausende von Pfaden, setzten SQL-Injection-Strings in Pfade und in den User-Agent-Header, schickten für die meisten Anfragen einen Chrome-User-Agent und für einige einen Googlebot-Anspruch, den Cloudflares Verified-Bot-Regel nicht akzeptierte, und schickten das Formular in Schüben von Dutzenden bis Hunderten pro Minute ab.31324441 Zeitbasierte Payloads wie sleep(15) und waitfor delay '0:0:15' gehören zu dokumentierten Sondenfamilien.3377 Ein Mensch schickt eine Nachricht, von einer Seite, die er tatsächlich gelesen hat, und bittet Ihren Server nicht, zu schlafen.
Sammelt Turnstile personenbezogene Daten?
Cloudflare sagt nicht, dass es keine sammelt. Sein Turnstile Privacy Addendum nennt die Signale, die es verarbeitet, „such as client IP address, TLS Fingerprint, User-Agent Header and Sitekey and associated origin“ (etwa die IP-Adresse des Clients, den TLS-Fingerprint, den User-Agent-Header sowie den Sitekey und den zugehörigen Origin), gibt als Zweck an, „solely to detect and block bots“ (ausschließlich Bots zu erkennen und zu blockieren), und sagt, dass Cloudflare für diese Signale zur Verbesserung seiner Bot-Erkennung Verantwortlicher ist. Seine Übersicht sagt, Turnstile „does not access, store, or transmit user communications, form entries, or other page inputs.“ (greift nicht auf Kommunikation von Nutzern, Formulareingaben oder andere Seiteneingaben zu, speichert und überträgt sie nicht.)2221 Es sieht also Verbindungsdaten, nicht das, was ein Mensch tippt.
Verwandte Beiträge auf dieser Website. Der Auto-Modus von Claude Code ist keine Sicherheitsgrenze führt dasselbe Argument für eine andere Kontrolle: Ein Klassifikator, der jede Aktion beurteilt, ist nicht die Mauer. Agents.txt ist keine Zugriffskontrolle zieht die Grenze zwischen dem Erklären einer Richtlinie gegenüber Bots und ihrer Durchsetzung. Alarmmüdigkeit: 82 E-Mails an einem Tag von meinem Security-Monitor behandelt den Scanner vom 2. November aus der Sicht meines Monitors: eine Sperre acht Sekunden nach seinem ersten Befund, acht Minuten nach Beginn des Scans, und null E-Mails dazu. Was ich vor dem Schlafengehen ausführe ist eine andere Art von Betriebsroutine, eine nächtliche Prüfung von 15.000 Seiten auf Status, Antwortzeit und Cache, und Was wirklich kaputtgeht, wenn KI-Agenten unbeaufsichtigt laufen katalogisiert die Fehlermodi unbeaufsichtigter Agenten-Sessions, jener Art von Automatisierung, die meine Infrastruktur betreibt.
Quellen
Datenhinweise des Autors: Jede Zahl aus meinen eigenen Systemen stammt aus Cloudflares GraphQL Analytics API für die beiden Zonen (den Request- und Firewall-Event-Datensätzen), der Nachrichtentabelle der Studio-Website, dem Log meines Monitors und dem Code der beiden Formulare, alles am 3. November 2026 gespeichert und per Skript zu einem Ledger verarbeitet. Zeiten sind UTC, sofern nicht als PDT oder PST gekennzeichnet. Keine Adresse, keine Regel- oder Listenkennung, kein Schlüssel und kein Dateisystempfad aus diesen Belegen erscheint in diesem Beitrag.
-
Daten des Autors. Cloudflares Request-Analytics für die Zone dieser Website, die POSTs des Scanners vom 30. Oktober an das Kontaktformular pro Minute und nach Edge-Status, abgefragt am 3. November 2026 über das geschlossene Zeitfenster: 1.065 Übermittlungen, davon 101 mit 200, 830 mit 429, 130 mit 422 und 4 mit 403 beantwortet, alle erreichten die App; erste Übermittlung und erste 200 in der Minute 18:00, Spitze von 208 Übermittlungen in der Minute 18:03, die meisten 200er in einer Minute 18 (18:06), acht Minuten mit mehr als fünf 200ern, die letzte 200 um 18:36 und die letzte Übermittlung, die die App erreichte, um 18:37. Im Ledger abgeleitet: 101 Versände vor der Sperre, die erste Übermittlung etwa 46 Minuten, bevor ich die Flut meldete, und 49,88 Minuten vor der Sperre, die letzte Übermittlung, die die App erreichte, 11,88 Minuten vor der Sperre. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
Daten des Autors. Cloudflares Request-Analytics für die Zone dieser Website, die POSTs des Scanners vom 2. November an das Kontaktformular pro Minute und nach Edge-Status, abgefragt am 3. November 2026: 500 Übermittlungen, davon 31 mit 200, 31 mit 429 und 438 in oder nach der Sperrminute mit 403 beantwortet; 62 erreichten die App; erste Übermittlung in der Minute 18:58, Spitze von 32 Übermittlungen, die die App erreichten, in der Minute 19:02, die meisten 200er in einer Minute 10 (19:03), letzte 200 in der Minute 19:04; im Mittel 8,86 Übermittlungen pro Minute über die sieben Minuten von der ersten Übermittlung, die die App erreichte, in der Minute 18:58, bis zur letzten, in der Minute 19:04; von den 31 Versänden 26 in den Minuten vor der Sperrminute und 5 in der Sperrminute selbst, 19:04, der Minute, in der die Adresse um 19:04:01 auf die Liste kam, was Minuten-Buckets nicht gegen diese Sekunde ordnen können; danach keiner. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
Analyse des Autors im Ledger: für jede Minute der kleinere Wert aus fünf und den Übermittlungen, die der App-Limiter sah (200er, 429er und die CSRF-403er der App; nicht die 422er, die FastAPI abweist, bevor der Limiter läuft), aufsummiert über die Minuten. 30. Oktober: höchstens 54 Versände bei einem exakten Limiter gegenüber 101 gemessenen, 47 darüber, aus 935 Übermittlungen, die der Limiter sah. 2. November: höchstens 22 gegenüber 31, 9 darüber, aus 62. Eine kontrafaktische Obergrenze, keine Messung; die gemessenen Versände sind die obigen Formularzahlen. ↩↩↩↩↩↩↩
-
Daten des Autors. Cloudflares Firewall-Events für die Adresse vom 2. November, abgefragt am 3. November 2026 (ein Stichproben-Datensatz, genutzt dafür, welche Regel wann handelte, nie für Volumen): das erste Event um 18:57:11 von der Exploit-Pfad-Regel, das erste Event der Sperrlisten-Regel um 19:04:03; jede Zeile eine Sperre, keine eine Managed Challenge; 310 Zeilen auf dem Kontaktformular, jede eine Sperre, die erste um 19:04:05; und kein Event der Rate-Limiting-Regel für die Adresse. ↩↩↩↩↩↩↩↩↩↩
-
Daten des Autors, Nachtrag zum Ledger. Cloudflares Request-Analytics für die Formular-POSTs der beiden Scanner, gruppiert nach Cloudflare-Rechenzentrum, Minute und Status, abgefragt am 3. November 2026: am 2. November von 18:56 bis 19:05:00, dem Ende der Abfrage, die Zählung enthält also nur die erste Übermittlung der Minute 19:05, 44 Übermittlungen über Amsterdam und 41 über London; in der Minute 19:02 18 über London (4 versendet, 14 von der App abgewiesen) und 14 über Amsterdam (5 versendet, 9 abgewiesen); die größte Zahl in einer Minute an einem Rechenzentrum, 19, war die von Amsterdam in der Minute 19:04, 15 davon nach der Sperre an der Edge abgewiesen. Am 30. Oktober kamen alle 1.065 Übermittlungen über Paris. ↩↩↩↩↩↩↩↩
-
Cloudflare, “How Cloudflare determines the request rate”, Cloudflare-WAF-Dokumentation, abgerufen am 3. November 2026, 14:08:57 Uhr UTC. Separate Zähler pro Merkmalskombination; Zähler werden nicht zwischen Rechenzentren geteilt, außer bei solchen am selben geografischen Standort;
cf.colo.idein verpflichtendes Merkmal; keine globalen Zähler. ↩↩↩↩↩↩↩ -
Daten des Autors. Cloudflares Request-Analytics für die Zone der Studio-Website, POSTs an das Kontaktformular nach Stunde, Adresse, Land und User-Agent, von 17:00 Uhr UTC am 26. Oktober bis 13:59:50 Uhr UTC am 3. November 2026: 52 Übermittlungen, alle mit 200 beantwortet, von 35 Adressen; 23 Übermittlungen und 23 Adressen mit Cloudflares Tor-Ländercode (44,2% und 65,7%); 32 Adressen mit genau einer Übermittlung (91,4%), jede Tor-Adresse darunter; die drei wiederholt sendenden Adressen mit 13 (Großbritannien, eine Stunde, der SQLSpider-User-Agent), 2 (Polen, eine Stunde) und 5 (Vereinigte Staaten, jedes Mal ein neuer User-Agent); Tor-User-Agents 18 Android und 5 Desktop-Chrome, 21 mit Chrome 81 oder älter; User-Agents ohne Tor 10 Desktop-Chrome 125 bis 135, 6 Firefox 128 bis 138 und 13 andere. Die Firewall der Zone protokollierte im Zeitfenster kein Event zu einem POST an das Formular. Honeypot-Treffer sind Übermittlungen minus gespeicherte Zeilen: 42, also 80,8%. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
Daten des Autors. Die Kontaktnachrichtentabelle der Studio-Website, gelesen am 3. November 2026 um 14:04 Uhr UTC: 108 Zeilen vom 6. Juni bis 07:24 Uhr UTC am 3. November, 10 davon im Zeitfenster der Abfrage; pro Tag wie in der Tabelle in Abschnitt 5; 32 Zeilen im Oktober an 21 von 31 Tagen, 1,03 pro Tag, nie mehr als drei an einem Tag; die neun Zeilen mit $50,000,000 unter einem Absendernamen mit neun Absenderadressen; 43 Zeilen seit dem Commit des Benachrichtigers und 65 davor. ↩↩↩↩↩↩↩↩↩↩↩
-
Daten des Autors. Die Zonenkonfiguration beider Websites, exportiert über Cloudflares API am 3. November 2026: die Rate-Limiting-Regel dieser Website (Version 1, zuletzt aktualisiert am 2026-10-30 um 18:49:53 Uhr UTC; Ausdruck wie zitiert; Merkmale Rechenzentrum und Client-Adresse; 3 Anfragen pro 10-Sekunden-Periode; 10 Sekunden Mitigation-Timeout, nominell 18 pro Minute pro Adresse und Rechenzentrum) und ihre fünf Custom Rules (eine Sperre für die Kontoliste und sieben ASNs, eine Exploit-Pfad-Sperre, ein Skip für verifizierte Bots und zwei Managed Challenges auf dem Kontaktformular und den Admin-Pfaden, nach User-Agent-String und nach Threat Score, beim Export beide aktiviert und die Threat-Score-Regel später am selben Tag deaktiviert); die Regeln der Studio-Website derselben Arten, mit der Challenge websiteweit und einer API-Pfad-Sperre, die das Formular ausnimmt, sowie eine websiteweite Rate-Limiting-Regel, gezählt pro Rechenzentrum und Adresse über Anfragen von Clients, die keine verifizierten Bots sind. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
Daten des Autors. Cloudflares Firewall-Events für die Adresse vom 30. Oktober, abgefragt am 3. November 2026 (3.932 Stichprobenzeilen, unter der Abfragegrenze, nur für Zuordnung und Zeitpunkt genutzt): das erste Event um 17:59:53 von der Exploit-Pfad-Regel, die ab der ersten Minute des Scans handelte; jedes der 111 Stichproben-Events vor 18:49:53 von dieser Regel; das erste Event der Sperrlisten-Regel um 18:49:53; kein Event der Rate-Limiting-Regel und kein Event auf dem Kontaktformular für die Adresse. ↩↩↩↩↩↩↩↩↩↩↩
-
Cloudflare, “cf.threat_score”, Feldreferenz der Ruleset Engine, abgerufen am 3. November 2026, 16:12:44 Uhr UTC. Die Beschreibung des Felds: ein Score, der einst von 0 bis 100 reichte, und „Now, the threat score is always 0 (zero).“ ↩↩↩
-
Aufzeichnung des Autors darüber, was am 3. November 2026 live ging, geschrieben anhand der Werkzeugausgaben der Session: die beiden Widgets (Managed-Modus, gegen 14:13 Uhr UTC über Cloudflares API angelegt), die CSP-Änderungen in beiden Zonen, der Verifier dieser Website und die Reihenfolge im Handler (schlägt geschlossen fehl, CSRF, dann Turnstile, dann der Versand, 403-Fehlerfragmente, Widget-Reset nach jeder HTMX-Anfrage), ihre zehn neuen Tests (10 bestanden; ein elfter kam mit der Hostname-Prüfung hinzu), die Reihenfolge im Handler der Studio-Website (Honeypot, dann Turnstile, dann Validierung, Speicherung und Benachrichtigung) und die lokalen Prüfungen gegen Cloudflares Testschlüssel; auf beiden Websites weist der Verifier ein fehlendes Token oder eines mit mehr als 2.048 Zeichen ab, ohne siteverify aufzurufen (aus dem Code gelesen). Beide Websites gingen zwischen 14:32 und 14:36 Uhr UTC live. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
Daten des Autors. Cloudflares Request-Analytics für die Adresse vom 30. Oktober, pro Minute und in einem Durchlauf über alle Zonen meines Kontos, abgefragt am 3. November 2026: 76.429 Anfragen an diese Website von der Minute 17:59 bis zur Minute 18:51 (53 Buckets), dieselbe Summe in beiden Abfragen; 69.484 vor der Sperrminute bei einem Mittel von 1.389,7 pro Minute, eine Spitze von 3.219 um 18:36, 5.234 nach der Sperrminute, keine davon ausgeliefert; nach Ergebnis 49.513 ausgeliefert (64,8%), 18.990 von der App abgewiesen (24,8%, darunter 10.019 GET 418 vom Agenten-Gate der Website), 2.339 Origin-520er (3,1%), 5.587 an der Edge blockiert (7,3%); 50,88 Minuten von der ersten Minute des Scans bis zur Sperre; 387 GETs an kiradex.app, alle mit 200 beantwortet, und keine andere Zone berührt. ↩↩↩↩↩↩↩↩↩↩
-
Daten des Autors. Cloudflares Request-Analytics für die Adresse vom 2. November, pro Minute, nach Pfad, nach User-Agent und in einem Durchlauf über alle Zonen, abgefragt am 3. November 2026: zwei GETs um 07:49 Uhr UTC, 11,1 Stunden vor dem Scan; 10.907 Anfragen von der Minute 18:56 bis zur Minute 19:17 (22 Buckets), 10.909 mit den frühen GETs, dieselbe Summe in vier Abfragen; 29 Anfragen in der ersten Minute, 637 in der geschäftigsten Minute vor der Sperre (18:57), 1.107 in der geschäftigsten Minute (19:07), 300 in der letzten; 2.986 vor der Sperrminute (ein Mittel von 373,2 pro Minute), 7.445 danach, keine davon ausgeliefert; nach Ergebnis 1.388 ausgeliefert (12,7%), 1.473 von der App abgewiesen (13,5%, darunter 420 GET 418), 116 Origin-520er (1,1%), 7.930 an der Edge blockiert (72,7%); 8,02 Minuten von der ersten Minute des Scans bis zur Sperre; 368 GETs an kiradex.app und keine andere Zone berührt. ↩↩↩↩↩↩↩↩↩↩↩
-
Codelektüre des Autors. Das CSRF-Modul dieser Website, wie es am 30. Oktober und 2. November im Einsatz war: ein Token aus Zeitstempel, Zufallswert und HMAC-SHA256-Signatur über beide; gültig, wenn die Signatur passt und der Zeitstempel jünger als 3.600 Sekunden ist; an keine Session, kein Cookie und keine Adresse gebunden. Am 3. November 2026 aus dem Quellcode gelesen. ↩↩↩↩
-
Daten des Autors. Das per Skript aus den gespeicherten Abfrageergebnissen erstellte Ledger (484 Zeilen, jede mit Quelle und Methode) und sein Codebuch, 3. November 2026, einschließlich der Regeln dafür, welche Schicht welchen Status beantwortete, der Regel, dass Firewall-Events Zuordnung und Zeitstempel liefern und Request-Datensätze die Zahlen, der Regel, dass nur eine 200 ein gültiges CSRF-Token beweist, und der Tabelle, wo meine Notizen vom Tag von den Abfragen vom 3. November abweichen (die Notizen vom 30. Oktober entstanden um 11:58 Uhr PDT, bevor Cloudflare die letzten Minuten eingespielt hatte, und zählen zu wenige Formularübermittlungen, 422er, Mal-Share-Übermittlungen und Anfragen der Stunde). ↩↩↩↩↩↩↩↩
-
Codelektüre des Autors. Der Formular-Rate-Limiter dieser Website, wie er am 30. Oktober und 2. November im Einsatz war: standardmäßig fünf Formularübermittlungen pro Client-Adresse und Zeitfenster, ein 60-Sekunden-Fenster mit der Uhrminute als Schlüssel, ein KV-Lesevorgang des Zählers, ein zweiter Lesevorgang im Inkrement, der einen fehlgeschlagenen Lesevorgang als null zählt, und ein separater KV-Schreibvorgang mit diesem Zählerstand plus eins, eine 429, wenn der gelesene Zählerstand das Limit erreicht, und ein Fallback auf eine prozesslokale Liste im Speicher, wenn der KV-Aufruf fehlschlägt. Am 3. November 2026 aus dem Quellcode gelesen. ↩↩↩↩↩↩↩↩↩↩
-
Daten des Autors, Nachtrag zum Ledger. Meine Messung der Rate-Limiting-Regel dieser Website am 3. November 2026 von meinem eigenen Rechner aus: um 14:36:49 Uhr UTC zehn POSTs ohne Token im Abstand von 0,4 Sekunden (vier 422, dann sechs 429); um 14:38:03 Uhr UTC acht mit allen Feldern (drei 403 vom Origin, dann fünf 429 mit dem Body „error code: 1015“); Cloudflares Firewall-Events für das Formular von 14:35 bis 14:40 Uhr UTC, abgerufen um 14:40:36 Uhr UTC: 11 Events, jedes eine Sperre durch die Rate-Limiting-Regel. ↩↩↩↩
-
Daten des Autors. Das Log meines Monitors für den 2. November 2026, 10:40 bis 11:32 Uhr PST, und seine Befund- und Sperrdatensätze, gespeichert am 3. November: ein Lauf, gestartet um 10:57:00 Uhr PST, mit einem Detektor-Zeitfenster bis 18:57:22 Uhr UTC und ohne Befund, die Anfragen der Adresse in diesem Fenster laut Minutendaten zwischen 29 und 666; der nächste Lauf um 11:02:46 Uhr PST; der erste Befund um 11:03:53 Uhr PST (eine kritische Anfrageflut, 1.914 Anfragen für die Adresse gezählt), 7,88 Minuten nach der ersten Minute des Scans; die Adresse um 11:04:01 Uhr PST auf die Kontosperrliste gesetzt; insgesamt sechs Befunde, jeder in den täglichen Digest umgeleitet und keiner gemailt. Der Monitor lief am 30. Oktober nicht; ich nahm ihn am 31. Oktober wieder in Betrieb, und sein Traffic-Detektor kam später hinzu. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
Analyse des Autors. Abgleich der gespeicherten Zeilen der Studio-Website mit den stündlichen POST-Daten nach Stunden-Buckets, 3. November 2026: keine gespeicherte Zeile in einer Stunde mit nur Tor-Übermittlungen; alle 10 in Stunden mit einer Übermittlung ohne Tor; 22 von 23 Tor-Übermittlungen in Stunden ohne gespeicherte Zeile, die 23. in einer Stunde, die sie mit der US-Leadgenerierungs-Adresse teilt; die fünf Leadgenerierungs-Zeilen im Zeitfenster in den fünf Stunden, in denen diese Adresse postete. Ein Schluss nach Stunden, kein Join über eine Request-ID. ↩↩↩↩↩↩↩
-
Cloudflare, “Overview”, Übersicht der Turnstile-Dokumentation, abgerufen am 3. November 2026, 14:08:57 Uhr UTC. „Cloudflare’s smart CAPTCHA alternative“; Einbettung ohne Cloudflare-Traffic; die nicht interaktiven JavaScript-Challenges und die Beschreibung von Proof-of-Work und Proof-of-Space; die Aussage, dass Turnstile nicht auf Formulareingaben zugreift, sie nicht speichert und nicht überträgt. ↩↩↩↩↩↩
-
Cloudflare, “Turnstile Privacy Addendum”, zuletzt aktualisiert am 18. Juni 2025, abgerufen am 3. November 2026, 14:08:57 Uhr UTC. Die Signale, die Turnstile verarbeitet, ihr erklärter Zweck und Cloudflares Rolle als Verantwortlicher zur Verbesserung der Bot-Erkennung, alles zitiert. ↩↩↩
-
Captain’s Log des Autors, 2. November 2026: meine Frage an die Agenten-Session, die meine Infrastruktur betreibt, ob sie die E-Mails verschickte, die drei Mailströme, die sich eine Absenderadresse teilen, die Betreffzeilen „Contact Form:“ der E-Mails mit den Payloads des Scanners und meine Notiz, dass die Formularübermittlungen des Scanners „ran about nine a minute“ (bei etwa neun pro Minute lagen). ↩↩↩↩↩
-
Codelektüre des Autors. Der Kontakt-Handler und das Formular-Template dieser Website, wie sie am 30. Oktober und 2. November im Einsatz waren: ein HTMX-Formular (
hx-postan den Formularpfad) mit eingerendertem CSRF-Token; der Limiter-Decorator vor dem Handler; die CSRF-Prüfung, die eine 403 auslöst, vor dem Versand; der Versand über Resend mit dem Betreff „Contact Form:“ und dem Namen des Absenders; das Erfolgsfragment bei einem akzeptierten Versand; und zwei Fehlerpfade, eine fehlende Mailkonfiguration und ein fehlgeschlagener Versand oder einer ohne id, die das Fehlerfragment mit dem Standardstatus von FastAPI zurückgeben, 200. Kein Honeypot-Feld. Am 3. November 2026 aus dem Quellcode gelesen. ↩↩↩↩↩↩↩↩↩ -
Codelektüre und Repository-Historie des Autors für die Studio-Website, wie sie vor dem 3. November 2026 im Einsatz war: der Honeypot-Zweig des Kontakt-Handlers (ein nicht leeres verstecktes Feld
websitebeantwortet das Erfolgsfragment und speichert und mailt nichts; es ist die erste Prüfung des Handlers, vor der Adressprüfung, dem Einfügen und dem Benachrichtiger), vorhanden seit dem Commit, der das Formular am 5. Juni 2026 baute;tabindex="-1"undaria-hiddendes versteckten Eingabefelds; kein CSRF-Token; der Benachrichtiger nach bestem Bemühen, hinzugefügt am 21. September 2026 um 17:46:47 Uhr UTC in einem Commit, dessen Nachricht lautet „a paying customer’s message sat unread in D1 for a week“; und ein speicherinterner Rate Limiter pro Prozess für anonyme Clients, mit Client-Adresse und Client-Klassifizierung als Schlüssel (der Bot-Stufe, die der Limiter anhand von User-Agent und Adresse vergibt), über jeden Pfad außer Health-Checks und statischen Dateien. Am 3. November 2026 aus dem Repository gelesen. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Captain’s Log des Autors, 30. Oktober 2026, geschrieben um 11:58 Uhr PDT: meine Meldung der Flut um 11:46 Uhr PDT, die Handsperre und die Rate-Limiting-Regel von 11:48 bis 11:49 Uhr PDT und mein Test um 11:52 Uhr PDT, zitiert. Seine Zahlen wurden erhoben, während Cloudflare die letzten Minuten der Flut noch einspielte, und dieser Beitrag nutzt stattdessen die Abfragen vom 3. November. ↩↩↩
-
Cloudflare, “GraphQL API - Limits”, Dokumentation der GraphQL Analytics API, abgerufen am 3. November 2026, 14:08:57 Uhr UTC. Adaptive Datensätze „retain at least 31 days of data on every plan“. ↩↩
-
Resend, “Event Types”, Webhook-Dokumentation von Resend, abgerufen am 3. November 2026, 14:14:47 Uhr UTC.
email.sent„Occurs whenever the API request was successful. Resend will attempt to deliver the message to the recipient’s mail server.“ (Tritt ein, sobald die API-Anfrage erfolgreich war; Resend versucht dann, die Nachricht an den Mailserver des Empfängers zuzustellen.);email.deliveredfür eine erfolgreiche Zustellung. ↩↩↩ -
Daten des Autors. Cloudflares Request-Analytics für jede andere Adresse, die in der Acht-Tage-Abfrage das Kontaktformular dieser Website abschickte, ohne die beiden Scanner und meine Testadresse, abgefragt am 3. November 2026: drei Adressen (Südkorea, Japan, Indien), jede mit genau zwei 200ern an einem Tag, am 27., 28. und 29. Oktober. ↩
-
RIPE NCC, RIPEstat network-info, whois und as-overview für die Adresse vom 30. Oktober und ihr AS, abgerufen am 3. November 2026, 14:10:34 und 14:11:34 Uhr UTC: AS34534, Inhaber „BULLIONET FBW NETWORKS SAS“, eine Zuteilung, registriert mit dem Land FR. ↩
-
Daten des Autors. Cloudflares Request-Analytics für die Adresse vom 30. Oktober, gruppiert nach Pfad, Methode und Status, abgefragt am 3. November 2026; die Abfrage lieferte ihr Limit von 3.000 Zeilen und deckte 74.125 der 76.429 Anfragen ab, Pfadzahlen sind also Untergrenzen: mindestens 2.881 verschiedene Pfade und mindestens 2.680 Anfragen mit einer SQL-Injection-Payload im Pfad über mindestens 810 verschiedene Pfade; 320 POSTs an den Mal-Share-Pfad, alle abgewiesen (422 oder 400), und keiner von einer anderen Adresse in acht Tagen; die in Abschnitt 2 zitierten Payload-Varianten des Share-Pfads, mit 404 beantwortet. ↩↩↩↩
-
Daten des Autors. Cloudflares Request-Analytics für die Adresse vom 30. Oktober, gruppiert nach User-Agent, abgefragt am 3. November 2026; die Abfrage lieferte ihr Limit von 500 Zeilen und deckte 76.095 der 76.429 Anfragen ab, jede Zahl hier ist also eine Untergrenze: mindestens 69.322 Anfragen mit einem schlichten Chrome-141-Windows-String, mindestens 2.755 mit einem Googlebot-Anspruch und mindestens 3.154 mit einer SQL-Injection-Payload in mindestens 396 verschiedenen Strings. ↩↩↩↩↩↩
-
PortSwigger Web Security Academy, “SQL injection cheat sheet”, abgerufen am 3. November 2026, 14:10:34 Uhr UTC. Zeitverzögerungen für Oracle (
dbms_pipe.receive_message), Microsoft (WAITFOR DELAY), PostgreSQL (pg_sleep) und MySQL (SLEEP); die bedingte FormIF(condition, SLEEP(10), 'a');SELECT @@versionfür Microsoft und MySQL; OraclesFROM dual. ↩↩↩↩↩ -
sqlmap-Projekt, “time_blind.xml”, die zeitbasierten Blind-Payload-Definitionen des Werkzeugs, abgerufen am 3. November 2026, 14:12:27 Uhr UTC. Enthält Payloads mit
SLEEP,PG_SLEEP,WAITFOR DELAYundDBMS_PIPE.RECEIVE_MESSAGE. Zitiert für die Familien, nicht als das Werkzeug, das einer der Scanner nutzte. ↩ -
PortSwigger Web Security Academy, “Blind SQL injection”, abgerufen am 3. November 2026, 14:10:34 Uhr UTC. Warum eine Zeitverzögerung in einer Abfrage die HTTP-Antwort verzögert, zitiert. ↩
-
OWASP Automated Threats to Web Applications, “OAT-014 Vulnerability Scanning”, abgerufen am 3. November 2026, 14:13:41 Uhr UTC. Die Zusammenfassung, zitiert. ↩
-
Daten des Autors, Nachtrag zum Ledger. Cloudflares Request-Analytics für die Formular-POSTs der beiden Scanner, gruppiert nach Edge-Status und Origin-Status, abgefragt am 3. November 2026: am 30. Oktober tragen 395 der 830 429er einen Origin-Status von 429 und die anderen 435 keinen aufgezeichneten; am 2. November tragen 16 der 31 429er eine Origin-429 und 15 keinen aufgezeichneten. Keine 429 trägt einen reinen Edge-Status, wo der des Origin aufgezeichnet wurde. ↩↩
-
Test des Autors, 3. November 2026: eine zehnzeilige FastAPI-App, ausgeführt mit der eigenen Umgebung der Website (FastAPI 0.128.0), mit einem Limiter-artigen Decorator auf dem Endpunkt. Ein POST ohne Pflichtfeld gab 422 zurück, ohne dass der Wrapper aufgerufen wurde; ein vollständiger POST erreichte den Wrapper. ↩
-
Daten des Autors. Cloudflares Firewall-Events und Request-Analytics für meine eigene Testadresse am 30. Oktober 2026, abgefragt am 3. November: drei Events der Rate-Limiting-Regel, das erste um 18:52:38 Uhr UTC, mit einem Browser-User-Agent, und 24 Managed Challenges der User-Agent-Challenge-Regel für meine curl-Übermittlungen. Aus jeder Vorfallszählung ausgeschlossen. ↩↩↩↩↩
-
RIPE NCC, RIPEstat network-info, whois und as-overview für die Adresse vom 2. November und ihr AS, abgerufen am 3. November 2026, 14:10:34 und 14:11:34 Uhr UTC: AS24875, Inhaber „NOVOSERVE-AS NovoServe B.V.“, eine auf NovoServe B.V. registrierte Route, Land NL. Die Beschreibung der Unterzuweisung nennt eine Privatperson und wird hier nicht wiedergegeben. ↩
-
Daten des Autors. Cloudflares Request-Analytics für die Adresse vom 2. November, gruppiert nach User-Agent, abgefragt am 3. November 2026, vollständig (73 Strings, die alle 10.909 Anfragen abdecken): 9.725 Anfragen mit einem schlichten Chrome-141-Windows-String, 942 mit einem Googlebot-Anspruch, 54 verschiedene SQL-Injection-Payloads in 149 Anfragen, 3 anführungszeichenbrechende Strings in 78, 12
@@-Marker-Tokens in 12 und 2 gewöhnliche andere. ↩↩↩↩↩↩ -
Daten des Autors. Die User-Agent-Tabellen für beide Tage, erstellt vom Ledger-Skript am 3. November 2026: Der schlichte Chrome-141-Windows-String und der Googlebot-String sind am 30. Oktober und 2. November bytegleich. ↩
-
Wiederholung durch den Autor, 3. November 2026, der eigenen Detektoren meines Monitors auf vollständigen Cloudflare-Daten für 18:58:23 bis 19:03:23 Uhr UTC am 2. November, das Zeitfenster des Laufs, der um 11:02:46 Uhr PST startete, ausgeführt um 15:55:04 Uhr UTC und als Wiederholungsdatensatz mit den exakten Befehlen und der vollständigen Ausgabe der Detektoren gespeichert; fünf Zeilen im Nachtrag des Ledgers sind per Skript aus diesem Datensatz abgeleitet: 2.143 Anfragen von der Adresse im Befund zur Anfrageflut, 1.055 abgewiesene Anfragen im Scanner-Befund und ein Befund zu einer Formularflut aus 39 Übermittlungen mit 23 abgewiesenen, mit hohem Schweregrad, der in den täglichen Digest geht und weder mailt noch sperrt. Die 1.914 des Live-Laufs, in einem Zeitfenster, das im Moment der Abfrage des Laufs endete, sind das, was das eigene Log meines Monitors verzeichnete, und der Live-Lauf meldete keinen Befund zu einer Formularflut. Die am selben Tag vorgenommene Änderung der Zeitfenster: Jedes Live-Zeitfenster endet jetzt 90 Sekunden vor dem Lauf und beginnt dort, wo das vorige endete, mit einem schlichten Rückblick von fünf Minuten, wenn der vorige Datensatz fehlt oder veraltet ist. ↩↩↩
-
Daten des Autors. Cloudflares Request-Analytics für die Adresse vom 2. November, gruppiert nach Pfad, abgefragt am 3. November 2026, vollständig (2.318 Zeilen, die alle 10.909 Anfragen abdecken): 2.173 verschiedene Pfade und 778 Anfragen auf 542 verschiedenen Pfaden mit einer SQL-Injection-Payload. ↩↩
-
OWASP Web Security Testing Guide v4.2, “Testing for SQL Injection”, abgerufen am 3. November 2026, 14:12:12 Uhr UTC. Die fehlerbasierte und die Zeitverzögerungstechnik, zitiert. ↩
-
Aufzeichnung des Autors zur dritten Änderung am 3. November 2026, vorgenommen nach einer Durchsicht des Entwurfs dieses Beitrags: um 16:12 Uhr UTC die erlaubten Domains beider Widgets auf die Website und ihren www-Host gesetzt, localhost entfernt; die Standard-Hostname-Listen beider Verifier auf dieselben beiden Hostnamen gesetzt, wobei die lokale Entwicklung ihre eigene Liste setzt; die Tests dieser Website stellen sicher, dass localhost standardmäßig abgewiesen wird (11 bestehen); die Studio-Website neu deployt, und lokal gegen Cloudflares Always-Pass-Testschlüssel ausgeführt erreichte ein Dummy-Token mit dem Platzhalter-Hostnamen dieses Schlüssels in der Liste weiterhin die Validierung und wurde ohne ihn abgewiesen. Um 16:13 Uhr UTC wurden die beiden an den Threat Score geknüpften Managed-Challenge-Regeln, eine pro Zone, deaktiviert, mit dem Grund in ihren Beschreibungen; die User-Agent-Challenges bleiben. Live-Prüfungen nach dieser Änderung: die Studio-Website um 16:15 Uhr UTC (Widget, Übermittlungen ohne Token und mit unsinnigem Token 403, Honeypot 200), diese Website um 16:18 Uhr UTC (Widget, Übermittlung ohne Token 403); keine davon löst die Hostname-Regel aus. ↩↩↩
-
Cloudflare, “Custom rules”, Cloudflare-WAF-Dokumentation, abgerufen am 3. November 2026, 14:10:34 Uhr UTC. Was Custom Rules tun, zitiert, und die fünf Regeln des Free-Tarifs mit jeder Aktion außer Log. ↩
-
Cloudflare, “Rate limiting rules”, Cloudflare-WAF-Dokumentation, abgerufen am 3. November 2026, 14:08:57 Uhr UTC. Die Regelparameter; der Präzisionsvorbehalt und die Verzögerung bei der Zähleraktualisierung, zitiert; die Verfügbarkeitstabelle, die dem Free-Tarif eine Zählperiode von 10 Sekunden, ein Mitigation-Timeout von 10 Sekunden, Zählung nach IP und eine Regel gibt. ↩↩↩↩↩↩
-
Ned Batchelder, “Stopping spambots with hashes and honeypots”, erstellt am 21. Januar 2007, abgerufen am 3. November 2026, 14:10:34 Uhr UTC. Die Definition des Honeypots, der Grund, warum Bots auf unsichtbare Felder hereinfallen, die Taxonomie aus Playback und Formularausfüllen und seine beiden Sätze darüber, dass Bots die Maßnahmen aushebeln und man zu CAPTCHAs zurückkehrt, alles zitiert. Die Seite beschreibt die Technik als die Art, wie er seine Website schützt; sie beansprucht nicht, sie erfunden zu haben. ↩↩↩↩↩↩
-
OWASP Cheat Sheet Series, “Credential Stuffing Prevention Cheat Sheet”, abgerufen am 3. November 2026, 14:10:34 Uhr UTC. Sich nicht auf eine einzige vorhersehbare Volumengrenze verlassen; niedriges Volumen pro Adresse hebelt Sperrlisten und Rate Limiting aus; Werkzeuge, die direkt posten, ohne JavaScript auszuführen, alles zitiert. ↩↩↩
-
Cloudflare, “Cloudflare HTTP headers”, Cloudflare-Fundamentals-Dokumentation, abgerufen am 3. November 2026, 14:14:20 Uhr UTC. Die Sondercodes des Headers CF-IPCountry, einschließlich „T1 - Used for clients using the Tor network.“ ↩
-
Cloudflare, “IP Access rules parameters”, Cloudflare-WAF-Dokumentation, abgerufen am 3. November 2026, 14:14:20 Uhr UTC. „T1: Tor exit nodes (country name: Tor)“. ↩
-
The Tor Project, “Bulk Tor Exit Exporter”, check.torproject.org, abgerufen am 3. November 2026, 14:10:34 Uhr UTC. Die Beschreibung der Liste, zitiert, und der Verweis auf CollecTor für frühere Daten; die Liste selbst, zur selben Zeit von https://check.torproject.org/torbulkexitlist abgerufen, enthielt 1.239 Adressen. ↩↩
-
Daten des Autors, Nachtrag zum Ledger. Jede verschiedene Adresse, die in der Acht-Tage-Abfrage das Formular der Studio-Website abschickte, getrennt nach Cloudflares Ländercode, per Skript abgeglichen mit der Bulk-Exit-Liste des Tor Project, abgerufen um 14:10:34 Uhr UTC am 3. November 2026 (1.239 Adressen): 23 der 23 mit T1 gekennzeichneten Adressen auf der Liste und 0 der 12 anderen. Nur die Liste vom 3. November; die Ereignistage wurden nicht gegen das Archiv geprüft. ↩
-
The Tor Project, “Sources”, Tor Metrics, abgerufen am 3. November 2026, 14:10:34 Uhr UTC. TorDNSEL-Exit-Listen „containing the IP addresses of relays that it found when exiting through them“ (mit den IP-Adressen der Relays, die es beim Austritt über sie fand) und ihr Archiv. ↩↩
-
OWASP Automated Threats to Web Applications, “OAT-017 Spamming”, abgerufen am 3. November 2026, 14:10:34 Uhr UTC. Die Zusammenfassung, die anderen Namen einschließlich Formularspam und der Ausschluss von Formular-zu-E-Mail-Missbrauch gegen nicht vorgesehene Empfänger, zitiert. ↩↩
-
Analyse des Autors. Die 108 gespeicherten Nachrichten der Studio-Website, mit festen Regeln sortiert, der erste Treffer gewinnt (Abgleich nach Absendername für die Preisfrage und die Gewinnköder, nach Nachrichtentext für den Rest, eine Zeile von Hand als echte Anfrage eingeordnet), 3. November 2026: die Zahlen in der Tabelle in Abschnitt 5; die 25 Sprachen der Preisfrage über die Tabelle und 9 im Oktober, nach meiner Lesart jedes festen Satzes, mit Kroatisch (oder Bosnisch) und Indonesisch (oder Malaiisch) als knappen Zuordnungen; jede Zeile im Zeitfenster der Abfrage als Spam eingestuft. ↩↩↩↩↩
-
Cloudflare, “Turnstile widgets”, Turnstile-Dokumentation, abgerufen am 3. November 2026, 14:08:57 Uhr UTC. Die Modi Managed, nicht interaktiv und unsichtbar und was ein Besucher jeweils sieht, für Managed zitiert; die Datenschutzbedingung des unsichtbaren Modus; der Origin challenges.cloudflare.com. ↩↩↩
-
Cloudflare, “Cloudflare Turnstile plans”, Turnstile-Dokumentation, abgerufen am 3. November 2026, 14:08:57 Uhr UTC. Free und Enterprise; bis zu 20 Widgets; unbegrenzte Challenges; Nutzung ohne andere Cloudflare-Dienste, zitiert. ↩
-
Cloudflare, “Validate the token”, Turnstile-Dokumentation zur serverseitigen Validierung, abgerufen am 3. November 2026, 14:08:57 Uhr UTC. Die Pflicht, siteverify aufzurufen; gefälschte Tokens; der Endpunkt und seine Felder
secret,responseundremoteip; die Gültigkeit von 300 Sekunden und die Einmalverwendung; das Maximum von 2.048 Zeichen; die Prüfung von Action und Hostname; die Fehlercodetabelle und ihre zwei Beschreibungen des Ablaufs; Rate Limiting gegen Validierungsfluten. Alle zitierten Passagen stammen von dieser Seite. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Cloudflare, “Cloudflare Turnstile - Easy CAPTCHA Alternative”, Produktseite, abgerufen am 3. November 2026, 14:08:57 Uhr UTC. „Turnstile never harvests data for ad retargeting.“ ↩
-
Cloudflare, “Content Security Policy”, Turnstile-Dokumentation, abgerufen am 3. November 2026, 14:08:57 Uhr UTC.
script-srcundframe-srcfür https://challenges.cloudflare.com; der nonce-basierte Ansatz; die Anforderungconnect-src 'self'für Pre-clearance, zitiert. ↩↩↩ -
Cloudflare, “Interstitial Challenge Pages”, Dokumentation zu Cloudflare Challenges, abgerufen am 3. November 2026, 14:08:57 Uhr UTC. Wie eine Challenge-Seite die Anfrage zurückhält; Managed Challenges als Empfehlung für die meisten WAF-Regeln; die Einschränkung bei AJAX und XHR (fetch) und die Empfehlung von Turnstile Pre-clearance, beides zitiert. ↩↩↩↩↩↩
-
Cloudflare, “Detect a Challenge Page response”, Dokumentation zu Cloudflare Challenges, abgerufen am 3. November 2026, 14:10:34 Uhr UTC. Der auf challenge gesetzte Header
cf-mitigatedund der Content-Type text/html jeder Challenge, zitiert. ↩ -
Cloudflare, “Clearance”, Dokumentation zu Cloudflare Challenges, abgerufen am 3. November 2026, 14:10:34 Uhr UTC. Das Cookie
cf_clearanceund Pre-clearance, zitiert: Das Cookie wird zusätzlich zum standardmäßigen Einmal-Token ausgestellt, Siteverify bleibt Pflicht, und ein Cookie umgeht Challenges bis zur Freigabestufe, die am Widget eingestellt ist. ↩↩↩ -
Aufzeichnung des Autors zur zweiten Änderung am 3. November 2026, am selben Vormittag vorgenommen: die Studio-Website um 15:19 Uhr UTC deployt und um 15:22 Uhr UTC live geprüft (Widget, Skript, Site Key und der Challenge-Origin im Live-
frame-src; eine Übermittlung ohne Token und eine mit unsinnigem Token mit 403 abgewiesen; eine Honeypot-Übermittlung mit 200 beantwortet); diese Website ab 15:20 Uhr UTC gebaut, ihr Cache gepurgt und um 15:25 Uhr UTC live geprüft (dieselben Seitenprüfungen, eine Übermittlung ohne Token mit 403 abgewiesen, dann eine Übermittlung mit gültigem CSRF-Token von der Live-Seite und ohne Turnstile-Token erneut mit 403 abgewiesen, was zeigt, dass das Formular nach dem Deployment geschlossen fehlschlägt, die Hostname-Prüfung aber nicht auslösen kann); beide Verifier verlangen jetzt, dass der Hostname in Cloudflares Antwort einer der eigenen der Website ist (eine Antwort ohne wird abgewiesen) und, wenn Cloudflare eine Action zurückgibt, die des Widgets selbst; die elf Tests dieser Website bestehen, wobei der neue den Hostnamen der Website akzeptiert und einen anderen Hostnamen, eine andere Action und einen fehlenden Hostnamen abweist; auf der Studio-Website, lokal gegen Cloudflares Always-Pass-Testschlüssel ausgeführt, der mit einem Platzhalter-Hostnamen und ohne Action antwortet, erreichte ein Dummy-Token mit diesem Hostnamen in der Liste die Validierung und wurde ohne ihn abgewiesen. ↩↩↩↩ -
Cloudflare, “Test your Turnstile implementation”, Turnstile-Dokumentation, abgerufen am 3. November 2026, 14:08:57 Uhr UTC. Die Always-Pass- und Always-Fail-Testschlüssel und das Dummy-Token; Produktions-Secrets weisen Dummy-Tokens ab; automatisierte Testsuiten werden als Bots erkannt, zitiert. ↩↩↩↩↩
-
Aufzeichnung des Autors zur vierten Änderung am 3. November 2026, vorgenommen nach einer zweiten Durchsicht des Entwurfs dieses Beitrags: Der Handler der ersten Version für das Before-Swap-Event von HTMX, der ein 403-Fragment an HTMX’ Standard vorbeilassen soll, keine 4xx-Antwort einzusetzen, hing am Kontaktformular, während HTMX dieses Event auf dem Swap-Ziel feuert, einem Geschwisterelement des Formulars, ein 403-Fragment wurde also nie angezeigt; die Durchsicht fand das, indem sie das gebündelte HTMX und das Template las. Die Korrektur ist ein Listener auf dem Body der Seite, beschränkt auf die id des Antwortziels, und der Test der Startseite stellt sicher, dass er gerendert wird (11 Tests bestehen). Nachdem das Deployment um 16:18 Uhr UTC geprüft war, ersetzte eine Browserprüfung um 16:21 Uhr UTC in Playwright Chromium das 730-Zeichen-Token des Widgets durch einen unsinnigen String, füllte die Pflichtfelder mit Platzhalterwerten und schickte das Formular über HTMX ab; das Antwortziel zeigte das Fehlerfragment „error: Verification failed. Reload the page and try again.“ und kein Erfolgsfragment, und nichts wurde versendet. Nicht durchgeführt: die End-to-End-Übermittlung durch einen Menschen. ↩↩
-
Cloudflare, “Embed the widget”, Turnstile-Dokumentation, abgerufen am 3. November 2026, 14:08:57 Uhr UTC. Das Skript api.js, der Widget-Container, das versteckte Eingabefeld
cf-turnstile-response, das innerhalb eines Formulars erzeugt wird, undturnstile.reset(widgetId)für abgelaufene Widgets, zitiert. ↩↩↩ -
Aufzeichnung des Autors zu den Live-Prüfungen am 3. November 2026, jeweils nachdem das Deployment der eigenen Website erfolgreich war, und nach dem Cache-Purge: um 06:35:24 und 06:35:54 Uhr PST ein Widget-Div, ein Script-Tag und der Site Key auf jeder Startseite, der Challenge-Origin in jedem Live-
frame-src, auf dieser Website eine Übermittlung mit allen Feldern und einem unsinnigen CSRF-Token von der CSRF-Prüfung mit 403 abgewiesen; um 14:43 Uhr UTC auf dieser Website eine Übermittlung mit einem gültigen CSRF-Token von 108 Zeichen von der Live-Seite und ohne Turnstile-Token mit 403 und „Verification failed. Reload the page and try again.“ abgewiesen, und dieselbe Übermittlung mit einem unsinnigen Turnstile-Token mit 403 abgewiesen; auf der Studio-Website eine Übermittlung ohne Token und eine mit unsinnigem Token mit 403 abgewiesen und eine Honeypot-Übermittlung mit 200 beantwortet, ohne dass etwas gespeichert oder gemailt wurde; in Playwright Chromium um 14:39:50 und 14:40:04 Uhr UTC das Widget gerendert (602 mal 73 und 557 mal 71 Pixel), das Turnstile-API-Objekt geladen und das versteckte Antwortfeld auf beiden Seiten binnen sechs Sekunden mit einem Token von 730 Zeichen gefüllt. Nicht durchgeführt: die End-to-End-Übermittlung durch einen Menschen. ↩↩↩ -
Cloudflare, “Rate limiting parameters”, Cloudflare-WAF-Dokumentation, abgerufen am 3. November 2026, 14:08:57 Uhr UTC. Die Parameternamen; in den Tarifen Free, Pro und Business drosselt eine Challenge-Aktion immer, sinngemäß wiedergegeben; eine bestandene Challenge setzt den Zähler zurück, zitiert. ↩
-
OWASP Cheat Sheet Series, “Authentication Cheat Sheet”, abgerufen am 3. November 2026, 14:10:34 Uhr UTC. CAPTCHA als Defense-in-Depth-Kontrolle, zitiert. ↩
-
Resend, “Retrieve Sent Email”, API-Referenz von Resend, abgerufen am 3. November 2026, 14:14:47 Uhr UTC. Das Feld
last_eventeiner abgerufenen E-Mail, zum Beispiel „delivered“. ↩↩ -
Resend, “View and manage emails”, Resend-Dokumentation, abgerufen am 3. November 2026, 14:15:12 Uhr UTC. Die Liste der E-Mail-Events, einschließlich „sent“ und „delivered“, zitiert, und „queued“ nur für Broadcasts und Batches. ↩
-
Cloudflare, “Onion Routing and Tor support”, Dokumentation zu Cloudflare Network, abgerufen am 3. November 2026, 14:08:57 Uhr UTC. Onion Routing in jedem Tarif und die Unterscheidung zwischen menschlichen Tor-Nutzern und Bots, zitiert. ↩
-
Cloudflare, “Challenges”, Dokumentation zu Cloudflare Challenges, abgerufen am 3. November 2026, 14:08:57 Uhr UTC. „Cloudflare does not use CAPTCHA puzzles or visual tests like selecting objects or typing distorted characters.“ ↩
-
OWASP Community, “Blind SQL Injection”, abgerufen am 3. November 2026, 14:10:34 Uhr UTC. Zeitbasierte Blind Injection, die darauf beruht, dass die Datenbank pausiert, mit
WAIT FOR DELAYundpg_sleep()als Beispielen. ↩