Dlaczego spam pokonał mój formularz kontaktowy z limitem żądań
Dlaczego spam przechodzi przez formularz kontaktowy z limitem żądań? Bo limit żądań liczy żądania z danego adresu, a nic w nim nie pyta, kto je wysłał. 30 października i 2 listopada dwa skanery podatności przeszukały moją witrynę, załadowały stronę główną z formularzem, tak jak robi to każdy odwiedzający, pobrały ważny token CSRF i wysyłały formularz, dopóki nie poszły dalej albo nie zostały zablokowane. Pierwszego dnia przeszło 101 zgłoszeń, drugiego 31, każde z odpowiedzią 200 od aplikacji, którą ta zwraca zarówno przy wysyłce, jak i przy nieudanej wysyłce, więc te liczby to najwięcej e-maili, ile mogły wygenerować.12 Limiter mojej aplikacji, ustawiony na pięć zgłoszeń na minutę z jednego adresu, odrzucił 830 i 31 ich zgłoszeń, a przepuścił 101 i 31 wysyłek, o 47 i 9 więcej, niż przepuściłby dokładny limiter przy tym ustawieniu.3 Limit żądań na krawędzi sieci Cloudflare (edge) nie zadziałał przy żadnym ze skanerów: utworzyłem go około 12 minut po ostatnim zgłoszeniu pierwszej fali, a zgłoszenia drugiego skanera przyszły przez dwa centra danych Cloudflare, które prowadzą osobne liczniki. Ten podział to moje najmocniejsze wyjaśnienie drugiej ciszy: pasuje do sum minutowych, które nie mogą go dowieść.456 Na stronie mojego studia pole honeypot złapało 42 z 52 zgłoszeń w ciągu ośmiu dni i przepuściło 10, których nadawcy zostawili honeypot pusty.78 Krawędź tej witryny miała regułę, która potrafi zapytać, czy po drugiej stronie jest człowiek, czyli zarządzane wyzwanie (managed challenge) na ścieżce formularza, ale uruchamia się ono tylko przy ciągu user-agent z listy, a oba skanery wysyłały ciąg przeglądarki; drugie wyzwanie, oparte na wysokim wyniku zagrożenia (threat score), nigdy nie mogło się uruchomić, bo Cloudflare ustawia teraz ten wynik na zero, więc nic nie zadało tego pytania każdemu zgłoszeniu.910411 Od rana 3 listopada Turnstile zadaje je każdemu zgłoszeniu, które mogłoby trafić do mojej skrzynki, w obu formularzach.12
TL;DR
- Dwa skanery, jedna skrzynka. Jeden adres wysłał do mojej witryny 76 429 żądań w 53 jednominutowych przedziałach 30 października; drugi wysłał 10 907 w 22 przedziałach 2 listopada. Oba przeszukały całą witrynę, oba wysyłały ciągi SQL injection w ścieżkach i w nagłówku User-Agent i oba wysyłały formularz kontaktowy: 1065 razy pierwszego dnia i 500 razy drugiego.131412
- Ważny token CSRF dowodzi tylko tego, że klient miał token wydany przez moją witrynę. Każda wysyłka z obu dni go miała, bo moje tokeny są bezstanowe i każdy klient, który pobierze stronę, dostaje jeden. Weryfikacja CSRF odrzuciła cztery zgłoszenia w dwóch falach.1516
- Mój własny limiter przeciekał. Odczytuje licznik z Cloudflare KV, porównuje go, potem odczytuje ponownie i w osobnym żądaniu zapisuje wartość powiększoną o jeden; nieudany drugi odczyt zapisuje jedynkę, a nieudane wywołanie KV przełącza go na licznik trzymany w pamięci jednego procesu. Każda z tych ścieżek może przepuścić więcej niż pięć, a logi nie mówią, która to zrobiła: w jednej minucie wyszło 18 wysyłek przy limicie pięciu.171
- Limit żądań na krawędzi liczy per adres i per centrum danych. 2 listopada 85 zgłoszeń formularza od skanera między 18:56 a 19:05:00 UTC, czyli w minutach po obu stronach blokady, przyszło przez Amsterdam i Londyn, 44 i 41, a najbardziej ruchliwa minuta rozłożyła się 18 do 14 między nimi. Ten podział to najbardziej prawdopodobny powód, dla którego reguła milczała, choć sumy minutowe nie mogą tego dowieść. Test, który przeprowadziłem następnego ranka, dowiódł, że reguła działa i że Cloudflare ją loguje.518
- Blokady były blokadami adresów i obie przyszły za późno, by zatrzymać e-maile. 30 października zablokowałem adres ręcznie 3,9 minuty po tym, jak zauważyłem problem, około 50 minut po jego pierwszym zgłoszeniu formularza. 2 listopada mój monitor zablokował go 8 minut po pierwszej minucie skanowania. Żadna wysyłka w żadnym z tych dni nie nastąpiła po minucie blokady: 30 października ostatnia przyszła ponad 12 minut przed blokadą, a 2 listopada 26 przyszło w minutach przed nią i 5 w samej minucie blokady, której dane minutowe nie potrafią umieścić przed sekundą blokady ani po niej.101912
- Honeypot łapie boty, które wypełniają każde pole. Na stronie mojego studia odpowiedział na 42 z 52 zgłoszeń tak, jakby się powiodły, i niczego nie zapisał; sądząc po godzinie nadejścia, było wśród nich każde zgłoszenie z węzła wyjściowego Tor. 10 zapisanych, a więc najwyżej 10 e-maili, pochodziło od nadawców, którzy zostawili honeypot pusty.720
- Co się zmieniło. Oba formularze mają teraz widżet Cloudflare Turnstile w trybie zarządzanym, a serwer niczego nie zapisuje ani nie wysyła, dopóki API siteverify Cloudflare nie zaakceptuje tokenu zgłoszenia; brakujący, zbyt długi lub niemożliwy do zweryfikowania token jest odrzucany. Turnstile uruchamia wyzwania w przeglądarce odwiedzającego i przetwarza sygnały takie jak adres IP, odcisk TLS i user-agent; Cloudflare twierdzi, że nie dotyka treści wpisanych do formularzy. Nie zatrzymuje człowieka wpisującego spam ręcznie i nie zastępuje limitów żądań, które nadal ograniczają wolumen.122122
1. Pytanie: „czy to ty?”
2 listopada moja skrzynka zapełniła się wiadomościami z formularza kontaktowego, a moje pierwsze pytanie, skierowane do sesji agenta, która obsługuje moją infrastrukturę, brzmiało: czy to moja własna automatyzacja je wysyła? Nie. Wysyłał je skaner, przez formularz kontaktowy tej witryny, a formularz na stronie mojego studia, 941apps.com, przyjmował w tym czasie własną, powolną strużkę, choć nic z niej nie trafiło tego dnia do skrzynki.238 Wszystkie trzy strumienie poczty, alerty mojego monitora i oba formularze, wychodzą z jednego adresu w mojej własnej domenie pocztowej, więc w skrzynce wyglądały tak samo.23
Tematy zdradziły, co się dzieje. Handler kontaktowy tej witryny wysyła mi każde zgłoszenie z tematem „Contact Form:”, po którym następuje to, co nadawca wpisał w pola imienia, więc ładunek skanera staje się wierszem tematu.2423 Formularz na stronie studia zapisuje każdą wiadomość w bazie danych, a potem wysyła ją mailem do tej samej skrzynki.25
Pytanie, które zadałem najpierw, było złe, a odpowiedź na właściwe zajęła dzień zapytań: która z kontroli na tych formularzach mogła odróżnić skaner od człowieka i dlaczego każda przepuściła resztę? Rodzajów kontroli było pięć. Na tej witrynie: firewall na krawędzi z regułami niestandardowymi, limit żądań na krawędzi, token CSRF i limiter w aplikacji. Na stronie studia: reguły krawędziowe tego samego rodzaju, limit żądań na krawędzi i własny limiter aplikacji, który nie odrzucił żadnego z zaobserwowanych zgłoszeń, oraz pole honeypot.9257 Dalsza część wpisu omawia po kolei dwie fale i powolną strużkę, a potem po kolei pięć warstw, z treścią reguły lub zachowaniem kodu dla każdej z nich.
Skąd pochodzą liczby
Każda liczba poniżej pochodzi z analityki żądań i zdarzeń firewalla Cloudflare dla obu stref, odpytanych 3 listopada dla zamkniętych już okien czasowych, a także z tabeli wiadomości strony studia i logu mojego monitora.16 Skrypt zamienił zapisane wyniki tych zapytań w rejestr liczący kilkaset wierszy, każdy z regułą, która go wytworzyła, a wpis bierze swoje liczby z rejestru i jego aneksu i z niczego innego. Tam, gdzie moje notatki z tamtego dnia nie zgadzają się z rejestrem, wygrywa rejestr: notatki z 30 października pisałem kilka minut po fali, gdy Cloudflare wciąż przyjmował jej ostatnie minuty, i zaniżają liczby.1626 Cloudflare przechowuje te zbiory danych przez „at least 31 days of data on every plan” (co najmniej 31 dni danych w każdym planie), więc mogłem policzyć falę po fakcie, ale nie w nieskończoność.27
Dwa rodzaje liczb wymagają ostrzeżenia. Zdarzenia firewalla są próbką, więc używam ich do ustalenia, która reguła zadziałała i kiedy, a zbiorów danych o żądaniach do ustalenia, ile ich było.16 Do tego zgłoszenie formularza, które dostało na tej witrynie odpowiedź 200, to wysyłka albo błąd, na który handler również odpowiada 200 (ma dwie takie ścieżki błędu), a nie dostarczony e-mail, i nie sprawdzałem logu mojego dostawcy poczty. Każda liczba e-maili w tym wpisie jest więc górną granicą.2428
Jak wyglądała normalność
Przed falami w formularzu panował spokój. W ciągu ośmiu dni objętych zapytaniami trzy adresy inne niż oba skanery wysłały formularz z powodzeniem, każdy dokładnie dwa razy, po jednym adresie dziennie 27, 28 i 29 października.29 Nie wiem, czy byli to ludzie. Dwa zgłoszenia z jednego adresu pasują do podwójnego kliknięcia i pasują do bota, który ponawia próbę. Ta niepewność to cały problem w miniaturze: nic w logach ani w kontrolach nie potrafiło tego rozstrzygnąć.
2. Pierwsza fala: 30 października, zablokowana ręcznie
Pierwsza fala przyszła z jednego adresu w sieci, którą regionalny rejestr internetowy przypisuje do BULLIONET FBW NETWORKS SAS, zarejestrowanej we Francji.30 Jej pierwsze żądanie trafiło w minutę 17:59 UTC, czyli 10:59 pacyficznego czasu letniego, a ostatnie w minutę 18:51, czyli 11:51 PDT: 53 jednominutowe przedziały i 76 429 żądań do tej witryny. Dwa niezależne zapytania, jedno minutowe dla tego adresu i jedno obejmujące wszystkie strefy na moim koncie, dają tę samą sumę, a adres nie wysłał niczego wcześniej ani później.13
Kształt skanowania
Formularz był jego niewielką częścią. Przed blokadą adres wysyłał średnio 1389,7 żądania na minutę, ze szczytem 3219 w minucie 18:36, i przeszedł całą witrynę: co najmniej 2881 różnych ścieżek, a to dolna granica, bo zapytanie o ścieżki trafiło na limit wierszy.1331 W całym skanowaniu wyniki rozłożyły się tak:13
| Wynik | Żądania | Udział |
|---|---|---|
| Obsłużone (2xx, 3xx) | 49 513 | 64,8% |
| Odrzucone przez aplikację (4xx), w tym 10 019 przez bramkę agentów witryny | 18 990 | 24,8% |
| Błędy origin (520) | 2339 | 3,1% |
| Zablokowane na krawędzi (403) | 5587 | 7,3% |
Te 7,3% krawędzi to prawie w całości blokada: 5234 z jej 5587 odmów nastąpiło po minucie blokady.13 Przez pięćdziesiąt minut przed nią działała tylko reguła ścieżek exploitów. Ta reguła działała od pierwszej minuty, z pierwszym zdarzeniem firewalla o 17:59:53 UTC, ale dopasowuje fragmenty ścieżek takie jak .php i /wp-, a większość tego skanowania prosiła o prawdziwe strony.109
Skanowaniem, a nie zwykłym crawlowaniem, czyniło to, co wkładało do żądań. Niemal cały ruch, co najmniej 69 322 żądania, używał jednego zwykłego user-agenta Chrome 141 dla Windows. Co najmniej 2755 kolejnych podawało się za Googlebota, a reguła zweryfikowanych botów na krawędzi nie potraktowała ich jak Google.32 Co najmniej 3154 żądania niosły ładunek SQL injection w samym nagłówku User-Agent, w co najmniej 396 różnych ciągach, a co najmniej 2680 żądań niosło go w ścieżce, w co najmniej 810 różnych ścieżkach.3231 Ładunki należą do udokumentowanych rodzin. PG_SLEEP(15), waitfor delay '0:0:15' i XOR(if(now()=sysdate(),sleep(15),0)) to sondy czasowe: ściągawka PortSwiggera podaje składnię opóźnień dla Oracle, Microsoft SQL Server, PostgreSQL i MySQL, a definicje ładunków samego sqlmap zawierają te same funkcje.3334 PortSwigger wyjaśnia, dlaczego to działa: „As SQL queries are normally processed synchronously by the application, delaying the execution of a SQL query also delays the HTTP response.” (ponieważ aplikacja zwykle przetwarza zapytania SQL synchronicznie, opóźnienie wykonania zapytania opóźnia też odpowiedź HTTP).35 Jedna wolna odpowiedź niewiele dowodzi; opóźnienie około 15 sekund, które wraca za każdym razem, gdy ładunek o nie prosi, a nie pojawia się w żądaniach kontrolnych bez niego, jest dowodem możliwej podatności na wstrzyknięcie.
Aplikacja do malowania na tej witrynie ma endpoint udostępniania i skaner go znalazł. Wysłał 320 żądań POST na ścieżkę udostępniania, jako jedyny adres w ciągu ośmiu dni objętych zapytaniami, a endpoint odrzucił wszystkie 320 i niczego nie zapisał. Wokół nich wysyłał żądania na warianty ścieżki z wbudowanym ładunkiem, takie jak /paint/share'||DBMS_PIPE.RECEIVE_MESSAGE(CHR(98)||CHR(98)||CHR(98),15)||' i /paint/(select 198766*667891 from DUAL), na które witryna odpowiadała 404.31 Adres dotknął też jednej innej strefy na moim koncie, wysyłając 387 żądań GET do kiradex.app, wszystkie z odpowiedzią 200, i żadnej więcej.13 W mojej ocenie podążał za linkami z tej witryny do tamtej; linków nie sprawdzałem.
Katalog zautomatyzowanych zagrożeń OWASP ma nazwę dla tej aktywności: OAT-014, Vulnerability Scanning, „Crawl and fuzz application to identify weaknesses and possible vulnerabilities.” (przeszukiwanie aplikacji i fuzzing w celu znalezienia słabości i możliwych podatności).36 Formularz kontaktowy był po prostu kolejnym wejściem do fuzzingu.
Formularz minuta po minucie
Adres wysłał formularz kontaktowy 1065 razy i każde zgłoszenie dotarło do aplikacji; krawędź nie zatrzymała żadnego.1 Pierwsze zgłoszenie i pierwsza wysyłka przyszły w minucie 18:00 UTC, czyli 11:00 PDT. Oto każda minuta, w której zgłoszenie formularza dotarło do aplikacji, według statusu zwróconego przez aplikację:1
| UTC | PDT | Wysłane (200) | Odrzucone przez CSRF (403) | Brak pola (422) | Odrzucone przez limiter (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 |
Sumy: 101 wysłanych, 830 odrzuconych przez limiter aplikacji, 130 odrzuconych z powodu braku wymaganego pola i 4 odrzucone przez weryfikację CSRF, wszystkie cztery w minucie 18:03.1 Odpowiedzi 429 pochodziły od aplikacji. Reguła limitu żądań na krawędzi jeszcze nie istniała, dla tego adresu nie ma żadnego zdarzenia firewalla związanego z limitem żądań, a tam, gdzie Cloudflare zapisał status serwera źródłowego (origin) dla tych zgłoszeń, 429 pochodziło z origin.1037 Odpowiedzi 422 pochodziły z walidacji formularzy FastAPI, która działa przed limiterem, więc nigdy nie zużyły miejsca w limiterze; sprawdziłem kolejność na dziesięciowierszowej aplikacji FastAPI, w której zgłoszenie bez wymaganego pola zwróciło 422, nigdy nie wywołując wrappera.38 Seria 129 o 18:37 wygląda na to, że skaner fuzzował pola formularza, pomijając je.
Proszę porównać kolumnę „Wysłane” z limiterem. Limiter dopuszcza pięć zgłoszeń z adresu na minutę zegarową, a w ośmiu z tych minut wyszło więcej niż pięć, 18 z nich o 18:06.13 Sekcja 4 wyjaśnia, jak to możliwe.
Zauważenie i blokada
Zauważyłem o 11:46 PDT i zanotowałem, że coś w kółko uderza w formularz kontaktowy witryny.26 Pierwsze zgłoszenie formularza miało wtedy około 46 minut, a sama fala na formularzu skończyła się o 11:37 PDT.1 Ręcznie dodałem adres do listy blokad na poziomie konta, do której pasuje już pierwsza reguła niestandardowa strefy, a pierwsze zdarzenie z tej reguły nastąpiło o 18:49:53 UTC, czyli 11:49:53 PDT.10 Blokada nastąpiła 3,9 minuty po tym, jak zauważyłem problem, około 50 minut po pierwszym zgłoszeniu formularza i 50,9 minuty po pierwszej minucie skanowania.113 Wszystkie 101 wysyłek wyszło przed nią; ostatnie zgłoszenie formularza, które dotarło do aplikacji, przyszło około 12 minut przed blokadą.1 Po minucie blokady adres wysłał jeszcze 5234 żądania, a krawędź odrzuciła każde z nich.13
W tej samej sekundzie, o 18:49:53 UTC, utworzyłem jedyną regułę limitu żądań w strefie: żądania POST formularzy na ścieżki kontaktu i udostępniania w aplikacji do malowania, 3 na 10 sekund z adresu, z blokadą na 10 sekund.9 O 18:52:38 UTC przetestowałem ją z mojego komputera. Według moich notatek z tego testu sześć zgłoszeń wysłanych w odstępie sekundy, z user-agentem przeglądarki i bez ważnego tokenu CSRF, „returned 403 403 403 429 429 429 (the app’s CSRF rejection, then the edge’s rate limit)” (zwróciło 403 403 403 429 429 429: odmowę CSRF aplikacji, a potem limit żądań krawędzi), a zwykłe zgłoszenia z curl nigdy nie dotarły do reguły, bo istniejąca reguła niestandardowa podała im zarządzane wyzwanie.26 Notatki mówią, że zgłoszenia nie miały tokenu, ale zgłoszenie bez tego pola dostaje 422, zanim handler w ogóle ruszy, co pokazał mój test z 3 listopada, więc odpowiedzi 403 oznaczają token obecny, ale nieważny.18 Zdarzenia firewalla się zgadzają: trzy zdarzenia limitu żądań z mojego adresu testowego, pierwsze w tej sekundzie, i 24 zarządzane wyzwania dla moich zgłoszeń z curl.39 Żaden mój ruch testowy nie wchodzi do żadnej z powyższych liczb.
Mój monitor tego dnia nie działał. Uruchomiłem go ponownie 31 października, a jego detektor ruchu powstał po tej fali.19 Blokadę z 30 października założył człowiek czytający skrzynkę.
30 października: 76 429 żądań z jednego adresu, minuta po minucie; dopóki ręcznie nie zablokowałem adresu, krawędź prawie żadnego z nich nie odrzuciła.1310
3. Druga fala: 2 listopada, zablokowana przez mój monitor
Drugi adres należy do NovoServe B.V., dostawcy hostingu w Holandii.40 Wysłał dwa żądania GET o 07:49 UTC, czyli 23:49 pacyficznego czasu standardowego 1 listopada, a potem nic przez 11,1 godziny. Właściwe skanowanie trwało od minuty 18:56 UTC do minuty 19:17, czyli od 10:56 do 11:17 PST: 22 jednominutowe przedziały i 10 907 żądań, albo 10 909 z dwoma wczesnymi GET. Cztery niezależne zapytania dają tę samą sumę.14
Wyglądało to na to samo narzędzie. Używało tego samego ciągu user-agent Chrome 141 dla Windows co adres z 30 października w 9725 żądaniach, tego samego ciągu Googlebota w 942, tych samych rodzin wstrzyknięć i dotknęło tych samych dwóch stref i żadnej innej, z 368 żądaniami GET do kiradex.app.414214 Dwa adresy w dwóch sieciach w dwóch krajach niczego nie dowodzą co do tego, kto za nimi stał, więc poprzestaję na „wyglądało”.
Oś czasu w obu strefach czasowych
| UTC | PST | Co się stało |
|---|---|---|
| 18:56 | 10:56 | Skanowanie się zaczyna: 29 żądań w tej minucie.1419 |
| 18:57:00 | 10:57:00 | Startuje przebieg mojego monitora. Jego okno zamyka się o 18:57:22, więc widzi mniej więcej minutę skanowania i niczego nie znajduje.19 |
| 18:57 | 10:57 | Najbardziej ruchliwa minuta przed blokadą, 637 żądań. Reguła ścieżek exploitów na krawędzi zaczyna blokować o 18:57:11.144 |
| 18:58 | 10:58 | Pierwsze trzy zgłoszenia formularza. Aplikacja wysyła wszystkie trzy.2 |
| 19:00 | 11:00 | Cztery kolejne zgłoszenia formularza, wszystkie cztery wysłane.2 |
| 19:02 | 11:02 | 32 zgłoszenia formularza docierają do aplikacji: 9 wysłanych, 23 odrzucone przez limiter aplikacji.2 |
| 19:02:46 | 11:02:46 | Startuje następny przebieg monitora.19 |
| 19:03 | 11:03 | 10 wysłanych, 8 odrzuconych.2 |
| 19:03:53 | 11:03:53 | Mój monitor zgłasza krytyczne wykrycie fali żądań: 1914 żądań z tego adresu.19 |
| 19:04:01 | 11:04:01 | Mój monitor dodaje adres do listy blokad konta.19 |
| 19:04:03 | 11:04:03 | Pierwsze zdarzenie z reguły listy blokad.4 |
| 19:04 | 11:04 | Ostatnie pięć wysyłek i 22 zgłoszenia formularza odrzucone na krawędzi, wszystkie w samej minucie blokady; przedział minutowy nie mówi, co było pierwsze.2 |
| 19:07 | 11:07 | Najbardziej ruchliwa minuta skanowania, 1107 żądań, wszystkie zablokowane.14 |
| 19:17 | 11:17 | Ostatnia minuta, 300 żądań, wszystkie zablokowane.14 |
Osiem minut mojego monitora
Mój monitor zgłosił pierwsze wykrycie 7,9 minuty po pierwszej minucie skanowania, a blokada zaczęła działać 8,0 minuty po niej, czyli 6,8 minuty po pierwszym zdarzeniu firewalla.194 Do tego czasu w minutach przed blokadą wyszło 26 wysyłek, a ostatnie 5 przypadło na samą minutę blokady, której dane minutowe nie potrafią umieścić przed sekundą blokady ani po niej; w kolejnych minutach nie było żadnej.2 Osiem minut to szybko jak na blokadę i wolno jak na skrzynkę pocztową.
Przebieg, który wystartował o 10:57:00 PST, miał skanowanie w polu widzenia przez mniej więcej minutę, a w jego oknie adres wysłał od 29 do 666 żądań; wyższa liczba obejmuje całą minutę 18:57, z której okno widziało tylko 22 sekundy. Detektor nie zalogował żadnego wykrycia.19 Następny przebieg, o 11:02:46 PST, znalazł falę i ją zablokował, a ten przebieg pokazuje, dlaczego pierwszy mógł ją przeoczyć. Jego okno kończyło się w chwili, gdy odpytywał Cloudflare, zanim dotarły najnowsze zdarzenia: jego log odnotowuje 1914 żądań z adresu, podczas gdy moje odtworzenie tego samego pięciominutowego okna 3 listopada, na kompletnych danych, liczy 2143; odtworzenie jest zapisane razem z poleceniami i pełnym wyjściem, a jego liczby są wierszami w aneksie rejestru.1943 Odtworzenie zgłasza też wykrycie fali na formularzu, którego przebieg na żywo nigdy nie zgłosił, z 39 zgłoszeń, z których 23 odrzucono, o wysokiej ważności, co byłoby wierszem w dziennym digeście, a nie e-mailem ani blokadą.43 Odtworzyłem okno drugiego przebiegu, nie pierwszego, więc dla przebiegu z 10:57 PST to samo opóźnienie jest prawdopodobną przyczyną, a nie zmierzoną. Tego samego dnia zmieniłem okna monitora tak, by kończyły się 90 sekund przed każdym przebiegiem i zaczynały tam, gdzie skończył się poprzedni.43 Łącznie na tym adresie uruchomiło się sześć wykryć, a mój monitor włączył każde z nich do dziennego digestu zamiast wysyłać mi e-mail, bo adres był już w jego rejestrze blokad.19 Tym, jak powinien alarmować mój monitor, i tym skanerem widzianym z jego strony zajmuje się osobny wpis; dla tego wpisu liczy się to, że blokada jest reakcją, a reakcja przyszła po co najmniej 26 z 31 wysyłek.
Formularz według statusu
Adres wysłał formularz 500 razy.2
| UTC | PST | Wysłane (200) | Odrzucone przez limiter (429) | Zablokowane na krawędzi (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 |
Sumy: 31 wysłanych, 31 odrzuconych przez limiter aplikacji i 438 odrzuconych na krawędzi, wszystkie w minucie blokady lub później. Do aplikacji dotarły więc 62 zgłoszenia.2 Każda wysyłka dowodzi, że skaner miał ważny token CSRF, bo handler sprawdza token przed wysyłką. Odpowiedzi 429 niczego nie dowodzą w żadną stronę, bo limiter odrzuca zgłoszenie, zanim handler przeczyta jego token.2416 Tam, gdzie Cloudflare zapisał status origin dla tych 429, był to status origin, czyli limitera aplikacji, i dla tego adresu nie ma żadnego zdarzenia firewalla związanego z limitem żądań.374
Reszta skanowania
Według wyniku skanowanie z 2 listopada rozłożyło się tak:14
| Wynik | Żądania | Udział |
|---|---|---|
| Obsłużone (2xx, 3xx) | 1388 | 12,7% |
| Odrzucone przez aplikację (4xx), w tym 420 przez bramkę agentów | 1473 | 13,5% |
| Błędy origin (520) | 116 | 1,1% |
| Zablokowane na krawędzi (403) | 7930 | 72,7% |
Te 72,7% krawędzi to głównie blokada: 2986 żądań przyszło przed minutą blokady, a 7445 po niej, i witryna nie obsłużyła żadnego z tych późniejszych.14 Zapytanie o ścieżki dla tego dnia było kompletne: 2173 różne ścieżki, a 778 żądań na 542 z nich niosło ładunek SQL injection.44 Zapytanie o user-agenty również było kompletne, 73 różne ciągi. Poza zwykłym ciągiem Chrome i podszywaniem się pod Googlebota skaner wysłał 54 różne ładunki SQL w 149 żądaniach, trzy ciągi łamiące cudzysłowy w 78 i 12 tokenów-znaczników, każdy raz.41
Niektórym z tych ciągów warto się przyjrzeć, bo pokazują, o co skaner pytał. (select 198766*667891 from DUAL) to wyrażenie arytmetyczne w selekcie bez tabeli; FROM dual to składnia Oracle dla takiego selektu, co pokazuje ściągawka PortSwiggera.33 if(now()=sysdate(),sleep(15),0) ma ten sam kształt IF(condition, SLEEP(n), x) co warunkowe opóźnienie czasowe w tej samej ściągawce.33 @@qkLpP to prefiks @@, ten sam, którego ściągawka używa w SELECT @@version dla Microsoftu i MySQL, z pięcioma znakami po nim, a skaner wysłał kilkanaście podobnych.3341 Przewodnik testowania OWASP nazywa dwie techniki, którym służą te ciągi: wstrzyknięcie oparte na błędach, które „forces the database to generate an error” (zmusza bazę danych do wygenerowania błędu), oraz opóźnienia czasowe, które „use database commands (e.g. sleep) to delay answers in conditional queries.” (używają poleceń bazy danych, np. sleep, by opóźnić odpowiedzi w zapytaniach warunkowych).45 Żadna ze stron, które pobrałem, nie dokumentuje dokładnie tych ciągów, więc je opisuję i nie przypisuję konkretnemu narzędziu.
2 listopada: 10 907 żądań z jednego adresu, minuta po minucie, z dwoma przebiegami mojego monitora i blokadą, którą założył o 11:04:01 PST.1419
4. Pięć warstw i to, co każda potrafiła rozpoznać
Między tymi botami a moją skrzynką stało pięć rodzajów kontroli. Każdej zadałem to samo pytanie: czy potrafi odróżnić bota od człowieka? Jedno zarządzane wyzwanie w regułach krawędzi może zadać to pytanie odwiedzającemu, ale tylko żądaniom, których ciąg user-agent jest na jego liście, a ciągi skanerów na niej nie były; drugie, oparte na wyniku zagrożenia, nigdy nie mogło się uruchomić, więc nic nie zadało tego pytania każdemu zgłoszeniu. Dwie z kontroli liczą, jedna dopasowuje nazwy i listy, jedna dowodzi, że klient ma token wydany przez witrynę, a jedna łapie tylko boty, które wypełniają ukryte pole. Oto każda z nich: co sprawdza, co zatrzymała i dokładny powód, dla którego przepuściła resztę.
Niestandardowe reguły krawędzi
Co sprawdzają. Na tej witrynie pierwsza reguła niestandardowa blokuje każdy adres z listy na poziomie konta oraz siedem sieci hostingowych według ASN. Kolejna blokuje fragmenty ścieżek exploitów, takie jak .php, /wp- i /.env. Dwie następne podają zarządzane wyzwanie na formularzu kontaktowym i ścieżce administracyjnej: jedna dla pustego user-agenta lub takiego, który zawiera curl, wget, python-requests, httpx, scrapy, HeadlessChrome, PhantomJS albo Selenium, a druga dla wysokiego wyniku zagrożenia Cloudflare. Strona studia ma reguły tego samego rodzaju, z oboma wyzwaniami stosowanymi w całej witrynie i blokadą ścieżek API, z której wyłączony jest formularz kontaktowy.9 Wyzwania oparte na wyniku zagrożenia, po jednym na każdej witrynie, były martwymi regułami: dokumentacja pól Cloudflare mówi, że wynik kiedyś mieścił się w zakresie od 0 do 100, i „Now, the threat score is always 0 (zero).” (teraz wynik zagrożenia zawsze wynosi 0). Reguła, która wymaga wysokiego wyniku, nie pasuje do niczego, więc wyłączyłem obie 3 listopada, w dniu, w którym to przeczytałem; wyzwania oparte na user-agencie zostają.1146 Plan Free Cloudflare dopuszcza pięć reguł niestandardowych, które „work as customized web application firewall (WAF) rules that you can use to perform actions like Block or Managed Challenge on incoming requests.” (działają jak dostosowane reguły zapory aplikacji webowych, za pomocą których można wykonywać na przychodzących żądaniach akcje takie jak Block lub Managed Challenge).47
Co zatrzymały. Ścieżki exploitów, od pierwszej minuty w oba dni.104 30 października żadnego zgłoszenia formularza.110 2 listopada 438 zgłoszeń formularza po blokadzie założonej przez mój monitor.2 Na stronie studia żadnego zgłoszenia formularza: firewall nie zalogował w ciągu tych ośmiu dni żadnego zdarzenia dla żądania POST do formularza.7
Dlaczego reszta przeszła. Żadna reguła nie patrzy na to, czym jest żądanie. Reguła listy dopasowuje adres dopiero wtedy, gdy coś umieści go na liście: 30 października zrobił to człowiek, 2 listopada mój monitor.91019 Wyzwanie oparte na user-agencie dopasowuje ciąg znaków, a oba skanery wysyłały ciąg przeglądarki, co najmniej 69 322 razy pierwszego dnia i 9725 razy drugiego.3241 Wyzwanie oparte na user-agencie działa przeciwko narzędziom, które się przedstawiają: mój własny test z curl 30 października wywołał 24 zarządzane wyzwania.39 Wyzwanie oparte na wyniku zagrożenia nie mogło uruchomić się dla nikogo, a firewall nie zalogował żadnego wyzwania na formularzu dla żadnego z adresów.11104 Węzły wyjściowe Tor i sieci hostingowe spoza siedmiu wymienionych ASN nie są na żadnej liście.
Reguła krawędzi potrafi odróżnić znany adres, znaną sieć albo uczciwe narzędzie od reszty, a jej wyzwanie pyta o obecność człowieka tylko te żądania, które wybiera jej wyrażenie; nie potrafi odróżnić ciągu przeglądarki wysłanego przez skaner od przeglądarki.
Limit żądań na krawędzi
Co sprawdza. Treść reguły jest krótka:9
(http.request.method eq "POST" and http.request.uri.path in {"/api/contact" "/paint/share"})
Liczy pasujące żądania według centrum danych Cloudflare i adresu klienta, dopuszcza 3 na 10 sekund i blokuje na 10 sekund. Rozłożony równomiernie, ten pułap to nominalny odpowiednik 18 na minutę z jednego adresu w jednym centrum danych; sama reguła liczy w okresach 10-sekundowych.916 Plan Free ustala okres, czas blokady i klucz liczenia oraz daje strefie jedną taką regułę: 10-sekundowy okres liczenia, 10-sekundowy czas mitygacji i liczenie według IP.48 Centrum danych również nie jest opcjonalne. „Every rate limiting rule includes the Cloudflare data center ID (cf.colo.id) as a mandatory characteristic” (każda reguła limitu żądań zawiera identyfikator centrum danych Cloudflare jako obowiązkową cechę), mówi dokumentacja Cloudflare, a także „Counters are not shared across data centers, with the exception of data centers associated with the same geographical location.” (liczniki nie są współdzielone między centrami danych, z wyjątkiem centrów powiązanych z tą samą lokalizacją geograficzną).6
Co zatrzymał. Żadnego żądania skanera w żaden z tych dni. Jego jedyne zdarzenia w oknie incydentu to trzy z mojego własnego testu.10439 Strona studia ma własną regułę limitu żądań dla całej witryny, liczoną w ten sam sposób dla wszystkich żądań z wyjątkiem tych od klientów, których Cloudflare uznaje za zweryfikowane boty, i nie odrzuciła ona żadnego zgłoszenia ze strużki: jej firewall nie zalogował w ciągu ośmiu dni żadnego zdarzenia dla zgłoszenia do formularza. Ta reguła liczy każde takie żądanie, które adres wysyła do witryny, a moje dane obejmują tylko zgłoszenia formularza, więc nie mierzą, ile zapasu zostało licznikowi.97
Dlaczego reszta przeszła. 30 października reguła nie istniała aż do 18:49:53 UTC, tej samej sekundy co pierwsze zdarzenie ręcznej blokady, około 12 minut po tym, jak ostatnie zgłoszenie formularza dotarło do aplikacji.91 Wszystkie 1065 zgłoszeń tego skanera przyszło przez jedno centrum danych, Paryż, więc obowiązująca reguła prowadziłaby dla nich jeden licznik.5 2 listopada reguła istniała, a zgłoszenia formularza od skanera przyszły przez dwa centra danych, Amsterdam z 44 i Londyn z 41, między 18:56 a 19:05:00 UTC. W najbardziej ruchliwej minucie, 19:02, Londyn obsłużył 18, a Amsterdam 14.5 Osiemnaście na minutę to nominalny odpowiednik pułapu reguły, ale moje dane zawierają sumy minutowe, a nie czasy nadejścia, więc nie mogą pokazać, czy w którymkolwiek 10-sekundowym okresie w jednym centrum danych było więcej niż trzy. Cloudflare mówi wprost o precyzji: „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.” (reguły limitu żądań nie są zaprojektowane tak, by do serwera źródłowego docierała precyzyjna liczba żądań; między wykryciem żądania a aktualizacją liczników może minąć do kilku sekund).48 W siedmiominutowym okresie od pierwszego zgłoszenia, które dotarło do aplikacji, do ostatniego skaner wysyłał średnio 8,86 na minutę.2 Moje notatki z tamtego dnia nazwały to „about nine a minute” (około dziewięciu na minutę) i na tym poprzestały; umknął im podział między centra danych.23
Chciałem dowodu, że reguła w ogóle działa, więc rano 3 listopada przetestowałem ją z mojego komputera. O 06:36:49 PST wysłałem dziesięć żądań POST w odstępach 0,4 sekundy bez tokenu CSRF: cztery wróciły z 422, bo brakowało wymaganego pola i FastAPI je odrzucił, a potem sześć wróciło z 429. O 06:38:03 PST wysłałem osiem ze wszystkimi polami: trzy wróciły z 403, czyli odmową CSRF z origin, a potem pięć wróciło z 429 i treścią „error code: 1015” od Cloudflare. Firewall zalogował w tych minutach 11 zdarzeń na formularzu, wszystkie będące blokadami z reguły limitu żądań.18 Reguła działa, a Cloudflare to loguje. Moje odczytanie 2 listopada to hipoteza: rozłożony na dwa liczniki i działający blisko nominalnego pułapu każdego z nich, skaner nie uruchomił żadnego. To najmocniejsze wyjaśnienie zgodne z sumami minutowymi, a nie demonstracja; sumy nie pokazują, jak zgłoszenia były rozłożone w obrębie każdej minuty, a Cloudflare przyznaje, że nadmiarowe żądania mogą przejść, zanim liczniki się zaktualizują.548
Limit żądań potrafi odróżnić szybkie od wolnego, dla jednego adresu w jednym centrum danych; nie potrafi powiedzieć, kto wysyła.
Token CSRF
Co sprawdza. Formularz tej witryny zawiera ukryty token, wstawiony do strony, złożony z trzech części: znacznika czasu, wartości losowej i podpisu HMAC dwóch pierwszych. Serwer go akceptuje, jeśli podpis się zgadza, a znacznik czasu ma mniej niż godzinę.15 Formularz na stronie studia nie ma tokenu CSRF.25
Co zatrzymał. Cztery zgłoszenia 30 października, wszystkie w minucie 18:03, i żadnego 2 listopada.12
Dlaczego reszta przeszła. Token jest bezstanowy. Nie jest powiązany z sesją, plikiem cookie ani adresem, więc każdy klient, który pobierze stronę, dostaje ważny token, a każdy klient, któremu inny przekaże token, może go przedstawić, dopóki jest ważny. Oba skanery rzeczywiście pobierały strony witryny; to pokazują logi żądań. To, co pokazuje sama weryfikacja, jest węższe: każda wysyłka z obu dni miała ważny, nieprzeterminowany token wydany przez moją witrynę, bo handler sprawdza token przed wysyłką.1513142416 Token CSRF istnieje po to, by inna witryna nie mogła sprawić, że przeglądarka odwiedzającego wyśle mój formularz. Nigdy nie miał zatrzymywać klienta, który sam chce wysłać formularz, i tego nie zrobił.
Ta sama weryfikacja miała drugą wadę, a ta uderzała w ludzi, nie w boty. Gdy token zawodził, handler zwracał 403 w formacie JSON, którego formularz HTMX nigdy nie wyświetlał, więc osoba z przeterminowanym tokenem kliknęłaby Wyślij i nie zobaczyłaby nic.12 Naprawiłem to, choć moja pierwsza poprawka również nie zadziałała; sekcja 6 opisuje, jak było.
Ważny token CSRF dowodzi, że klient miał nieprzeterminowany token wydany przez moją witrynę; nie może dowieść, kto wypełnił formularz, ani nawet tego, że ten klient załadował stronę.
Limiter w aplikacji
Co sprawdza. Handler tej witryny stoi za dekoratorem, który liczy zgłoszenia formularza z adresu klienta na minutę zegarową w Cloudflare KV, z zapasowym licznikiem w pamięci, i po pięciu odrzuca kolejne z kodem 429.17 Strona studia ma limiter w pamięci, osobny dla każdego procesu, dla anonimowych klientów, liczony według adresu i klasyfikacji klienta, czyli klasy, którą limiter przypisuje na podstawie user-agenta i adresu, na wszystkich ścieżkach z wyjątkiem health checków i plików statycznych.25
Co zatrzymał. 830 zgłoszeń 30 października i 31 dnia 2 listopada. Żadnego na stronie studia.127
Dlaczego reszta przeszła. Z założenia przepuszcza każdemu pięć na minutę, a ponieważ oknem jest minuta zegarowa, seria rozciągnięta na granicę minuty dostaje dwa przydziały.17 Tyle wiedziałem. Czego nie wiedziałem, dopóki nie zestawiłem liczb z kodem, to że limiter przecieka, a kod ma co najmniej trzy sposoby, by przeciekać. Limiter odczytuje licznik z KV przez HTTP, porównuje go z limitem, a potem w osobnym żądaniu zapisuje wartość powiększoną o jeden, więc dwa zgłoszenia, które oba odczytają licznik, zanim którekolwiek zapisze, oba przejdą. Inkrementacja odczytuje licznik po raz drugi przed zapisem, a jeśli ten drugi odczyt zawiedzie, zapis ustawia licznik z powrotem na jeden, więc minuta zaczyna się od nowa. A gdy odczyt z KV nic nie zwraca albo zapis się nie udaje, decyzja przechodzi na listę trzymaną w pamięci jednego procesu, której inne procesy nie współdzielą.17
Liczby pokazują przeciek. 30 października osiem minut miało więcej niż pięć wysyłek, a minuta 18:06 miała ich 18.1 Dokładny limiter przy pięciu na minutę przepuściłby najwyżej 54 wysyłki w minutach, w których skaner wysyłał zgłoszenia, a wyszło 101, o 47 ponad pułap. 2 listopada minuty 19:02 i 19:03 przepuściły 9 i 10; dokładny limiter przepuściłby najwyżej 22 z 62 zgłoszeń, które zobaczył, a wyszło 31, o 9 ponad pułap.3 Liczby nie pokazują, która ścieżka przeciekła. W mojej ocenie zgłoszenia odczytały ten sam stan licznika, zanim którekolwiek zapisało, albo wywołania KV zawodziły pod obciążeniem, resetując licznik lub oddając decyzję listom per proces, albo zadziałała jakaś mieszanka tych przyczyn; logi, które mam, zapisują status zwrócony przez aplikację, a nie to, która ścieżka o nim zdecydowała, więc nie potrafią rozróżnić ścieżek.173 Lekarstwo jest dla wszystkich takie samo i nie jest to zmiana jednej linii: magazyn liczników z atomową inkrementacją, czego odczyt z KV, a po nim osobny zapis, nie zapewnia. Przeciek to mój błąd. Wyostrza on też argument tego wpisu bardziej, niż mógłbym to zrobić celowo: kontrola, która mówi pięć, a przepuszcza osiemnaście, nigdy nie miała być tym, co decyduje, kto może do mnie pisać.
Na stronie studia limiter aplikacji nie odrzucił żadnego z zaobserwowanych zgłoszeń, a najbardziej aktywny adres wysłał 13 w ciągu godziny. Jego liczniki obejmują żądania adresu do wszystkich innych ścieżek w tej samej klasyfikacji klienta, a moje dane obejmują tylko zgłoszenia formularza, więc liczby nie mierzą, ile zapasu miały te liczniki.725
Limiter potrafi odróżnić pięć od sześciu, gdy żądania przychodzą po kolei; przy żadnej liczbie nie odróżni człowieka od skryptu.
Honeypot
Co sprawdza. Formularz na stronie studia ma pole tekstowe o nazwie website, którego człowiek nigdy nie widzi: ma tabindex="-1" i aria-hidden, więc nie trafi na nie ani klawiatura, ani czytnik ekranu. Jeśli wróci z jakąkolwiek treścią, handler odpowiada tym samym komunikatem o sukcesie, który zobaczyłby człowiek, i niczego nie zapisuje ani nie wysyła.25 Ta gałąź jest w handlerze od czasu, gdy zbudowałem formularz 5 czerwca.25 Formularz tej witryny nie ma honeypota.24
Ned Batchelder opisał tę technikę w 2007 roku: „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” (takie pola nazywa się honeypotami i sprawdza się je przy wysłaniu formularza; jeśli zawierają jakikolwiek tekst, nadawca musi być botem).49 Jego zdaniem technika działa, bo boty wypełniające formularze „cannot distinguish invisible fields from visible ones.” (nie potrafią odróżnić pól niewidocznych od widocznych).49
Co zatrzymał. 42 z 52 zgłoszeń w ciągu ośmiu dni, czyli 80,8%, w tym, sądząc po godzinie nadejścia, każde zgłoszenie z węzła wyjściowego Tor oraz, sądząc po godzinie i wzorcu wiadomości, wszystkie 13 z jednej serii samozwańczego SQL spidera.720
Dlaczego reszta przeszła. Bot, który wypełnia tylko widoczne pola, nigdy nie dotyka website, a nadawcy, których zgłoszenia zostały zapisane i mogły trafić do mojej skrzynki, zostawili to pole puste.825 To, że celowo wybrali widoczne pola, to moja interpretacja; handler zapisuje tylko, że ukryte pole wróciło puste. Batchelder to przewidział: „In theory, it is possible for a spambot to defeat any of these measures. But in practice, bots are very stupid” (w teorii spambot może pokonać każdy z tych środków, ale w praktyce boty są bardzo głupie).49 Niektóre są mniej głupie od innych, a najmniej głupie są te, które sprzedają generowanie leadów.
Honeypot dowodzi tylko tego, że klient wypełnił pole, którego człowiek nie widzi; puste niczego nie dowodzi.
Jak daleko dotarł każdy strumień: zgłoszenia obu skanerów na tej witrynie i osiem dni strużki na stronie studia.1278
Co łączyło wszystkie pięć
Wystarczy przeczytać te pięć zdań razem, a luka staje się oczywista. Limity żądań ograniczają wolumen z adresu. Token CSRF dowodzi, że klient miał token wydany przez witrynę. Honeypot dowodzi, że klient wypełnił ukryte pole. Reguły krawędzi znają nazwy i listy, a ta, która mogłaby zapytać, czy po drugiej stronie jest człowiek, czyli wyzwanie oparte na user-agencie, pyta tylko żądania, których ciąg user-agent jest na jej liście; wyzwanie oparte na wyniku zagrożenia obok niej nigdy nie mogło się uruchomić. Żaden ze skanerów nie pasował, więc nic nie zadało tego pytania każdemu zgłoszeniu, a skanery nie zrobiły niczego, czego nie potrafi przeglądarka: pobrały stronę, zachowały jej token i wysyłały w tempie, które limity częściowo przepuściły. Wytyczne OWASP dotyczące credential stuffingu mówią to samo o innym formularzu, zalecając obronę, która „not rely on a single predictable volume limit” (nie opiera się na jednym przewidywalnym limicie wolumenu).50
5. Powolna strużka na stronie studia
Formularz na stronie studia nigdy nie przeżył fali. Przeżywał strużkę, a strużka to zupełnie inne zwierzę. Okno obejmuje tu okres od pierwszej godziny ze zgłoszeniem formularza w ośmiodniowym zapytaniu, 17:00 UTC 26 października, do chwili, w której zapisałem dane, 13:59:50 UTC 3 listopada; pierwszy i ostatni dzień są niepełne.7
Zgłoszenia a zapisane wiersze
Formularz przyjął w tym oknie 52 żądania POST i na każde odpowiedział 200. W tym samym oknie w tabeli wiadomości przybyło 10 wierszy. Ponieważ gałąź honeypota to jedyna ścieżka w handlerze, która odpowiada 200 i niczego nie zapisuje, honeypot złapał pozostałe 42, czyli 80,8%.7825 Według dni UTC:78
| Data (UTC) | Zgłoszenia (200) | Zapisane wiersze | Złapane przez honeypot |
|---|---|---|---|
| 26 paź (niepełny) | 4 | 2 | 2 |
| 27 paź | 5 | 0 | 5 |
| 28 paź | 3 | 2 | 1 |
| 29 paź | 5 | 1 | 4 |
| 30 paź | 16 | 1 | 15 |
| 31 paź | 8 | 2 | 6 |
| 1 lis | 4 | 0 | 4 |
| 2 lis | 5 | 0 | 5 |
| 3 lis (niepełny) | 2 | 2 | 0 |
Skok z 30 października to jeden adres w Wielkiej Brytanii, który w ciągu jednej godziny wysłał 13 zgłoszeń z user-agentem Mozilla/5.0 (compatible; SQLSpider/1.5). Sądząc po godzinie i wzorcu wiadomości, tabela nie zapisała żadnego z nich: jedyny wiersz zapisany w tej godzinie to oferta generowania leadów, wzorzec amerykańskiego adresu, który również wysyłał w tej godzinie.7820
Kto wysyłał
Cloudflare oznacza żądania z sieci Tor specjalnym kodem kraju, T1. Jego dokumentacja nagłówków mówi „T1 - Used for clients using the Tor network” (T1: używany dla klientów korzystających z sieci Tor), a strona reguł IP Access mówi „T1: Tor exit nodes (country name: Tor)” (T1: węzły wyjściowe Tor, z nazwą kraju Tor).5152 Według tego kodu 23 z 52 zgłoszeń pochodziły z Tora, czyli 44,2%, a 23 z 35 różnych adresów były węzłami wyjściowymi Tor, czyli 65,7%.7 Dla kontroli porównałem każdy adres, który wysłał formularz, z zbiorczą listą węzłów wyjściowych Tor Project, którą projekt nazywa „a nearly real time authoritative source for Tor exits that allow contacting your server on port 80” (miarodajnym źródłem, niemal w czasie rzeczywistym, węzłów wyjściowych Tor, które pozwalają łączyć się z serwerem na porcie 80). Wszystkie 23 adresy, którym Cloudflare nadał kod T1, były na liście, którą pobrałem 3 listopada, a żaden z pozostałych 12 nie był.5354 Oba źródła się zgadzają. Ta lista to jednak węzły wyjściowe z 3 listopada, więc potwierdza kod dla tego dnia, a nie dowodzi, które adresy były węzłami wyjściowymi w dniu nadejścia każdego zgłoszenia. Archiwum CollecTor projektu przechowuje dawne listy węzłów wyjściowych i rozstrzygnęłoby przynależność w dniu zdarzenia; nie odpytywałem go.55
Większość adresów w tym oknie wysłała formularz raz: 32 z 35, czyli 91,4%, w tym każdy węzeł wyjściowy Tor.7 Trzy, które wysłały więcej niż raz, to SQL spider z 13 zgłoszeniami, adres w Polsce z dwoma w ciągu godziny i adres w Stanach Zjednoczonych z pięcioma. Pięć zgłoszeń tego ostatniego przypada na te same pięć godzin co pięć ofert generowania leadów zapisanych przez tabelę w tym oknie, a za każdym razem zmieniał user-agenta: Chrome 135 na Linuksie, potem Firefox 128, 137, 138 i znowu 137.720
User-agenty podzieliły się według tej samej linii. Spośród zgłoszeń z Tora 18 wskazywało urządzenie z Androidem, a 5 Chrome na komputerze stacjonarnym, przy czym 21 z 23 wskazywało Chrome 81 lub starszy. Zgłoszenia spoza Tora wskazywały znacznie nowsze przeglądarki desktopowe, 10 desktopowych Chrome od 125 do 135 i 6 Firefoksów od 128 do 138, plus 13 od spidera pod jego własną nazwą.7
Kto został zapisany
Obie połowy spotkały się przy honeypocie. Żaden wiersz zapisany w oknie nie przypada na godzinę, w której były wyłącznie zgłoszenia z Tora, a wszystkie 10 przypada na godzinę ze zgłoszeniem spoza Tora. 22 z 23 zgłoszeń z Tora przypadły na godziny bez żadnego zapisanego wiersza, a 23. dzieli godzinę z amerykańskim adresem generującym leady.20 Dopasowywałem według godziny, bo logi żądań i tabela wiadomości nie mają wspólnego identyfikatora żądania, więc dopasowanie jest wnioskiem: sądząc po godzinie nadejścia, każde zgłoszenie z Tora w tym oknie wpadło w honeypot.20 To, co zostało zapisane, a więc mogło trafić do skrzynki, pochodziło z adresów spoza Tora, które wysyłały nowsze ciągi przeglądarek desktopowych i zostawiały honeypot pusty.
Liczby pasują do starego wzorca spamu w formularzach. Taksonomia Batcheldera z 2007 roku dzieli twórców spamu na „Playback spambots, form-filling spambots, and humans.” (spamboty odtwarzające, spamboty wypełniające formularze i ludzi).49 Strużka z Tora zachowuje się jak boty wypełniające formularze, które wypełniają każde pole i wpadają w honeypot. Nadawcy, którzy przeszli, zachowują się jak coś bliższego człowiekowi przy klawiaturze albo skryptowi zbudowanemu tak, by go przypominał. OWASP klasyfikuje tę aktywność jako OAT-017, Spamming: „Malicious or questionable information addition to public or private content, or messages.” (dodawanie złośliwych lub wątpliwych informacji do publicznych lub prywatnych treści albo wiadomości).56 OWASP wyłącza też jedną rzecz: „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” (masowe nadużywanie wadliwych funkcji formularz-e-mail i formularz-SMS do wysyłania wiadomości niezamierzonym odbiorcom nie należy do tego zagrożenia).56 Moje formularze piszą tylko do mnie, więc są przypadkiem objętym definicją, czyli spamem skierowanym do właściciela witryny, a nie tamtym.
Strużka z założenia pokonuje też limity żądań. Wytyczne OWASP dotyczące credential stuffingu opisują skutek rozłożenia żądań na wiele adresów: „This may defeat both IP block-lists and rate limiting, as per IP request volume may remain relatively low” (to może pokonać zarówno listy blokad IP, jak i limity żądań, bo wolumen na jeden IP może pozostać stosunkowo niski).50 Jedno zgłoszenie z adresu to najniższy możliwy wolumen.
Co mówią zapisane wiadomości
Tabela wiadomości sięga 6 czerwca i w chwili odczytu miała 108 wierszy; najnowszy przyszedł o 07:24 UTC 3 listopada.8 Posortowałem je według wzorca stałymi regułami, wygrywa pierwsze dopasowanie:57
| Wzorzec | Cała tabela | Październik |
|---|---|---|
| „Hi, I wanted to know your price” (dzień dobry, chciałem poznać Państwa cenę), jeden język na wiersz, jedna nazwa nadawcy | 49 | 10 |
| Oferty dostawców: leady webowe, wzrost na Instagramie, film objaśniający, SEO, dane B2B, katalog, projektowanie | 22 | 4 |
| Przynęty nagrodowe: jackpot, kod promocyjny, PlayStation, Lamborghini, linki do Rolls-Royce’a, jedna nazwa nadawcy | 16 | 3 |
| Link „$50,000,000 per month” (50 milionów dolarów miesięcznie) w telegra.ph | 9 | 9 |
| Oferta generowania leadów: „If I were a real customer, how soon would I get a call back?” (gdybym był prawdziwym klientem, jak szybko ktoś by do mnie oddzwonił?) | 6 | 5 |
| Inne: dwa ciągi bełkotu, prośba o listę mailingową, notka z prośbą, bym pisał gdzie indziej | 4 | 1 |
| Mój własny test powiadomień e-mail, 21 września | 1 | 0 |
| Prawdziwe zapytanie, w mojej ocenie | 1 | 0 |
Kilku z nich warto przyjrzeć się bliżej. Dziewięć wierszy o 50 milionach przyszło pod jedną nazwą nadawcy z dziewięciu różnych adresów nadawcy, a każdy był drobną wariacją tego samego nagłówka.8 Pytanie o cenę pojawia się w całej tabeli w 25 językach: afrikaans, ormiańskim, baskijskim, białoruskim, bengalskim, bułgarskim, katalońskim, chorwackim, duńskim, angielskim, gruzińskim, greckim, węgierskim, islandzkim, indonezyjskim, irlandzkim, włoskim, łacinie, łotewskim, litewskim, luksemburskim, hiszpańskim, wietnamskim, walijskim i zulu. W samym październiku przyszło w dziewięciu: ormiańskim, baskijskim, duńskim, indonezyjskim, łacinie, łotewskim, litewskim, hiszpańskim i wietnamskim.57 Dwa z tych przypisań językowych są moje i niepewne: zdanie, które odczytałem jako chorwackie, może być bośniackie, a to, które odczytałem jako indonezyjskie, może być malajskie.57 Ktoś przeprowadza rozmówki przez formularz kontaktowy, język po języku.
Tempo to druga sygnatura strużki. Październik przyniósł 32 zapisane wiersze, 1,03 dziennie, w 21 z 31 dni, i nigdy więcej niż trzy w jednym dniu.8 Żaden limit żądań per adres nie zadziała przy jednym dziennie.
Które trafiły do skrzynki
Formularz wysyła e-maile do skrzynki studia dopiero od 21 września, od 17:46:47 UTC. Od tego czasu zapisał, a więc wysłał mailem, 43 wiersze; to górna granica, bo wywołanie powiadomienia działa w trybie best effort, a czasu wdrożenia nie zweryfikowałem; 65 wierszy zapisanych przed tą datą nigdy nie wysłano mailem.258 Każdy wiersz w oknie dowodowym to spam.57
Poza moim własnym testem jeden wiersz w całej tabeli spamem nie jest. 14 września klient jednego z produktów studia napisał o konkretnym błędzie, ludzkim głosem, a wiadomość leżała w tabeli i nikt jej nie czytał.57 Gdy tydzień później podłączyłem formularz do wysyłki e-maili do skrzynki, komunikat commita mówił wprost dlaczego: „a paying customer’s message sat unread in D1 for a week” (wiadomość płacącego klienta przez tydzień leżała nieprzeczytana w D1).25 Ta wiadomość jest powodem, dla którego istnieje mechanizm powiadomień, i powodem, dla którego nie mogę naprawić spamu, wyłączając go. Nadawcę zostawiam tu anonimowym; chodzi o to, że prawdziwy człowiek jest w tej samej tabeli co rozmówki.
6. Czym jest weryfikacja botów i co wdrożyłem
Turnstile słowami Cloudflare
Cloudflare nazywa Turnstile swoją „smart CAPTCHA alternative” (inteligentną alternatywą dla CAPTCHA) i twierdzi, że „can be embedded into any website without sending traffic through Cloudflare and works without showing visitors a CAPTCHA.” (można go osadzić w dowolnej witrynie bez kierowania ruchu przez Cloudflare i działa bez pokazywania odwiedzającym CAPTCHA).21 Mechanizm to ta część, która odpowiada na moje pytanie. „First, we run a series of small non-interactive JavaScript challenges to gather signals about the visitor or browser environment” (najpierw uruchamiamy serię małych, nieinteraktywnych wyzwań JavaScript, by zebrać sygnały o odwiedzającym lub środowisku przeglądarki), mówi przegląd, a także „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.” (te wyzwania obejmują proof-of-work, czyli łamigłówki obliczeniowe, proof-of-space, sondowanie webowych API i różne inne wyzwania wykrywające osobliwości przeglądarek i ludzkie zachowanie).21 Limit żądań pyta: ile. Token CSRF pyta: czy klient ma token wydany przez witrynę. Turnstile pyta: jaki rodzaj klienta jest po drugiej stronie. Moje wyzwanie oparte na user-agencie też mogło o to pytać, ale tylko żądań, których ciąg user-agent był na jego liście, a moje wyzwanie oparte na wyniku zagrożenia nigdy nie mogło się uruchomić; różnica, która się tu liczy, to wyzwanie uruchamiane wybiórczo kontra weryfikacja wymagana od każdego zgłoszenia.
Widżet ma trzy tryby. Używam zarządzanego, który Cloudflare opisuje jako taki, który „Automatically chooses between non-interactive or checkbox challenge based on visitor risk level. No images or text to decipher.” (automatycznie wybiera między wyzwaniem nieinteraktywnym a polem wyboru na podstawie poziomu ryzyka odwiedzającego; bez obrazków i tekstu do odszyfrowania).58 Mówiąc prościej, odwiedzający widzi krótką weryfikację i nic więcej, chyba że sygnały wymagają więcej, a wtedy dostaje pole do zaznaczenia. Tryb niewidoczny ukrywa nawet to, a Cloudflare stawia mu warunek: witryna, która go włącza, musi w swojej polityce prywatności odwołać się do Turnstile Privacy Addendum Cloudflare.58 Turnstile ma własny darmowy plan, do 20 widżetów, z nieograniczoną liczbą wyzwań, a Cloudflare twierdzi, że „can be used independently without requiring other Cloudflare services.” (można go używać niezależnie, bez innych usług Cloudflare).59
Połowa serwerowa, czyli ta, która się liczy
Sam widżet nic nie daje formularzowi i Cloudflare mówi to w pierwszych wierszach swojego przewodnika po stronie serwera: „You must call the Siteverify API to complete your Turnstile implementation. The client-side widget alone does not protect your forms.” (aby dokończyć implementację Turnstile, trzeba wywołać API Siteverify; sam widżet po stronie klienta nie chroni formularzy). A potem jeszcze dobitniej: „Tokens can be forged. An attacker can submit any string to your form endpoint without completing a challenge.” (tokeny można sfałszować; atakujący może wysłać dowolny ciąg do endpointu formularza bez rozwiązania wyzwania).60 Serwer wysyła token do https://challenges.cloudflare.com/turnstile/v0/siteverify wraz ze swoim kluczem tajnym i może dołączyć adres IP odwiedzającego jako remoteip.60
Dwie właściwości tokenu kształtują kod wokół niego. „Each token is valid for 300 seconds (5 minutes) after generation.” (każdy token jest ważny przez 300 sekund, czyli 5 minut, od wygenerowania). Oraz „Tokens are single-use. Each token can only be validated once. A replayed token will be rejected with the timeout-or-duplicate error code.” (tokeny są jednorazowe; każdy token można zweryfikować tylko raz, a token użyty ponownie zostanie odrzucony z kodem błędu timeout-or-duplicate).60 Skaner, który zdobyłby jeden token, nie mógłby wysłać nim formularza 1065 razy. Cloudflare ogranicza też token do 2048 znaków i każe serwerowi „Validate the action and hostname when specified.” (sprawdzać akcję i nazwę hosta, gdy są podane).60 Jedno zastrzeżenie z tej samej strony: w jednym miejscu podaje timeout-or-duplicate dla przeterminowanego tokenu, a w tabeli błędów opisuje invalid-input-response jako „Token is invalid, malformed, or expired” (token jest nieważny, zniekształcony lub przeterminowany), więc serwer powinien traktować każdy błąd jako odmowę, zamiast opierać logikę na jednym kodzie.60
Co przetwarza, słowami Cloudflare
Cloudflare nie twierdzi, że Turnstile nie używa danych osobowych, i ja też tego nie twierdzę. Jego aneks o prywatności Turnstile wymienia sygnały: „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.” (Turnstile przetwarza różne sygnały po stronie klienta, takie jak adres IP klienta, odcisk TLS, nagłówek User-Agent oraz sitekey i powiązany origin). Podaje cel: „The purpose of collecting these Signals is not to identify, profile or target any individuals but solely to detect and block bots.” (celem zbierania tych sygnałów nie jest identyfikowanie, profilowanie ani targetowanie żadnych osób, lecz wyłącznie wykrywanie i blokowanie botów). Mówi też, że Cloudflare używa ich do ulepszania produktu: „Cloudflare is a data controller of Signals that we process to improve Turnstile’s bot detection capabilities.” (Cloudflare jest administratorem danych sygnałów, które przetwarza, by ulepszać wykrywanie botów w Turnstile).22 Tym, czego Cloudflare według własnych słów nie dotyka, jest treść: „Turnstile does not access, store, or transmit user communications, form entries, or other page inputs.” (Turnstile nie uzyskuje dostępu do komunikacji użytkowników, treści formularzy ani innych danych wpisywanych na stronie, nie przechowuje ich ani nie przesyła).21 Strona marketingowa dodaje, że „Turnstile never harvests data for ad retargeting.” (Turnstile nigdy nie zbiera danych do retargetingu reklam).61 W mojej ocenie Turnstile widzi ten sam rodzaj danych o połączeniu, który moja krawędź i tak loguje dla każdego żądania, i nigdy nie widzi tego, co człowiek wpisuje w mój formularz.
Potrzebna polityka CSP
Turnstile ładuje się z originu wyzwań Cloudflare, a Content Security Policy musi na to pozwalać. Cloudflare wymienia dwie dyrektywy: „script-src: https://challenges.cloudflare.com” i „frame-src: https://challenges.cloudflare.com”.62 Obie moje strefy ustawiają politykę w regule nagłówków odpowiedzi Cloudflare. Na tej witrynie lista ramek już istniała i dostała origin wyzwań. Strona studia nie miała w ogóle frame-src, więc jej default-src 'self' zablokowałoby iframe widżetu; dostała frame-src 'self' plus origin wyzwań, a własna polityka aplikacji w kodzie dostała to samo.12
Dlaczego nie zarządzane wyzwanie na samym żądaniu POST formularza
Oczywisty skrót był już w mojej strefie: reguła niestandardowa, która odpowiada na POST do formularza zarządzanym wyzwaniem. Moje istniejące reguły robią to dla curl i jego krewnych i to działa, bo żaden człowiek nie używa curl, żeby wysłać mi wiadomość.939 Rozszerzenie tego na każde żądanie POST zepsułoby formularz wszystkim. Cloudflare dokumentuje to ograniczenie wprost: „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.” (strony wyzwań przerywają przepływ żądania, zwracając pełną stronę HTML, którą przeglądarka ma wyrenderować i rozwiązać; ten mechanizm zawodzi, gdy przeglądarka oczekuje odpowiedzi innej niż HTML, na przykład przy żądaniu AJAX lub XHR).63 Formularz kontaktowy tej witryny to formularz HTMX, wysyłany przez hx-post z podmianą odpowiedzi na stronie, więc jego POST jest dokładnie takim żądaniem; dokumentacja nie wymienia HTMX z nazwy, a to powiązanie jest moją interpretacją.24 Żądanie POST poddane wyzwaniu wróciłoby jako strona wyzwania HTML, którą Cloudflare oznacza nagłówkiem cf-mitigated „set to challenge” (z wartością challenge), a „Regardless of the requested resource-type, the content-type of a challenge will be text/html.” (niezależnie od typu żądanego zasobu content-type wyzwania to text/html).64 Formularz wstawiłby stronę wyzwania do swojego pola odpowiedzi albo nic.
Odpowiedzią samego Cloudflare na ten przypadek jest inna funkcja: „To ensure your API calls are protected without breaking single-page applications (SPAs) or API integrations, Cloudflare recommends using Turnstile Pre-clearance.” (by chronić wywołania API bez psucia aplikacji jednostronicowych i integracji API, Cloudflare zaleca Turnstile Pre-clearance).63 Przy pre-clearance rozwiązany widżet wydaje też plik cookie cf_clearance, który „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.” (pozwala odwiedzającym omijać wyzwania WAF we wszystkich kolejnych żądaniach w strefie, w tym żądaniach API, zgodnie z poziomem dopuszczenia ustawionym przez klienta). Cookie nie zastępuje tokenu: Cloudflare mówi, że „is issued to the visitor in addition to the default Turnstile token” (jest wydawane odwiedzającemu oprócz domyślnego tokenu Turnstile), a ta sama strona nadal wymaga weryfikacji po stronie serwera: „You must call Siteverify to complete your Turnstile configuration.” (aby dokończyć konfigurację Turnstile, trzeba wywołać Siteverify).65 Pre-clearance dodaje wyzwanie na krawędzi, które żądanie POST przez XHR może przetrwać, dla odwiedzającego, którego cookie zawiera ważne dopuszczenie na poziomie wyzwania lub wyższym. Ja użyłem ustawienia domyślnego: widżet w formularzu i token sprawdzany przez mój własny serwer.
Co wdrożyłem 3 listopada
Na obu witrynach utworzyłem widżet w trybie zarządzanym przez API Cloudflare, ograniczony do własnej domeny witryny. Sekret każdego widżetu trafił z odpowiedzi API do pęku kluczy mojego Maca i do zmiennych usługi hostingowej w tym samym procesie, nigdy na wiersz poleceń ani do pliku.12
Ta witryna. Nowy weryfikator wysyła token do siteverify wraz z adresem odwiedzającego. Brakujący token, token dłuższy niż 2048 znaków, odrzucony token albo błąd transportu zwracają false, więc formularz zamyka się w razie awarii (fail closed), gdy tylko sekret jest ustawiony; bez sekretu, jak w lokalnym środowisku programistycznym i w zestawie testów, formularz działa jak wcześniej.12 Token zaakceptowany przez Cloudflare musi też zostać wydany na jednej z nazw hostów tej witryny, a gdy Cloudflare zwraca akcję widżetu, musi to być akcja tego formularza, jak zaleca przewodnik po stronie serwera; odpowiedź bez nazwy hosta oznacza odmowę. Ważny token wygenerowany na innej witrynie albo przez inny widżet nie przejdzie. Ta weryfikacja weszła jako druga zmiana tego samego ranka, budowana od 07:20 PST; o 07:25 PST, po wyczyszczeniu cache, zgłoszenie z ważnym tokenem CSRF z działającej strony i bez tokenu Turnstile znów wróciło z 403. To dowodzi, że formularz po wdrożeniu nadal zamyka się w razie awarii; zgłoszenie bez tokenu nie może sprawdzić samej weryfikacji nazwy hosta, która opiera się na kodzie i testach.6660 Do trzeciej zmiany oba widżety i oba weryfikatory dopuszczały też localhost, a ponieważ sitekey jest publiczny, token wygenerowany z moim kluczem na czyimkolwiek localhost przeszedłby zarówno siteverify, jak i weryfikację nazwy hosta. Strona testowania Cloudflare odradza to: „Cloudflare recommends that sitekeys used in production do not allow local domains” (Cloudflare zaleca, by sitekeye używane w produkcji nie dopuszczały domen lokalnych), i wymienia wśród nich localhost. O 08:12 PST usunąłem localhost z obu widżetów, a oba weryfikatory domyślnie dopuszczają teraz tylko witrynę i jej host www, więc weryfikacja przepuszcza wyłącznie nazwy hostów tej witryny; lokalne środowisko programistyczne podaje swoje nazwy hostów w ustawieniu. Po tej zmianie obie witryny ponownie przeszły te same testy na żywo, o 08:15 i 08:18 czasu pacyficznego: widżet na stronie i zgłoszenie bez tokenu odrzucone z 403. Żaden test na żywo nie może sprawdzić samej reguły nazwy hosta, bo brakujący lub śmieciowy token zawodzi, zanim nazwa hosta zostanie odczytana; ta reguła opiera się na ustawieniach widżetu, kodzie i testach.4667 Kolejność weryfikacji w handlerze to teraz CSRF, potem Turnstile, potem wysyłka, za tym samym limiterem per adres co wcześniej. Obie odmowy zwracają fragment błędu strony z kodem 403, a HTMX domyślnie nie wstawia odpowiedzi 4xx do strony, więc formularz potrzebuje handlera, który przepuści 403. Pierwsza wersja tego handlera nie działała. Podpiąłem go pod formularz, ale HTMX wywołuje zdarzenie na elemencie, który otrzymuje odpowiedź, a na tej stronie tym elementem jest rodzeństwo formularza, więc zdarzenie nigdy nie docierało do handlera, a odrzucona osoba nadal nic nie widziała. Wychwyciła to druga recenzja szkicu tego wpisu, czytając dołączony HTMX razem z szablonem. Poprawka, czwarta zmiana tego ranka, umieszcza listener na elemencie body strony, ograniczony do pola odpowiedzi, a test sprawdza, że jest renderowany. O 08:21 PST, po tym wdrożeniu, uruchomiłem formularz w Chromium z Playwright, zastępując token widżetu śmieciowym ciągiem: pole odpowiedzi pokazało „error: Verification failed. Reload the page and try again.” (błąd: weryfikacja nie powiodła się; proszę odświeżyć stronę i spróbować ponownie), i nic nie zostało wysłane, bo siteverify odrzuca śmieciowy token, zanim handler dojdzie do wysyłki.68 Stara odmowa CSRF, czyli 403 w JSON, którego HTMX nigdy nie pokazywał, zniknęła. Formularz resetuje swój widżet po każdym żądaniu HTMX, podając kontener widżetu tak, jak przyjmuje go wywołanie resetu Cloudflare, więc ponowienie niesie świeży token, bo każdy token jest jednorazowy i żyje 300 sekund.126069 Przewodnik Cloudflare po stronie klienta podaje powód i wywołanie: „To reset the widget if the given widget timed out or expired, you can use the function:” (aby zresetować widżet, gdy minął jego czas lub wygasł, można użyć funkcji:) turnstile.reset(widgetId);.69 Moja pierwsza wersja wywoływała reset bez argumentu, czego przewodnik nie dokumentuje; wychwyciła to czwarta runda recenzji, wywołanie podaje teraz kontener na obu witrynach, a test w przeglądarce 3 listopada wysłał śmieciowy token w każdym formularzu, zobaczył odmowę, a potem znalazł nowy token w ukrytym polu: w ciągu ośmiu sekund na stronie studia i w ciągu dwudziestu pięciu na tej, gdzie widżet najpierw ponownie uruchomił swoje wyzwanie.12 Div widżetu i skrypt renderują się tylko wtedy, gdy klucz witryny jest ustawiony. Jedenaście testów przechodzi: bez sekretu wysyła jak wcześniej; z sekretem handler odrzuca brakujący token przed jakąkolwiek wysyłką i odrzuca odrzucony token, zaakceptowany token wysyła, a CSRF nadal działa jako pierwszy; weryfikator akceptuje nazwę hosta tej witryny i odrzuca inną nazwę hosta, domyślnie localhost, inną akcję albo jej brak.126646
Strona studia. Honeypot pozostaje na początku. Wypełnione ukryte pole nadal dostaje komunikat o sukcesie i nic nie zostaje zapisane, więc najtańsze boty nigdy nie kosztują wywołania Cloudflare. Potem przychodzi Turnstile, a brakujący, odrzucony lub niemożliwy do zweryfikowania token dostaje 403 z fragmentem błędu. Potem walidacja, wstawienie do tabeli wiadomości i powiadomienie.12 Jej weryfikator sprawdza nazwę hosta (teraz tylko dwie własne nazwy hostów witryny) i akcję w ten sam sposób; ta zmiana została wdrożona o 07:19 PST, a o 07:22 PST działająca strona miała widżet i klucz witryny, zgłoszenie bez tokenu i zgłoszenie ze śmieciowym tokenem wróciły z 403, a zgłoszenie do honeypota nadal dostało 200.66 Przed wdrożeniem uruchomiłem handler lokalnie z udokumentowanymi kluczami testowymi Cloudflare. Z sekretem, który zawsze przepuszcza, zgłoszenie bez tokenu wróciło z 403, a token testowy przeszedł Turnstile i dotarł do walidacji; z sekretem, który zawsze odrzuca, oba wróciły z 403; honeypot odpowiedział 200.1267 Strona testowania Cloudflare wyjaśnia, dlaczego token testowy nie może przejść w produkcji: „Production secret keys: Only accept real tokens, reject dummy tokens.” (produkcyjne klucze tajne akceptują tylko prawdziwe tokeny i odrzucają testowe).67
Test na żywo. Wcześniej tego ranka, gdy wdrożenie każdej witryny zakończyło się powodzeniem, i po wyczyszczeniu cache, o 06:35 PST, każda strona główna miała jeden div widżetu, jeden tag skryptu Turnstile i klucz witryny, a działająca polityka każdej strefy wymieniała origin wyzwań w frame-src. Zgłoszenie do formularza tej witryny ze śmieciowym tokenem CSRF wróciło z 403, ale to weryfikacja CSRF je odrzuciła i nie dowodzi to niczego o Turnstile. Samą weryfikację Turnstile przetestowałem o 06:43 PST: zgłoszenie z ważnym tokenem CSRF pobranym z działającej strony i bez tokenu Turnstile wróciło z 403 i komunikatem „error: Verification failed. Reload the page and try again.”, a to samo zgłoszenie ze śmieciowym tokenem Turnstile również wróciło z 403. Sekret jest więc w usłudze, a weryfikacja działa po CSRF. Na stronie studia zgłoszenie bez tokenu i zgłoszenie ze śmieciowym tokenem wróciły z 403, a zgłoszenie do honeypota wróciło z 200, niczego nie zapisując ani nie wysyłając.70
Test w przeglądarce. W prawdziwym Chromium sterowanym przez Playwright widżet wyrenderował się na obu stronach, skrypt Turnstile się załadował, a ukryte pole cf-turnstile-response zawierało 730-znakowy token na obu stronach w ciągu sześciu sekund.70 To dowodzi mniej, niż się wydaje. Strona testowania Cloudflare ostrzega, że „Automated testing suites (like Selenium, Cypress, or Playwright) are detected as bots by Turnstile” (Turnstile wykrywa zautomatyzowane zestawy testów, takie jak Selenium, Cypress czy Playwright, jako boty), a o tym, czy token przechodzi, decyduje wyłącznie siteverify.67 Test dowodzi, że widżet działa przy obu politykach i wypełnia swoje pole. Nie wykonałem natomiast testu, który dla czytelnika tego wpisu liczy się najbardziej: człowieka wypełniającego formularz i otrzymującego odpowiedź. Ten test wysyła e-mail, więc należy do mnie, i jeszcze go nie przeprowadziłem.
7. Do czego nadal służy limit żądań
Turnstile zastąpił limity żądań w roli bramy. Nie zastąpił ich w roli ogrodzenia. Weryfikacja botów odpowiada na pytanie „kto”; coś nadal musi odpowiadać na pytanie „ile”, bo każde zgłoszenie, które dociera do weryfikacji z niepustym tokenem o poprawnym rozmiarze, kosztuje teraz wywołanie API siteverify Cloudflare, a każde zgłoszenie, które przejdzie, nadal kosztuje wysyłkę.12 Przewodnik Cloudflare po stronie serwera wymienia limity żądań wśród zaleceń dla samego wywołania siteverify: „Rate limit to protect against validation flooding.” (należy stosować limit żądań, by chronić się przed zalewem walidacji).60 Oba moje limity zostają: reguła na krawędzi z 3 zgłoszeniami na 10 sekund z adresu w jednym centrum danych i limiter aplikacji z pięcioma zgłoszeniami na minutę z adresu.917
Reguła na krawędzi zasługuje na uczciwy opis teraz, gdy wiem, jak liczy. Cloudflare przedstawia jej konstrukcję bez ogródek: „Cloudflare tracks request rates by maintaining separate counters for each unique combination of values in a rule’s characteristics” (Cloudflare śledzi tempo żądań, prowadząc osobne liczniki dla każdej unikalnej kombinacji wartości cech reguły), a także „Cloudflare does not support global rate limiting counters across the entire network.” (Cloudflare nie obsługuje globalnych liczników limitu żądań w całej sieci).6 Adres, który dociera do mojej strefy przez dwa centra danych, dostaje dwa przydziały, a zgłoszenia skanera z 2 listopada rzeczywiście przyszły przez dwa; to moje najmocniejsze wyjaśnienie, dlaczego reguła milczała, zgodne z sumami minutowymi, ale przez nie nieudowodnione.5 Do tego zastrzeżenie Cloudflare co do precyzji, że reguły „are not designed to allow a precise number of requests to reach your origin server” (nie są zaprojektowane tak, by do serwera źródłowego docierała precyzyjna liczba żądań), i regułę najlepiej opisać jako nominalny budżet, a nie dowiedziony limit: 3 zgłoszenia formularza na 10 sekund z adresu w jednym centrum danych, nominalny odpowiednik 18 na minutę, egzekwowany w przybliżeniu.489 W planie Free to także jedyna reguła limitu żądań, jaką mam, ze stałym 10-sekundowym okresem i liczeniem wyłącznie według IP.48
Jeden szczegół planu Free ma znaczenie dla każdego, kogo, tak jak mnie, kusiło, by to sam limit żądań stał się wyzwaniem. Strona parametrów Cloudflare mówi, że w planach Free, Pro i Business reguła limitu żądań z akcją wyzwania zawsze dławi ruch zamiast utrzymywać czas trwania, oraz że „When visitors pass a challenge, their corresponding request counter is set to zero.” (gdy odwiedzający przejdą wyzwanie, ich licznik żądań jest zerowany).71 Wyzwanie na żądaniu POST formularza wysyłanym przez XHR też by zawiodło z powodu opisanego w sekcji 6.63 Reguła na krawędzi pozostaje więc blokadą, a wyzwanie mieszka w widżecie.
Limiter aplikacji potrzebuje poprawki, której domaga się jego przeciek: magazynu liczników z atomową inkrementacją, tak by żadne dwa zgłoszenia nie mogły odczytać tego samego stanu, i ścieżki awaryjnej, która nie dzieli licznika między procesy. Obecny kod, czyli odczyt z KV, a po nim osobny zapis z zapasem per proces, nie zapewnia żadnej z tych rzeczy, więc to zmiana magazynu, a nie jednowierszowa łatka.173 Skoro Turnstile jest dodatkowym warunkiem dla każdego zgłoszenia, które mogłoby trafić do skrzynki, zgłoszenie przepuszczone przez przeciek nadal musi mieć token zaakceptowany przez Cloudflare, więc przeciek kosztuje wolumen i pieniądze, a decyzja o tym, kto może wysyłać, nie opiera się już na liczniku. Limiter to budżet. Budżet, który w jednej fali myli się o 47, jest złym budżetem, ale nigdy nie był strażnikiem.
Wytyczne OWASP dotyczące uwierzytelniania opisują to samo warstwowanie własnymi słowami. Traktują CAPTCHA „as a defense-in-depth control to make brute-force attacks more time-consuming and expensive, rather than as a preventative.” (jako kontrolę obrony w głąb, która sprawia, że ataki brute force są bardziej czasochłonne i kosztowne, a nie jako środek zapobiegawczy).72 A wytyczne dotyczące credential stuffingu zauważają, że „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” (większość narzędzi do takich ataków wysyła bezpośrednie żądania POST do serwera i czyta odpowiedzi, ale nie pobiera ani nie wykonuje JavaScriptu).50 Moje dwa skanery pobierały strony witryny, co jest najbardziej prawdopodobnym sposobem, w jaki zdobyły ważne tokeny CSRF. Klient, który wysyła formularz bez uruchomienia widżetu, nie ma tokenu Turnstile do wysłania, a oba formularze odrzucają teraz zgłoszenie bez niego: na tej witrynie zgłoszenie z ważnym tokenem CSRF i bez tokenu Turnstile wróciło z 403, tak samo jak zgłoszenie bez tokenu na stronie studia.70
Lista kontrolna
Dla formularza kontaktowego za Cloudflare, w kolejności, w jakiej zrobiłbym to dziś:
- Dodać weryfikację botów do formularza i sprawdzać każdy token na serwerze. Słowami samego Cloudflare: „The client-side widget alone does not protect your forms.” (sam widżet po stronie klienta nie chroni formularzy).60 Należy też sprawdzać nazwę hosta i akcję w odpowiedzi, jak zaleca ta sama strona, żeby token wygenerowany gdzie indziej nie przeszedł, i trzymać domeny lokalne z dala od produkcyjnego sitekeya, bo „Cloudflare recommends that sitekeys used in production do not allow local domains”.606667
- Zamykać się w razie awarii. Brakujący, zbyt długi, odrzucony lub niemożliwy do zweryfikowania token oznacza odmowę, bo „Tokens can be forged.” (tokeny można sfałszować).6012
- Resetować widżet po każdym zgłoszeniu. Tokeny są jednorazowe i ważne przez 300 sekund, więc ponowienie bez resetu to ponowienie ze zużytym tokenem.6069
- Dopuścić origin wyzwań w obu dyrektywach CSP. Cloudflare prosi o
script-srciframe-srcdlahttps://challenges.cloudflare.com, a polityka z samymdefault-src 'self'blokuje iframe.6212 - Nie stosować zarządzanego wyzwania na formularzu wysyłanym przez XHR lub fetch. Cloudflare mówi, że strona wyzwania „fails when the browser expects a non-HTML response” (zawodzi, gdy przeglądarka oczekuje odpowiedzi innej niż HTML), a zgłoszenie HTMX jest właśnie takim żądaniem.63
- Zwracać błąd, który człowiek zobaczy. Moja odmowa CSRF była odpowiedzią 403 w JSON, której formularz HTMX nigdy nie wyświetlał, więc osoba z przeterminowanym tokenem kliknęłaby Wyślij i nie zobaczyła nic, a moja pierwsza poprawka nasłuchiwała na formularzu, podczas gdy HTMX wywołuje zdarzenie podmiany na celu odpowiedzi, rodzeństwie formularza; odmowę trzeba więc przetestować w przeglądarce.1268
- Trzymać honeypot przed weryfikacją botów. Złapał 42 z 52 zgłoszeń na stronie mojego studia kosztem jednego ukrytego pola i jednego
if.725 - Zachować limit żądań i dobrać go jako limit kosztów, a nie bramę. Cloudflare liczy go per adres i per centrum danych, a skaner z 2 listopada przyszedł przez dwa, co jest najbardziej prawdopodobnym powodem, dla którego reguła nigdy nie zadziałała, choć sumy minutowe nie mogą tego dowieść.65
- Oprzeć limiter aplikacji na magazynie z atomową inkrementacją. Mój odczytywał licznik i zapisywał go z powrotem w osobnym żądaniu, resetował go do jedynki, gdy zawiódł drugi odczyt, przełączał się na pamięć jednego procesu, gdy zawiodło KV, i przepuścił 47 i 9 wysyłek ponad własny pułap; logi nie mówią, która ścieżka przeciekła.173
- Nie liczyć na to, że reguła oparta na user-agencie zatrzyma skaner. Oba skanery wysyłały zwykły ciąg Chrome, co najmniej 69 322 razy 30 października i 9725 razy 2 listopada.3241
- Liczyć e-maile u dostawcy poczty, nie w formularzu. Mój handler odpowiada 200 na swoich ścieżkach błędów, a zdarzenie
email.sentw Resend oznacza tylko, że „the API request was successful” (żądanie do API się powiodło); polelast_eventpobranego e-maila mówi, co stało się dalej.242873 - Odpytać analitykę w ciągu miesiąca. Adaptacyjne zbiory danych Cloudflare przechowują „at least 31 days of data on every plan” (co najmniej 31 dni danych w każdym planie).27
- Policzyć ponownie po zamknięciu okna. Moje notatki napisane kilka minut po fali z 30 października zaniżyły liczbę zgłoszeń formularza, bo Cloudflare nie skończył jeszcze przyjmować ostatnich minut.16
- Traktować blokadę adresu jako sprzątanie. Moja przyszła około 50 minut po pierwszym zgłoszeniu formularza 30 października i 8 minut po rozpoczęciu skanowania 2 listopada, a żadna wysyłka w żadnym z tych dni nie nastąpiła po minucie blokady: 101 przed nią 30 października oraz 26 przed nią i 5 w jej trakcie 2 listopada.1192
Co mogłoby to zmienić
To warunki, nie prognozy. Każdy z nich mówi, co musiałoby się wydarzyć i co bym wtedy zrobił.
Jeśli skaner zacznie przechodzić zarządzane wyzwania. Weryfikacje Turnstile działają w przeglądarce i szukają automatyzacji; narzędzie, które uruchamia pełną przeglądarkę na tyle dobrze, by je przejść, i to na dużą skalę, zamieniłoby widżet w kolejny koszt dla siebie, a nie w mur. Sygnałem byłyby wysyłki formularza z tokenami akceptowanymi przez siteverify, z adresów, które jednocześnie wysyłają ciągi wstrzyknięć w nagłówku User-Agent. Batchelder napisał uczciwą wersję tego w odniesieniu do honeypotów w 2007 roku: „If and when they do, we can switch back to CAPTCHAs.” (jeśli i kiedy to zrobią, możemy wrócić do CAPTCHA).49 Moje wyjście awaryjne byłoby w tym samym duchu, czyli silniejsze wyzwanie, a w międzyczasie honeypot i limity żądań nadal stoją.
Jeśli dodam weryfikację na krawędzi z pre-clearance. Cloudflare zaleca Turnstile pre-clearance dla żądań, które nie mogą wyrenderować strony wyzwania, a cookie cf_clearance pozwala odwiedzającemu „bypass WAF Challenges on all subsequent requests on the zone, including API requests” (omijać wyzwania WAF we wszystkich kolejnych żądaniach w strefie, w tym żądaniach API).6563 Jeśli to wdrożę, krawędź mogłaby poddać wyzwaniu samo żądanie POST formularza bez psucia HTMX dla odwiedzającego, którego cookie zawiera ważne dopuszczenie na poziomie wyzwania lub wyższym, a strona CSP Cloudflare dodaje dla tego trybu jeden wymóg: „your connect-src directive must include ‘self’.” (dyrektywa connect-src musi zawierać ‘self’).62 Cookie przychodzi „in addition to the default Turnstile token” (oprócz domyślnego tokenu Turnstile), a siteverify pozostaje wymagane, więc wyzwanie na krawędzi byłoby drugą weryfikacją przed weryfikacją mojego serwera, a nie jej zastępstwem.65 Nie zrobiłem tego, a wpis opisuje działający projekt z widżetem i siteverify.
Jeśli wyeksportuję log Resend dla obu okien. Każda liczba e-maili w tym wpisie jest górną granicą, bo odpowiedź 200 formularza to wysyłka albo błąd, na który mój handler również odpowiada 200.24 Endpoint pobierania w Resend zwraca dla każdego e-maila last_event, na przykład delivered, a jego lista zdarzeń odróżnia „sent - The email was sent successfully.” (wysłano: e-mail został wysłany pomyślnie) od „delivered - Resend successfully delivered the email to the recipient’s mail server.” (dostarczono: Resend dostarczył e-mail na serwer pocztowy odbiorcy).7374 Z takim eksportem 101 i 31 stałyby się dokładnymi liczbami i mogłyby tylko spaść.
Jeśli człowiek wyśle spam ręcznie. Turnstile pyta, czy jest przeglądarka, a czasem, czy jest człowiek. Nie pyta, po co. Osoba opłacana za wklejanie ofert do formularzy kontaktowych może go przejść, podobnie jak nadawcy generujący leady na stronie mojego studia, jeśli stoi za nimi człowiek. Na ten przypadek obrona jest stara: limit żądań na wolumen i mój własny osąd w skrzynce.
Jeśli strużka z Tora będzie wymagała odpowiedzi dzień po dniu. Lista węzłów wyjściowych, którą sprawdziłem, to ta pobrana 3 listopada, a usługa CollecTor Tor Project przechowuje dawne listy.5355 Jeśli odpytam ją dla tych ośmiu dni, przypisanie do Tora będzie się opierać na listach z dni, w których przyszły zgłoszenia, a nie na kodzie Cloudflare i liście z 3 listopada. Cloudflare oferuje też Onion Routing w każdym planie, który obsługuje użytkowników Tora bezpośrednio przez sieć Tor; Cloudflare twierdzi, że dzięki niemu „Human Tor users and bots can be distinguished by our Onion services, such that interactive challenges are only served to malicious bot traffic.” (nasze usługi Onion potrafią odróżnić ludzkich użytkowników Tora od botów, tak że interaktywne wyzwania trafiają tylko do złośliwego ruchu botów).75 Nie włączyłem tego i chciałbym najpierw zmierzyć ruch z Tora na stronie studia po wdrożeniu Turnstile.
Najważniejsze wnioski
Dla prowadzących formularz kontaktowy:
- Token CSRF i limit żądań razem nadal pozwalają skanerowi do Państwa napisać. Oba moje skanery miały ważne tokeny, a mój limiter przepuścił 101 i 31 wysyłek.12
- Token weryfikacji botów należy sprawdzać na serwerze, zamykać się w razie awarii i resetować widżet po każdym żądaniu; sam widżet „does not protect your forms.” (nie chroni formularzy).60
- Warto przetestować limiter pod obciążeniem i w warunkach awarii, a nie jednym żądaniem naraz. Licznik typu „odczyt, potem zapis” z zapasem per proces przepuścił 18 wysyłek w minucie ustawionej na pięć.117
- Warto zachować honeypot. To najtańszy filtr w całym stosie, a na stronie mojego studia w ciągu ośmiu dni złapał 80,8% tych zgłoszeń, zanim handler sprawdził adres, zapisał wiersz czy wysłał e-mail.725
Dla zarządzających krawędzią witryny:
- Trzeba wiedzieć, co liczy limit żądań. W Cloudflare każda reguła liczy per centrum danych, a skaner, który przychodzi przez dwa, dostaje dwa liczniki.65
- Warto testować własne reguły, a potem czytać zdarzenia firewalla. Jedenaście zdarzeń firewalla z mojego testu powiedziało mi o regule więcej niż dzień rozważań, dlaczego milczała.18
- Nie należy poddawać żądań POST wysyłanych przez XHR lub fetch wyzwaniu w postaci strony wyzwania; Cloudflare dokumentuje, że to zawodzi, i zaleca dla tego przypadku pre-clearance.63
- Należy zakładać, że reguły oparte na user-agencie złapią tylko uczciwe narzędzia. Skanery wysyłały tu ciąg przeglądarki; mój test z curl wywołał wyzwania.3239
Dla czytających skrzynkę:
- Gdy skrzynka się zapełnia, warto sprawdzić nadawcę przed tematem. Trzy moje strumienie poczty dzielą jeden adres nadawcy, dlatego moje pierwsze przypuszczenie było błędne.23
- Odpowiedzi 200 formularza należy traktować jako górną granicę liczby e-maili, a nie jako ich liczbę, dopóki log dostawcy poczty nie powie inaczej.2428
- Przed wyłączeniem powiadomień warto przeczytać zapisane wiadomości. Wiadomość prawdziwego klienta przeleżała kiedyś nieprzeczytana tydzień i to ona jest powodem, dla którego formularz studia w ogóle wysyła mi e-maile.25
Często zadawane pytania
Dlaczego mój formularz kontaktowy dostaje spam mimo Cloudflare?
Najpewniej dlatego, że nic przed formularzem nie pyta każdego zgłoszenia, czy wysłał je człowiek. Reguły niestandardowe Cloudflare dopasowują adresy, sieci, ścieżki i ciągi user-agent, a jego limity żądań liczą żądania per adres i per centrum danych; skaner, który wysyła user-agenta przeglądarki, najpierw ładuje stronę i wysyła w umiarkowanym tempie, przechodzi przez nie wszystkie. Na mojej witrynie dwa skanery zrobiły dokładnie to i przepchnęły 101 i 31 zgłoszeń, a spam na stronie mojego studia przychodził w ciągu ośmiu dni pomiaru głównie po jednym zgłoszeniu z adresu, czego żadne liczenie per adres nie wychwyci.96127
Czy limit żądań zatrzymuje boty?
Limit żądań ogranicza tempo jednego adresu; nie odróżnia bota od człowieka. Sama dokumentacja Cloudflare mówi, że jego reguły „are not designed to allow a precise number of requests to reach your origin server” (nie są zaprojektowane tak, by do serwera źródłowego docierała precyzyjna liczba żądań) i że liczniki nie są współdzielone między centrami danych. Moja reguła na krawędzi nie zadziałała przy żadnym ze skanerów, a mój limiter aplikacji, ustawiony na pięć na minutę, przepuścił 18 w jednej minucie: jego licznik to odczyt, a po nim osobny zapis, nieudany drugi odczyt resetuje go do jedynki, a gdy magazyn zawodzi, przejmuje zapas per proces; każda z tych ścieżek może przepuścić dodatkowe zgłoszenia.48641173
Czy pole honeypot wystarczy?
Wystarczy na boty, które wypełniają ukryte pole, a na stronie mojego studia było to 80,8% tych zgłoszeń: mój honeypot złapał 42 z 52 zgłoszeń w ciągu ośmiu dni, w tym, sądząc po godzinie nadejścia, każde zgłoszenie z węzła wyjściowego Tor. Nic nie robi przeciwko nadawcy, który zostawia ukryte pole puste, a 10, które przepuścił, zapisanych i więc najwyżej 10 e-maili, zrobiło dokładnie to.720 Ned Batchelder, opisując tę technikę w 2007 roku, sam to przyznał: „In theory, it is possible for a spambot to defeat any of these measures.” (w teorii spambot może pokonać każdy z tych środków).49
Turnstile a CAPTCHA: na czym polega różnica?
Klasyczna CAPTCHA prosi odwiedzającego o rozwiązanie łamigłówki. Turnstile uruchamia „a series of small non-interactive JavaScript challenges to gather signals about the visitor or browser environment” (serię małych, nieinteraktywnych wyzwań JavaScript, by zebrać sygnały o odwiedzającym lub środowisku przeglądarki), a w trybie zarządzanym pokazuje pole wyboru tylko wtedy, gdy wymaga tego ryzyko odwiedzającego, z „No images or text to decipher.” (bez obrazków i tekstu do odszyfrowania).2158 Cloudflare twierdzi, że jego wyzwania nie używają „CAPTCHA puzzles or visual tests like selecting objects or typing distorted characters.” (łamigłówek CAPTCHA ani testów wizualnych, takich jak wybieranie obiektów czy przepisywanie zniekształconych znaków).76 Jedno i drugie nadal wymaga, by serwer zweryfikował wynik; w przypadku Turnstile jest to wywołanie siteverify.60
Dlaczego ochrona CSRF nie zatrzymuje spamu?
Poprawnie zaimplementowana obrona przed CSRF ma powstrzymać inną witrynę przed sprawieniem, że przeglądarka odwiedzającego wyśle Państwa formularz. Mój token jest słabszy: jest bezstanowy i podpisany, ważny godzinę i niepowiązany z sesją, plikiem cookie ani adresem, więc dowodzi tylko, że klient ma token wydany przez moją witrynę w ciągu ostatniej godziny, a nie skąd przyszło żądanie. Żadna z tych odmian nie zatrzyma klienta, który ładuje stronę i sam wysyła formularz, bo załadowanie strony to właśnie sposób, w jaki klient dostaje ważny token, a oba skanery zebrały ważne tokeny; weryfikacja odrzuciła cztery ich zgłoszenia w obu falach.1512
Jak odróżnić w logach skaner od człowieka?
Trzeba spojrzeć na wszystko, co zrobił adres, a nie tylko na formularz. Moje dwa skanery przeszły tysiące ścieżek, umieszczały ciągi SQL injection w ścieżkach i w nagłówku User-Agent, w większości żądań wysyłały user-agenta Chrome, a w niektórych podawały się za Googlebota, czego reguła zweryfikowanych botów Cloudflare nie zaakceptowała, i wysyłały formularz seriami od kilkudziesięciu do kilkuset na minutę.31324441 Ładunki czasowe, takie jak sleep(15) i waitfor delay '0:0:15', należą do udokumentowanych rodzin sond.3377 Człowiek wysyła jedną wiadomość, ze strony, którą rzeczywiście przeczytał, i nie prosi serwera, żeby zasnął.
Czy Turnstile zbiera dane osobowe?
Cloudflare nie twierdzi, że nie zbiera żadnych. Jego aneks o prywatności Turnstile wymienia przetwarzane sygnały, „such as client IP address, TLS Fingerprint, User-Agent Header and Sitekey and associated origin” (takie jak adres IP klienta, odcisk TLS, nagłówek User-Agent oraz sitekey i powiązany origin), mówi, że celem jest „solely to detect and block bots” (wyłącznie wykrywanie i blokowanie botów), i stwierdza, że Cloudflare jest administratorem tych sygnałów w celu ulepszania wykrywania botów. Jego przegląd mówi, że Turnstile „does not access, store, or transmit user communications, form entries, or other page inputs.” (nie uzyskuje dostępu do komunikacji użytkowników, treści formularzy ani innych danych wpisywanych na stronie, nie przechowuje ich ani nie przesyła).2221 Widzi więc dane o połączeniu, a nie to, co człowiek wpisuje.
Powiązane wpisy na tej witrynie. Tryb auto w Claude Code nie jest granicą bezpieczeństwa stawia ten sam argument wobec innej kontroli: klasyfikator, który ocenia każdą akcję, nie jest murem. Agents.txt nie jest mechanizmem kontroli dostępu wyznacza granicę między deklarowaniem botom polityki a jej egzekwowaniem. Zmęczenie alertami: 82 e-maile w jeden dzień od mojego monitora opisuje skaner z 2 listopada od strony mojego monitora: blokada osiem sekund po pierwszym wykryciu, osiem minut po rozpoczęciu skanowania, i zero e-maili na jego temat. Co uruchamiam przed snem to inny rodzaj rutyny operacyjnej, nocna kontrola 15 000 stron pod kątem statusu, czasu odpowiedzi i cache, a Co faktycznie się psuje, gdy agenty AI działają bez nadzoru kataloguje tryby awarii sesji agentów działających bez nadzoru, czyli tego rodzaju automatyzacji, która obsługuje moją infrastrukturę.
Źródła
Uwagi autora o danych: każda liczba z moich własnych systemów pochodzi z GraphQL Analytics API Cloudflare dla obu stref (zbiory danych o żądaniach i o zdarzeniach firewalla), z tabeli wiadomości strony studia, z logu mojego monitora i z kodu obu formularzy, wszystko zapisane 3 listopada 2026 i zamienione skryptem w rejestr. Czasy podano w UTC, chyba że oznaczono je jako PDT lub PST. Żaden adres, identyfikator reguły ani listy, klucz ani ścieżka w systemie plików z tego materiału dowodowego nie pojawia się w tym wpisie.
-
Dane autora. Analityka żądań Cloudflare dla strefy tej witryny, żądania POST skanera z 30 października do formularza kontaktowego według minuty i statusu na krawędzi, odpytane 3 listopada 2026 dla zamkniętego okna: 1065 zgłoszeń, z czego 101 z odpowiedzią 200, 830 z 429, 130 z 422 i 4 z 403, wszystkie docierające do aplikacji; pierwsze zgłoszenie i pierwsze 200 w minucie 18:00, szczyt 208 zgłoszeń w minucie 18:03, najwięcej odpowiedzi 200 w jednej minucie 18 (18:06), osiem minut z ponad pięcioma odpowiedziami 200, ostatnie 200 o 18:36 i ostatnie zgłoszenie docierające do aplikacji o 18:37. Wyprowadzone w rejestrze: 101 wysyłek przed blokadą, pierwsze zgłoszenie około 46 minut przed moim zgłoszeniem fali i 49,88 minuty przed blokadą, ostatnie zgłoszenie docierające do aplikacji 11,88 minuty przed blokadą. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
Dane autora. Analityka żądań Cloudflare dla strefy tej witryny, żądania POST skanera z 2 listopada do formularza kontaktowego według minuty i statusu na krawędzi, odpytane 3 listopada 2026: 500 zgłoszeń, z czego 31 z odpowiedzią 200, 31 z 429 i 438 z 403 w minucie blokady lub później; 62 dotarły do aplikacji; pierwsze zgłoszenie w minucie 18:58, szczyt 32 zgłoszeń docierających do aplikacji w minucie 19:02, najwięcej odpowiedzi 200 w jednej minucie 10 (19:03), ostatnie 200 w minucie 19:04; średnio 8,86 zgłoszenia na minutę w siedmiominutowym okresie od pierwszego zgłoszenia docierającego do aplikacji, w minucie 18:58, do ostatniego, w minucie 19:04; z 31 wysyłek 26 w minutach przed minutą blokady i 5 w samej minucie blokady, 19:04, minucie, w której adres trafił na listę o 19:04:01, czego przedziały minutowe nie potrafią uporządkować względem tej sekundy; żadnej później. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
Analiza autora w rejestrze: dla każdej minuty mniejsza z liczb: pięć albo liczba zgłoszeń widzianych przez limiter aplikacji (odpowiedzi 200, 429 i 403 CSRF aplikacji; bez 422, które FastAPI odrzuca, zanim limiter zadziała), zsumowana po minutach. 30 października: najwyżej 54 wysyłki dla dokładnego limitera wobec 101 zmierzonych, 47 ponad to, z 935 zgłoszeń widzianych przez limiter. 2 listopada: najwyżej 22 wobec 31, 9 ponad to, z 62. Kontrfaktyczny pułap, nie pomiar; zmierzone wysyłki to powyższe liczby dla formularza. ↩↩↩↩↩↩↩
-
Dane autora. Zdarzenia firewalla Cloudflare dla adresu z 2 listopada, odpytane 3 listopada 2026 (zbiór próbkowany, używany do ustalenia, która reguła zadziałała i kiedy, nigdy do wolumenów): pierwsze zdarzenie o 18:57:11 z reguły ścieżek exploitów, pierwsze zdarzenie reguły listy blokad o 19:04:03; każdy wiersz to blokada, żaden nie jest zarządzanym wyzwaniem; 310 wierszy na formularzu kontaktowym, wszystkie blokady, pierwszy o 19:04:05; brak zdarzeń z reguły limitu żądań dla tego adresu. ↩↩↩↩↩↩↩↩↩↩
-
Dane autora, aneks rejestru. Analityka żądań Cloudflare dla żądań POST obu skanerów do formularza, pogrupowanych według centrum danych Cloudflare, minuty i statusu, odpytana 3 listopada 2026: 2 listopada od 18:56 do 19:05:00, czyli końca zapytania, więc liczba obejmuje tylko pierwsze zgłoszenie z minuty 19:05, 44 zgłoszenia przez Amsterdam i 41 przez Londyn; w minucie 19:02 18 przez Londyn (4 wysłane, 14 odrzuconych przez aplikację) i 14 przez Amsterdam (5 wysłanych, 9 odrzuconych); największa liczba w jednej minucie w jednym centrum danych, 19, przypadła na Amsterdam w minucie 19:04, z czego 15 odrzucono na krawędzi po blokadzie. 30 października wszystkie 1065 zgłoszeń przyszło przez Paryż. ↩↩↩↩↩↩↩↩
-
Cloudflare, “How Cloudflare determines the request rate”, dokumentacja Cloudflare WAF, pobrano 3 listopada 2026 o 14:08:57 UTC. Osobne liczniki dla każdej kombinacji cech; liczniki niewspółdzielone między centrami danych z wyjątkiem tych w tej samej lokalizacji geograficznej;
cf.colo.idjako obowiązkowa cecha; brak liczników globalnych. ↩↩↩↩↩↩↩ -
Dane autora. Analityka żądań Cloudflare dla strefy strony studia, żądania POST do formularza kontaktowego według godziny, adresu, kraju i user-agenta, od 17:00 UTC 26 października do 13:59:50 UTC 3 listopada 2026: 52 zgłoszenia, wszystkie z odpowiedzią 200, z 35 adresów; 23 zgłoszenia i 23 adresy z kodem kraju Tor w Cloudflare (44,2% i 65,7%); 32 adresy z dokładnie jednym zgłoszeniem (91,4%), w tym każdy adres Tor; trzy powtarzające się adresy z 13 (Wielka Brytania, jedna godzina, user-agent SQLSpider), 2 (Polska, jedna godzina) i 5 (Stany Zjednoczone, za każdym razem nowy user-agent); user-agenty Tor: 18 Android i 5 desktopowy Chrome, 21 wskazujących Chrome 81 lub starszy; user-agenty spoza Tora: 10 desktopowy Chrome od 125 do 135, 6 Firefox od 128 do 138 i 13 innych. Firewall strefy nie zalogował w tym oknie żadnego zdarzenia dla żądania POST do formularza. Wpadnięcia w honeypot to zgłoszenia minus zapisane wiersze: 42, czyli 80,8%. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
Dane autora. Tabela wiadomości kontaktowych strony studia, odczytana 3 listopada 2026 o 14:04 UTC: 108 wierszy od 6 czerwca do 07:24 UTC 3 listopada, 10 z nich w oknie po zapytaniu; dziennie jak w tabeli w sekcji 5; 32 wiersze w październiku w 21 z 31 dni, 1,03 dziennie, nigdy więcej niż trzy w jednym dniu; dziewięć wierszy o 50 000 000 dolarów pod jedną nazwą nadawcy z dziewięcioma adresami nadawcy; 43 wiersze od commita z powiadomieniami i 65 przed nim. ↩↩↩↩↩↩↩↩↩↩↩
-
Dane autora. Konfiguracja stref obu witryn, wyeksportowana przez API Cloudflare 3 listopada 2026: reguła limitu żądań tej witryny (wersja 1, ostatnia aktualizacja 2026-10-30 o 18:49:53 UTC; wyrażenie jak w cytacie; cechy: centrum danych i adres klienta; 3 żądania na 10-sekundowy okres; 10-sekundowy czas mitygacji, nominalny odpowiednik 18 na minutę z adresu w jednym centrum danych) i jej pięć reguł niestandardowych (blokada listy konta i siedmiu ASN, blokada ścieżek exploitów, pominięcie zweryfikowanych botów oraz dwa zarządzane wyzwania na formularzu kontaktowym i ścieżkach administracyjnych, według ciągu user-agent i według wyniku zagrożenia, obie włączone w chwili eksportu, a ta oparta na wyniku zagrożenia wyłączona później tego dnia); reguły tego samego rodzaju na stronie studia, z wyzwaniem dla całej witryny i blokadą ścieżek API, z której wyłączony jest formularz, oraz reguła limitu żądań dla całej witryny liczona per centrum danych i adres dla żądań od klientów niebędących zweryfikowanymi botami. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
Dane autora. Zdarzenia firewalla Cloudflare dla adresu z 30 października, odpytane 3 listopada 2026 (3932 próbkowane wiersze, poniżej limitu zapytania, używane tylko do przypisania i ustalenia czasu): pierwsze zdarzenie o 17:59:53 z reguły ścieżek exploitów, która działała od pierwszej minuty skanowania; każde ze 111 próbkowanych zdarzeń przed 18:49:53 z tej reguły; pierwsze zdarzenie reguły listy blokad o 18:49:53; brak zdarzeń z reguły limitu żądań i brak zdarzeń na formularzu kontaktowym dla tego adresu. ↩↩↩↩↩↩↩↩↩↩↩
-
Cloudflare, “cf.threat_score”, dokumentacja pól Ruleset Engine, pobrano 3 listopada 2026 o 16:12:44 UTC. Opis pola: wynik, który kiedyś mieścił się w zakresie od 0 do 100, oraz „Now, the threat score is always 0 (zero).” ↩↩↩
-
Zapis autora tego, co wdrożono 3 listopada 2026, sporządzony na podstawie wyjścia narzędzi z sesji: dwa widżety (tryb zarządzany, utworzone przez API Cloudflare około 14:13 UTC), zmiany CSP w obu strefach, weryfikator tej witryny i kolejność w handlerze (zamyka się w razie awarii, CSRF, potem Turnstile, potem wysyłka, fragmenty błędów z 403, reset widżetu po każdym żądaniu HTMX), jej dziesięć nowych testów (10 przeszło; jedenasty doszedł wraz z weryfikacją nazwy hosta), kolejność w handlerze strony studia (honeypot, potem Turnstile, potem walidacja, zapis i powiadomienie) oraz lokalne testy z kluczami testowymi Cloudflare; na obu witrynach weryfikator odrzuca brakujący token albo dłuższy niż 2048 znaków bez wywoływania siteverify (odczytane z kodu). Obie witryny wdrożono między 14:32 a 14:36 UTC. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
Dane autora. Analityka żądań Cloudflare dla adresu z 30 października, według minuty i w przeglądzie wszystkich stref na moim koncie, odpytana 3 listopada 2026: 76 429 żądań do tej witryny od minuty 17:59 do minuty 18:51 (53 przedziały), ta sama suma w obu zapytaniach; 69 484 przed minutą blokady przy średniej 1389,7 na minutę, szczyt 3219 o 18:36, 5234 po minucie blokady, żadne nieobsłużone; według wyniku 49 513 obsłużonych (64,8%), 18 990 odrzuconych przez aplikację (24,8%, w tym 10 019 GET 418 z bramki agentów witryny), 2339 błędów 520 origin (3,1%), 5587 zablokowanych na krawędzi (7,3%); 50,88 minuty od pierwszej minuty skanowania do blokady; 387 żądań GET do kiradex.app, wszystkie z odpowiedzią 200, i żadna inna strefa. ↩↩↩↩↩↩↩↩↩↩
-
Dane autora. Analityka żądań Cloudflare dla adresu z 2 listopada, według minuty, ścieżki, user-agenta i w przeglądzie wszystkich stref, odpytana 3 listopada 2026: dwa żądania GET o 07:49 UTC, 11,1 godziny przed skanowaniem; 10 907 żądań od minuty 18:56 do minuty 19:17 (22 przedziały), 10 909 z wczesnymi GET, ta sama suma w czterech zapytaniach; 29 żądań w pierwszej minucie, 637 w najbardziej ruchliwej minucie przed blokadą (18:57), 1107 w najbardziej ruchliwej minucie (19:07), 300 w ostatniej; 2986 przed minutą blokady (średnio 373,2 na minutę), 7445 po niej, żadne nieobsłużone; według wyniku 1388 obsłużonych (12,7%), 1473 odrzucone przez aplikację (13,5%, w tym 420 GET 418), 116 błędów 520 origin (1,1%), 7930 zablokowanych na krawędzi (72,7%); 8,02 minuty od pierwszej minuty skanowania do blokady; 368 żądań GET do kiradex.app i żadna inna strefa. ↩↩↩↩↩↩↩↩↩↩↩
-
Odczyt kodu przez autora. Moduł CSRF tej witryny w wersji wdrożonej 30 października i 2 listopada: token złożony ze znacznika czasu, wartości losowej i podpisu HMAC-SHA256 obu; ważny, jeśli podpis się zgadza, a znacznik czasu ma mniej niż 3600 sekund; niepowiązany z sesją, plikiem cookie ani adresem. Odczytane ze źródeł 3 listopada 2026. ↩↩↩↩
-
Dane autora. Rejestr zbudowany skryptem z zapisanych wyników zapytań (484 wiersze, każdy ze źródłem i metodą) oraz jego słownik kodów, 3 listopada 2026, w tym reguły określające, która warstwa odpowiedziała danym statusem, reguła, że zdarzenia firewalla dają przypisanie i znaczniki czasu, a zbiory danych o żądaniach dają liczby, reguła, że tylko 200 dowodzi ważnego tokenu CSRF, oraz tabela miejsc, w których moje notatki z tamtego dnia różnią się od zapytań z 3 listopada (notatki z 30 października napisano o 11:58 PDT, zanim Cloudflare przyjął ostatnie minuty, i zaniżają one zgłoszenia formularza, odpowiedzi 422, zgłoszenia do udostępniania w aplikacji do malowania i żądania z tej godziny). ↩↩↩↩↩↩↩↩
-
Odczyt kodu przez autora. Limiter formularza tej witryny w wersji wdrożonej 30 października i 2 listopada: domyślnie pięć zgłoszeń formularza z adresu klienta na okno, 60-sekundowe okno oparte na minucie zegarowej, odczyt licznika z KV, drugi odczyt wewnątrz inkrementacji, który traktuje nieudany odczyt jako zero, i osobny zapis do KV tej wartości plus jeden, odpowiedź 429, gdy odczytana wartość jest na limicie, oraz przejście na listę w pamięci per proces, gdy wywołanie KV zawiedzie. Odczytane ze źródeł 3 listopada 2026. ↩↩↩↩↩↩↩↩↩↩
-
Dane autora, aneks rejestru. Mój test reguły limitu żądań tej witryny 3 listopada 2026 z mojego komputera: o 14:36:49 UTC dziesięć żądań POST bez tokenu w odstępach 0,4 sekundy (cztery 422, potem sześć 429); o 14:38:03 UTC osiem ze wszystkimi polami (trzy 403 z origin, potem pięć 429 z treścią „error code: 1015”); zdarzenia firewalla Cloudflare dla formularza od 14:35 do 14:40 UTC, pobrane o 14:40:36 UTC: 11 zdarzeń, każde to blokada z reguły limitu żądań. ↩↩↩↩
-
Dane autora. Log mojego monitora z 2 listopada 2026, od 10:40 do 11:32 PST, oraz jego zapisy wykryć i blokad, zapisane 3 listopada: przebieg rozpoczęty o 10:57:00 PST z oknem detektora kończącym się o 18:57:22 UTC i bez wykrycia, z liczbą żądań adresu w tym oknie między 29 a 666 według danych minutowych; następny przebieg o 11:02:46 PST; pierwsze wykrycie o 11:03:53 PST (krytyczna fala żądań, 1914 żądań policzonych dla adresu), 7,88 minuty po pierwszej minucie skanowania; adres dodany do listy blokad konta o 11:04:01 PST; łącznie sześć wykryć, każde włączone do dziennego digestu i żadne niewysłane e-mailem. Monitor nie działał 30 października; uruchomiłem go ponownie 31 października, a jego detektor ruchu powstał później. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
Analiza autora. Dopasowanie w przedziałach godzinowych zapisanych wierszy strony studia do godzinowych danych o żądaniach POST, 3 listopada 2026: żaden zapisany wiersz w godzinie z wyłącznie zgłoszeniami z Tora; wszystkie 10 w godzinach ze zgłoszeniem spoza Tora; 22 z 23 zgłoszeń z Tora w godzinach bez zapisanego wiersza, 23. w godzinie dzielonej z amerykańskim adresem generującym leady; pięć wierszy z ofertami generowania leadów w oknie w pięciu godzinach, w których ten adres wysyłał. Wniosek oparty na godzinie, a nie złączenie po identyfikatorze żądania. ↩↩↩↩↩↩↩
-
Cloudflare, “Overview”, przegląd dokumentacji Turnstile, pobrano 3 listopada 2026 o 14:08:57 UTC. „Cloudflare’s smart CAPTCHA alternative”; osadzanie bez ruchu przez Cloudflare; nieinteraktywne wyzwania JavaScript i opis proof-of-work i proof-of-space; stwierdzenie, że Turnstile nie uzyskuje dostępu do treści formularzy, nie przechowuje ich ani nie przesyła. ↩↩↩↩↩↩
-
Cloudflare, “Turnstile Privacy Addendum”, ostatnia aktualizacja 18 czerwca 2025, pobrano 3 listopada 2026 o 14:08:57 UTC. Sygnały przetwarzane przez Turnstile, ich deklarowany cel i rola Cloudflare jako administratora danych w celu ulepszania wykrywania botów, wszystko cytowane. ↩↩↩
-
Captain’s Log autora, 2 listopada 2026: moje pytanie do sesji agenta, która obsługuje moją infrastrukturę, czy to ona wysyła e-maile, trzy strumienie poczty dzielące jeden adres nadawcy, tematy e-maili „Contact Form:” z ładunkami skanera oraz moja notatka, że zgłoszenia formularza od skanera „ran about nine a minute” (szły w tempie około dziewięciu na minutę). ↩↩↩↩↩
-
Odczyt kodu przez autora. Handler kontaktowy i szablon formularza tej witryny w wersji wdrożonej 30 października i 2 listopada: formularz HTMX (
hx-postna ścieżkę formularza) z wstawionym tokenem CSRF; dekorator limitera przed handlerem; weryfikacja CSRF, zwracająca 403, przed wysyłką; wysyłka przez Resend z tematem „Contact Form:” i imieniem nadawcy; fragment sukcesu przy zaakceptowanej wysyłce; oraz dwie ścieżki błędów, brak konfiguracji poczty i nieudana lub pozbawiona id wysyłka, które zwracają fragment błędu z domyślnym statusem FastAPI, 200. Bez pola honeypot. Odczytane ze źródeł 3 listopada 2026. ↩↩↩↩↩↩↩↩↩ -
Odczyt kodu i historii repozytorium przez autora dla strony studia w wersji wdrożonej przed 3 listopada 2026: gałąź honeypota w handlerze kontaktowym (niepuste ukryte pole
websitedostaje fragment sukcesu i niczego nie zapisuje ani nie wysyła; to pierwsza weryfikacja handlera, przed sprawdzeniem adresu, wstawieniem i powiadomieniem), obecna od commita, który zbudował formularz 5 czerwca 2026;tabindex="-1"iaria-hiddenukrytego pola; brak tokenu CSRF; powiadomienia w trybie best effort, dodane 21 września 2026 o 17:46:47 UTC w commicie, którego komunikat brzmi „a paying customer’s message sat unread in D1 for a week”; oraz limiter w pamięci per proces dla anonimowych klientów, z kluczem według adresu klienta i klasyfikacji klienta (poziomu bota, który limiter przypisuje na podstawie user-agenta i adresu), na wszystkich ścieżkach z wyjątkiem health checków i plików statycznych. Odczytane z repozytorium 3 listopada 2026. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Captain’s Log autora, 30 października 2026, napisany o 11:58 PDT: moje zgłoszenie fali o 11:46 PDT, ręczna blokada i reguła limitu żądań od 11:48 do 11:49 PDT oraz mój test o 11:52 PDT, cytowany. Jego liczby zebrano, gdy Cloudflare wciąż przyjmował ostatnie minuty fali, a ten wpis używa zamiast nich zapytań z 3 listopada. ↩↩↩
-
Cloudflare, “GraphQL API - Limits”, dokumentacja GraphQL Analytics API, pobrano 3 listopada 2026 o 14:08:57 UTC. Adaptacyjne zbiory danych „retain at least 31 days of data on every plan” (przechowują co najmniej 31 dni danych w każdym planie). ↩↩
-
Resend, “Event Types”, dokumentacja webhooków Resend, pobrano 3 listopada 2026 o 14:14:47 UTC.
email.sent„Occurs whenever the API request was successful. Resend will attempt to deliver the message to the recipient’s mail server.”;email.delivereddla udanego doręczenia. ↩↩↩ -
Dane autora. Analityka żądań Cloudflare dla wszystkich pozostałych adresów, które wysłały formularz kontaktowy tej witryny w ośmiodniowym zapytaniu, z wyłączeniem obu skanerów i mojego adresu testowego, odpytana 3 listopada 2026: trzy adresy (Korea Południowa, Japonia, Indie), każdy z dokładnie dwiema odpowiedziami 200 w jednym dniu, 27, 28 i 29 października. ↩
-
RIPE NCC, RIPEstat network-info, whois i as-overview dla adresu z 30 października i jego AS, pobrano 3 listopada 2026 o 14:10:34 i 14:11:34 UTC: AS34534, posiadacz „BULLIONET FBW NETWORKS SAS”, przydział zarejestrowany z krajem FR. ↩
-
Dane autora. Analityka żądań Cloudflare dla adresu z 30 października pogrupowana według ścieżki, metody i statusu, odpytana 3 listopada 2026; zapytanie zwróciło swój limit 3000 wierszy, obejmujący 74 125 z 76 429 żądań, więc liczby ścieżek są dolnymi granicami: co najmniej 2881 różnych ścieżek i co najmniej 2680 żądań z ładunkiem SQL injection w ścieżce w co najmniej 810 różnych ścieżkach; 320 żądań POST na ścieżkę udostępniania w aplikacji do malowania, wszystkie odrzucone (422 lub 400), i żadne z innego adresu w ciągu ośmiu dni; warianty ścieżki udostępniania z ładunkiem cytowane w sekcji 2, z odpowiedzią 404. ↩↩↩↩
-
Dane autora. Analityka żądań Cloudflare dla adresu z 30 października pogrupowana według user-agenta, odpytana 3 listopada 2026; zapytanie zwróciło swój limit 500 wierszy, obejmujący 76 095 z 76 429 żądań, więc każda liczba tutaj jest dolną granicą: co najmniej 69 322 żądania z jednym zwykłym ciągiem Chrome 141 dla Windows, co najmniej 2755 podających się za Googlebota i co najmniej 3154 z ładunkiem SQL injection w co najmniej 396 różnych ciągach. ↩↩↩↩↩↩
-
PortSwigger Web Security Academy, “SQL injection cheat sheet”, pobrano 3 listopada 2026 o 14:10:34 UTC. Opóźnienia czasowe dla Oracle (
dbms_pipe.receive_message), Microsoftu (WAITFOR DELAY), PostgreSQL (pg_sleep) i MySQL (SLEEP); warunkowa formaIF(condition, SLEEP(10), 'a');SELECT @@versiondla Microsoftu i MySQL;FROM dualw Oracle. ↩↩↩↩↩ -
Projekt sqlmap, “time_blind.xml”, definicje ładunków ślepego wstrzyknięcia czasowego tego narzędzia, pobrano 3 listopada 2026 o 14:12:27 UTC. Zawiera ładunki
SLEEP,PG_SLEEP,WAITFOR DELAYiDBMS_PIPE.RECEIVE_MESSAGE. Cytowane ze względu na rodziny, a nie jako narzędzie, którego użył którykolwiek ze skanerów. ↩ -
PortSwigger Web Security Academy, “Blind SQL injection”, pobrano 3 listopada 2026 o 14:10:34 UTC. Dlaczego opóźnienie czasowe w zapytaniu opóźnia odpowiedź HTTP, cytowane. ↩
-
OWASP Automated Threats to Web Applications, “OAT-014 Vulnerability Scanning”, pobrano 3 listopada 2026 o 14:13:41 UTC. Podsumowanie, cytowane. ↩
-
Dane autora, aneks rejestru. Analityka żądań Cloudflare dla żądań POST obu skanerów do formularza pogrupowanych według statusu na krawędzi i statusu origin, odpytana 3 listopada 2026: 30 października 395 z 830 odpowiedzi 429 ma status origin 429, a pozostałe 435 nie mają zapisanego statusu; 2 listopada 16 z 31 odpowiedzi 429 ma origin 429, a 15 nie ma zapisanego. Żadna odpowiedź 429 nie ma statusu wyłącznie krawędziowego tam, gdzie zapisano status origin. ↩↩
-
Test autora, 3 listopada 2026: dziesięciowierszowa aplikacja FastAPI uruchomiona w środowisku samej witryny (FastAPI 0.128.0), z dekoratorem w stylu limitera na endpoincie. Żądanie POST bez wymaganego pola zwróciło 422, a wrapper nigdy nie został wywołany; kompletne żądanie POST dotarło do wrappera. ↩
-
Dane autora. Zdarzenia firewalla i analityka żądań Cloudflare dla mojego własnego adresu testowego 30 października 2026, odpytane 3 listopada: trzy zdarzenia z reguły limitu żądań, pierwsze o 18:52:38 UTC, z user-agentem przeglądarki, i 24 zarządzane wyzwania z reguły wyzwania opartej na user-agencie dla moich zgłoszeń z curl. Wyłączone ze wszystkich liczb dotyczących incydentu. ↩↩↩↩↩
-
RIPE NCC, RIPEstat network-info, whois i as-overview dla adresu z 2 listopada i jego AS, pobrano 3 listopada 2026 o 14:10:34 i 14:11:34 UTC: AS24875, posiadacz „NOVOSERVE-AS NovoServe B.V.”, trasa zarejestrowana na NovoServe B.V., kraj NL. Opis podprzydziału wymienia osobę prywatną i nie jest tu powtarzany. ↩
-
Dane autora. Analityka żądań Cloudflare dla adresu z 2 listopada pogrupowana według user-agenta, odpytana 3 listopada 2026, kompletna (73 ciągi obejmujące wszystkie 10 909 żądań): 9725 żądań z jednym zwykłym ciągiem Chrome 141 dla Windows, 942 podające się za Googlebota, 54 różne ładunki SQL injection w 149 żądaniach, 3 ciągi łamiące cudzysłowy w 78, 12 tokenów-znaczników
@@w 12 i 2 zwykłe inne. ↩↩↩↩↩↩ -
Dane autora. Tabele user-agentów z obu dni, zbudowane przez skrypt rejestru 3 listopada 2026: zwykły ciąg Chrome 141 dla Windows i ciąg Googlebota są bajt w bajt identyczne 30 października i 2 listopada. ↩
-
Odtworzenie wykonane przez autora 3 listopada 2026, z użyciem detektorów mojego monitora na kompletnych danych Cloudflare dla okresu od 18:58:23 do 19:03:23 UTC 2 listopada, czyli okna przebiegu rozpoczętego o 11:02:46 PST, uruchomione o 15:55:04 UTC i zapisane jako rekord odtworzenia z dokładnymi poleceniami i pełnym wyjściem detektorów; pięć wierszy aneksu rejestru wyprowadzono z tego rekordu skryptem: 2143 żądania z adresu w wykryciu fali żądań, 1055 odrzuconych żądań w wykryciu skanera oraz wykrycie fali na formularzu z 39 zgłoszeń, z których 23 odrzucono, o wysokiej ważności, które trafia do dziennego digestu i ani nie wysyła e-maila, ani nie blokuje. Liczba 1914 z przebiegu na żywo, w oknie kończącym się w chwili odpytania, to wartość zapisana w logu mojego monitora, a przebieg na żywo nie zgłosił wykrycia fali na formularzu. Zmiana okna wprowadzona tego samego dnia: każde okno na żywo kończy się teraz 90 sekund przed przebiegiem i zaczyna tam, gdzie skończyło się poprzednie, ze zwykłym pięciominutowym spojrzeniem wstecz, gdy brakuje poprzedniego rekordu albo jest on stary. ↩↩↩
-
Dane autora. Analityka żądań Cloudflare dla adresu z 2 listopada pogrupowana według ścieżki, odpytana 3 listopada 2026, kompletna (2318 wierszy obejmujących wszystkie 10 909 żądań): 2173 różne ścieżki i 778 żądań na 542 różnych ścieżkach z ładunkiem SQL injection. ↩↩
-
OWASP Web Security Testing Guide v4.2, “Testing for SQL Injection”, pobrano 3 listopada 2026 o 14:12:12 UTC. Techniki oparte na błędach i na opóźnieniach czasowych, cytowane. ↩
-
Zapis autora o trzeciej zmianie 3 listopada 2026, wprowadzonej po recenzji szkicu tego wpisu: o 16:12 UTC dozwolone domeny obu widżetów ustawione na witrynę i jej host www, localhost usunięty; domyślne listy nazw hostów obu weryfikatorów ustawione na te same dwie nazwy, a lokalne środowisko programistyczne ustawia własną listę; testy tej witryny sprawdzają, że localhost jest domyślnie odrzucany (11 przechodzi); strona studia wdrożona ponownie, a uruchomiona lokalnie z kluczem testowym Cloudflare, który zawsze przepuszcza, token testowy nadal docierał do walidacji, gdy zastępcza nazwa hosta tego klucza była na liście, i był odrzucany, gdy jej nie było. O 16:13 UTC wyłączono dwie reguły zarządzanego wyzwania oparte na wyniku zagrożenia, po jednej w każdej strefie, z uzasadnieniem w ich opisach; wyzwania oparte na user-agencie zostają. Testy na żywo po tej zmianie: strona studia o 16:15 UTC (widżet, zgłoszenia bez tokenu i ze śmieciowym tokenem 403, honeypot 200), ta witryna o 16:18 UTC (widżet, zgłoszenie bez tokenu 403); żaden nie sprawdza reguły nazwy hosta. ↩↩↩
-
Cloudflare, “Custom rules”, dokumentacja Cloudflare WAF, pobrano 3 listopada 2026 o 14:10:34 UTC. Co robią reguły niestandardowe, cytowane, oraz pięć reguł planu Free z każdą akcją oprócz Log. ↩
-
Cloudflare, “Rate limiting rules”, dokumentacja Cloudflare WAF, pobrano 3 listopada 2026 o 14:08:57 UTC. Parametry reguły; zastrzeżenie co do precyzji i opóźnienie aktualizacji liczników, cytowane; tabela dostępności, która daje planowi Free 10-sekundowy okres liczenia, 10-sekundowy czas mitygacji, liczenie według IP i jedną regułę. ↩↩↩↩↩↩
-
Ned Batchelder, “Stopping spambots with hashes and honeypots”, utworzone 21 stycznia 2007, pobrano 3 listopada 2026 o 14:10:34 UTC. Definicja honeypota, powód, dla którego boty wpadają w niewidoczne pola, taksonomia odtwarzania i wypełniania formularzy oraz jego dwa zdania o botach pokonujących te środki i o powrocie do CAPTCHA, wszystko cytowane. Strona opisuje technikę jako sposób, w jaki chroni swoją witrynę; nie twierdzi, że ją wymyślił. ↩↩↩↩↩↩
-
OWASP Cheat Sheet Series, “Credential Stuffing Prevention Cheat Sheet”, pobrano 3 listopada 2026 o 14:10:34 UTC. Nieopieranie się na jednym przewidywalnym limicie wolumenu; niski wolumen per adres pokonujący listy blokad i limity żądań; narzędzia wysyłające żądania bezpośrednio bez wykonywania JavaScriptu, wszystko cytowane. ↩↩↩
-
Cloudflare, “Cloudflare HTTP headers”, dokumentacja Cloudflare Fundamentals, pobrano 3 listopada 2026 o 14:14:20 UTC. Specjalne kody nagłówka CF-IPCountry, w tym „T1 - Used for clients using the Tor network.” ↩
-
Cloudflare, “IP Access rules parameters”, dokumentacja Cloudflare WAF, pobrano 3 listopada 2026 o 14:14:20 UTC. „T1: Tor exit nodes (country name: Tor)”. ↩
-
The Tor Project, “Bulk Tor Exit Exporter”, check.torproject.org, pobrano 3 listopada 2026 o 14:10:34 UTC. Opis listy, cytowany, i odesłanie do CollecTor po dane historyczne; sama lista, pobrana z https://check.torproject.org/torbulkexitlist w tym samym czasie, zawierała 1239 adresów. ↩↩
-
Dane autora, aneks rejestru. Każdy odrębny adres, który wysłał formularz strony studia w ośmiodniowym zapytaniu, podzielony według kodu kraju Cloudflare i porównany skryptem ze zbiorczą listą węzłów wyjściowych Tor Project pobraną o 14:10:34 UTC 3 listopada 2026 (1239 adresów): 23 z 23 adresów z kodem T1 na liście i 0 z 12 pozostałych. Tylko lista z 3 listopada; dni zdarzeń nie zostały sprawdzone w archiwum. ↩
-
The Tor Project, “Sources”, Tor Metrics, pobrano 3 listopada 2026 o 14:10:34 UTC. Listy węzłów wyjściowych TorDNSEL „containing the IP addresses of relays that it found when exiting through them” (zawierające adresy IP przekaźników znalezionych przy wychodzeniu przez nie) i ich archiwum. ↩↩
-
OWASP Automated Threats to Web Applications, “OAT-017 Spamming”, pobrano 3 listopada 2026 o 14:10:34 UTC. Podsumowanie, inne nazwy, w tym spam w formularzach, oraz wyłączenie nadużyć formularz-e-mail wymierzonych w niezamierzonych odbiorców, cytowane. ↩↩
-
Analiza autora. 108 zapisanych wiadomości strony studia posortowanych stałymi regułami, wygrywa pierwsze dopasowanie (dopasowanie po nazwie nadawcy dla pytania o cenę i przynęt nagrodowych, po treści wiadomości dla reszty, jeden wiersz przypisany ręcznie jako prawdziwe zapytanie), 3 listopada 2026: liczby w tabeli w sekcji 5; 25 języków pytania o cenę w całej tabeli i 9 w październiku, według mojego odczytania każdego stałego zdania, z chorwackim (lub bośniackim) i indonezyjskim (lub malajskim) jako przypadkami niepewnymi; każdy wiersz w oknie po zapytaniu sklasyfikowany jako spam. ↩↩↩↩↩
-
Cloudflare, “Turnstile widgets”, dokumentacja Turnstile, pobrano 3 listopada 2026 o 14:08:57 UTC. Tryby zarządzany, nieinteraktywny i niewidoczny oraz to, co odwiedzający widzi w każdym z nich, cytowane dla zarządzanego; warunek polityki prywatności dla trybu niewidocznego; origin challenges.cloudflare.com. ↩↩↩
-
Cloudflare, “Cloudflare Turnstile plans”, dokumentacja Turnstile, pobrano 3 listopada 2026 o 14:08:57 UTC. Free i Enterprise; do 20 widżetów; nieograniczona liczba wyzwań; użycie bez innych usług Cloudflare, cytowane. ↩
-
Cloudflare, “Validate the token”, dokumentacja walidacji po stronie serwera Turnstile, pobrano 3 listopada 2026 o 14:08:57 UTC. Wymóg wywołania siteverify; fałszowane tokeny; endpoint i jego pola
secret,responseiremoteip; ważność 300 sekund i jednorazowość; maksimum 2048 znaków; weryfikacja akcji i nazwy hosta; tabela kodów błędów i jej dwa opisy wygaśnięcia; limit żądań przeciw zalewowi walidacji. Wszystkie cytowane fragmenty pochodzą z tej strony. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Cloudflare, “Cloudflare Turnstile - Easy CAPTCHA Alternative”, strona produktu, pobrano 3 listopada 2026 o 14:08:57 UTC. „Turnstile never harvests data for ad retargeting.” ↩
-
Cloudflare, “Content Security Policy”, dokumentacja Turnstile, pobrano 3 listopada 2026 o 14:08:57 UTC.
script-srciframe-srcdla https://challenges.cloudflare.com; podejście oparte na nonce; wymógconnect-src 'self'dla pre-clearance, cytowany. ↩↩↩ -
Cloudflare, “Interstitial Challenge Pages”, dokumentacja Cloudflare Challenges, pobrano 3 listopada 2026 o 14:08:57 UTC. Jak strona wyzwania wstrzymuje żądanie; zarządzane wyzwania zalecane dla większości reguł WAF; ograniczenie dla AJAX i XHR (fetch) oraz zalecenie Turnstile pre-clearance, oba cytowane. ↩↩↩↩↩↩
-
Cloudflare, “Detect a Challenge Page response”, dokumentacja Cloudflare Challenges, pobrano 3 listopada 2026 o 14:10:34 UTC. Nagłówek
cf-mitigatedz wartością challenge i content-type text/html każdego wyzwania, cytowane. ↩ -
Cloudflare, “Clearance”, dokumentacja Cloudflare Challenges, pobrano 3 listopada 2026 o 14:10:34 UTC. Cookie
cf_clearancei pre-clearance, cytowane: cookie jest wydawane oprócz domyślnego jednorazowego tokenu, Siteverify pozostaje wymagane, a cookie pozwala omijać wyzwania do poziomu dopuszczenia ustawionego w widżecie. ↩↩↩ -
Zapis autora o drugiej zmianie 3 listopada 2026, wprowadzonej tego samego ranka: strona studia wdrożona o 15:19 UTC i sprawdzona na żywo o 15:22 UTC (widżet, skrypt, klucz witryny i origin wyzwań w działającym
frame-src; zgłoszenie bez tokenu i zgłoszenie ze śmieciowym tokenem odrzucone z 403; zgłoszenie do honeypota z odpowiedzią 200); ta witryna budowana od 15:20 UTC, z wyczyszczonym cache, sprawdzona na żywo o 15:25 UTC (te same testy strony, zgłoszenie bez tokenu odrzucone z 403, potem zgłoszenie z ważnym tokenem CSRF z działającej strony i bez tokenu Turnstile znów odrzucone z 403, co pokazuje, że formularz po wdrożeniu zamyka się w razie awarii, ale nie sprawdza weryfikacji nazwy hosta); oba weryfikatory wymagają teraz, by nazwa hosta w odpowiedzi Cloudflare była jedną z nazw witryny (odpowiedź bez niej jest odrzucana) oraz, gdy Cloudflare zwraca akcję, by była to akcja tego widżetu; jedenaście testów tej witryny przechodzi, a nowy akceptuje nazwę hosta witryny i odrzuca inną nazwę hosta, inną akcję i brak nazwy hosta; na stronie studia, uruchomionej lokalnie z kluczem testowym Cloudflare, który zawsze przepuszcza i odpowiada zastępczą nazwą hosta bez akcji, token testowy dotarł do walidacji, gdy ta nazwa była na liście, i został odrzucony, gdy jej nie było. ↩↩↩↩ -
Cloudflare, “Test your Turnstile implementation”, dokumentacja Turnstile, pobrano 3 listopada 2026 o 14:08:57 UTC. Klucze testowe zawsze przepuszczające i zawsze odrzucające oraz token testowy; produkcyjne sekrety odrzucające tokeny testowe; zautomatyzowane zestawy testów wykrywane jako boty, cytowane. ↩↩↩↩↩
-
Zapis autora o czwartej zmianie 3 listopada 2026, wprowadzonej po drugiej recenzji szkicu tego wpisu: handler pierwszej wersji dla zdarzenia before-swap w HTMX, który przepuszcza fragment 403 wbrew domyślnemu zachowaniu HTMX, niewstawiającemu odpowiedzi 4xx, był podpięty pod formularz kontaktowy, podczas gdy HTMX wywołuje to zdarzenie na celu podmiany, rodzeństwie formularza, więc fragment 403 nigdy się nie wyświetlał; recenzja wykryła to, czytając dołączony HTMX i szablon. Poprawka to listener na elemencie body strony ograniczony do id celu odpowiedzi, a test strony głównej sprawdza, że jest renderowany (11 testów przechodzi). Po sprawdzeniu wdrożenia o 16:18 UTC test w przeglądarce o 16:21 UTC w Chromium z Playwright zastąpił 730-znakowy token widżetu śmieciowym ciągiem, wypełnił wymagane pola wartościami zastępczymi i wysłał formularz przez HTMX; cel odpowiedzi pokazał fragment błędu, „error: Verification failed. Reload the page and try again.”, bez fragmentu sukcesu, i nic nie zostało wysłane. Niewykonane: pełne zgłoszenie przez człowieka. ↩↩
-
Cloudflare, “Embed the widget”, dokumentacja Turnstile, pobrano 3 listopada 2026 o 14:08:57 UTC. Skrypt api.js, kontener widżetu, ukryte pole
cf-turnstile-responsetworzone wewnątrz formularza orazturnstile.reset(widgetId)dla widżetów, którym minął czas lub które wygasły, cytowane. ↩↩↩ -
Zapis autora o testach na żywo 3 listopada 2026, każdy po tym, jak wdrożenie jego własnej witryny zakończyło się powodzeniem, i po wyczyszczeniu cache: o 06:35:24 i 06:35:54 PST jeden div widżetu, jeden tag skryptu i klucz witryny na każdej stronie głównej, origin wyzwań w każdym działającym
frame-src, a na tej witrynie zgłoszenie ze wszystkimi polami i śmieciowym tokenem CSRF odrzucone z 403 przez weryfikację CSRF; o 14:43 UTC na tej witrynie zgłoszenie z ważnym 108-znakowym tokenem CSRF z działającej strony i bez tokenu Turnstile odrzucone z 403 i komunikatem „Verification failed. Reload the page and try again.”, a to samo zgłoszenie ze śmieciowym tokenem Turnstile odrzucone z 403; na stronie studia zgłoszenie bez tokenu i zgłoszenie ze śmieciowym tokenem odrzucone z 403, a zgłoszenie do honeypota z odpowiedzią 200 bez zapisu i wysyłki; w Chromium z Playwright o 14:39:50 i 14:40:04 UTC widżet wyrenderowany (602 na 73 i 557 na 71 pikseli), obiekt API Turnstile załadowany, a ukryte pole odpowiedzi z 730-znakowym tokenem na obu stronach w ciągu sześciu sekund. Niewykonane: pełne zgłoszenie przez człowieka. ↩↩↩ -
Cloudflare, “Rate limiting parameters”, dokumentacja Cloudflare WAF, pobrano 3 listopada 2026 o 14:08:57 UTC. Nazwy parametrów; w planach Free, Pro i Business akcja wyzwania zawsze dławi ruch, parafrazowane; przejście wyzwania zeruje licznik, cytowane. ↩
-
OWASP Cheat Sheet Series, “Authentication Cheat Sheet”, pobrano 3 listopada 2026 o 14:10:34 UTC. CAPTCHA jako kontrola obrony w głąb, cytowane. ↩
-
Resend, “Retrieve Sent Email”, dokumentacja API Resend, pobrano 3 listopada 2026 o 14:14:47 UTC. Pole
last_eventpobranego e-maila, na przykład „delivered”. ↩↩ -
Resend, “View and manage emails”, dokumentacja Resend, pobrano 3 listopada 2026 o 14:15:12 UTC. Lista zdarzeń e-maila, w tym „sent” i „delivered”, cytowane, oraz „queued” tylko dla Broadcasts i Batches. ↩
-
Cloudflare, “Onion Routing and Tor support”, dokumentacja Cloudflare Network, pobrano 3 listopada 2026 o 14:08:57 UTC. Onion Routing w każdym planie i rozróżnienie ludzkich użytkowników Tora i botów, cytowane. ↩
-
Cloudflare, “Challenges”, dokumentacja Cloudflare Challenges, pobrano 3 listopada 2026 o 14:08:57 UTC. „Cloudflare does not use CAPTCHA puzzles or visual tests like selecting objects or typing distorted characters.” ↩
-
OWASP Community, “Blind SQL Injection”, pobrano 3 listopada 2026 o 14:10:34 UTC. Ślepe wstrzyknięcie czasowe opierające się na wstrzymaniu bazy danych, z
WAIT FOR DELAYipg_sleep()jako przykładami. ↩