Por que o spam venceu meu formulário de contato com limite de taxa
Por que o spam atravessa um formulário de contato com limite de taxa? Porque um limite de taxa conta requisições por endereço, e nada nele pergunta quem as enviou. Em 30 de outubro e 2 de novembro, dois scanners de vulnerabilidades rastrearam meu site, carregaram a página inicial que traz o formulário, como faz qualquer visitante, pegaram um token CSRF válido e enviaram o formulário até seguirem adiante ou serem bloqueados. No primeiro dia passaram 101 envios e no segundo 31, cada um com um 200 da aplicação, que ela devolve tanto para um envio quanto para um envio que falhou, então esses números são o máximo de e-mails que eles poderiam ter gerado.12 O limitador da minha aplicação, configurado para cinco envios por minuto por endereço, recusou 830 e 31 dos envios deles e deixou passar 101 e 31, 47 e 9 a mais do que um limitador exato com essa configuração teria permitido.3 O limite de taxa na edge nunca disparou com nenhum dos dois scanners: eu o criei cerca de 12 minutos depois do último envio do formulário da primeira enxurrada, e os envios do segundo scanner chegaram por dois data centers da Cloudflare, que mantêm contadores separados. Essa divisão é minha explicação mais forte para o segundo silêncio: ela se encaixa nos totais por minuto, que não podem prová-la.456 No site do meu estúdio, um campo honeypot pegou 42 de 52 envios em oito dias e deixou passar os 10 cujos remetentes deixaram o honeypot vazio.78 A edge deste site tinha, sim, uma regra capaz de perguntar se há uma pessoa do outro lado, um desafio gerenciado no caminho do formulário, mas ele só dispara com uma string de user agent de uma lista, e os dois scanners enviaram a string de um navegador; um segundo desafio, baseado em uma pontuação de ameaça alta, nunca poderia disparar, porque a Cloudflare agora fixa essa pontuação em zero, então nada fez essa pergunta a cada envio.910411 Desde a manhã de 3 de novembro, o Turnstile faz essa pergunta a cada envio que poderia chegar à minha caixa de entrada, nos dois formulários.12
TL;DR
- Dois scanners, uma caixa de entrada. Um endereço enviou ao meu site 76.429 requisições em 53 intervalos de um minuto em 30 de outubro; outro enviou 10.907 em 22 em 2 de novembro. Os dois rastrearam o site inteiro, os dois enviaram strings de injeção de SQL nos caminhos e no cabeçalho User-Agent, e os dois enviaram o formulário de contato: 1.065 vezes no primeiro dia e 500 no segundo.131412
- Um token CSRF válido só prova que um cliente tinha um token emitido pelo meu site. Todo envio dos dois dias carregava um, porque meus tokens não têm estado e qualquer cliente que baixa a página recebe um. A verificação CSRF recusou quatro envios em duas enxurradas.1516
- Meu próprio limitador vazava. Ele lê um contador do Cloudflare KV, compara, depois lê de novo e grava a contagem mais um em uma requisição separada; se a segunda leitura falha, ele grava um, e se uma chamada ao KV falha, ele recorre a uma contagem guardada na memória de um único processo. Qualquer um desses caminhos pode deixar passar mais de cinco, e os logs não dizem qual deles fez isso: 18 envios saíram em um minuto contra um limite de cinco.171
- O limite de taxa na edge conta por endereço e por data center. Em 2 de novembro, os 85 envios do formulário feitos pelo scanner entre 18:56 e 19:05:00 UTC, os minutos de cada lado do bloqueio, vieram por Amsterdã e Londres, 44 e 41, e o minuto mais movimentado se dividiu em 18 a 14 entre os dois. Essa divisão é o motivo mais provável para a regra ter ficado em silêncio, embora os totais por minuto não possam prová-lo. Um teste que fiz na manhã seguinte provou que a regra dispara e que a Cloudflare registra isso.518
- Os bloqueios foram bloqueios de endereço, e os dois chegaram tarde demais para manter o e-mail de fora. Em 30 de outubro bloqueei o endereço à mão 3,9 minutos depois de perceber, cerca de 50 minutos depois do primeiro envio do formulário. Em 2 de novembro meu monitor o bloqueou 8 minutos depois do primeiro minuto da varredura. Nenhum envio de nenhum dos dois dias veio depois do minuto do bloqueio: em 30 de outubro o último veio mais de 12 minutos antes do bloqueio, e em 2 de novembro 26 vieram nos minutos anteriores e 5 no próprio minuto do bloqueio, que os dados por minuto não conseguem situar antes ou depois do segundo do bloqueio.101912
- O honeypot pega os bots que preenchem todos os campos. No site do meu estúdio ele respondeu a 42 de 52 envios como se tivessem dado certo e não gravou nada; pela hora em que chegaram, todo envio vindo de uma saída do Tor estava entre eles. Os 10 que foram gravados, e portanto no máximo 10 e-mails, vieram de remetentes que deixaram o honeypot vazio.720
- O que mudou. Os dois formulários agora têm um widget do Cloudflare Turnstile em modo gerenciado, e o servidor não grava nem envia nada até que a API siteverify da Cloudflare aceite o token do envio, recusando um token ausente, grande demais ou impossível de verificar. O Turnstile executa desafios no navegador do visitante e processa sinais como endereço IP, impressão digital TLS e user agent; a Cloudflare diz que ele não toca no que é digitado nos formulários. Ele não impede uma pessoa que digita spam à mão e não substitui os limites de taxa, que continuam limitando o volume.122122
1. A pergunta: “é você?”
Em 2 de novembro minha caixa de entrada se encheu de mensagens do formulário de contato, e minha primeira pergunta, feita à sessão de agente que opera minha infraestrutura, foi se a minha própria automação estava enviando aquilo. Não estava. Quem enviava era um scanner, pelo formulário de contato deste site, e o formulário do site do meu estúdio, 941apps.com, recebia um gotejamento lento próprio, embora nada dele tenha chegado à caixa de entrada naquele dia.238 Os três fluxos de e-mail, os alertas do meu monitor e os dois formulários, saem de um mesmo endereço do meu próprio domínio de e-mail, então na caixa de entrada eles pareciam iguais.23
Os assuntos entregaram o jogo. O handler de contato deste site me envia cada mensagem com o assunto “Contact Form:” seguido do que o remetente digitou nos campos de nome, então o payload de um scanner vira uma linha de assunto.2423 O formulário do site do estúdio grava cada mensagem em um banco de dados e depois a envia por e-mail para a mesma caixa.25
A pergunta que fiz primeiro era a errada, e a certa levou um dia de consultas para responder: qual dos controles desses formulários poderia ter distinguido um scanner de uma pessoa, e por que cada um deixou passar o resto? Havia cinco tipos de controle. Neste site, um firewall na edge com regras personalizadas, um limite de taxa na edge, um token CSRF e um limitador de taxa na aplicação. No site do estúdio, o mesmo tipo de regras na edge, um limite de taxa na edge e um limitador próprio da aplicação que não recusou nenhum dos envios observados, além de um campo honeypot.9257 O resto deste post percorre as duas enxurradas e o gotejamento lento, um de cada vez, e depois as cinco camadas, uma de cada vez, com o texto da regra ou o comportamento do código para cada uma.
De onde vêm os números
Toda contagem abaixo vem das análises de requisições e dos eventos de firewall da Cloudflare para as duas zonas, consultados em 3 de novembro sobre janelas já fechadas, além da tabela de mensagens do site do estúdio e do log do meu próprio monitor.16 Um script transformou os resultados salvos dessas consultas em um registro de algumas centenas de linhas, cada uma com a regra que a produziu, e o post tira seus números do registro e do seu anexo e de nenhum outro lugar. Onde minhas anotações do dia discordam do registro, o registro vence: escrevi as anotações de 30 de outubro minutos depois da enxurrada, enquanto a Cloudflare ainda ingeria os últimos minutos, e elas contam a menos.1626 A Cloudflare mantém esses conjuntos de dados por “at least 31 days of data on every plan” (pelo menos 31 dias de dados em todos os planos), então pude contar uma enxurrada depois do fato, mas não para sempre.27
Dois tipos de número pedem um aviso. Os eventos de firewall são uma amostra, então eu os uso para saber qual regra agiu e quando, e os conjuntos de dados de requisições para saber quantas foram.16 E um envio do formulário que recebeu 200 neste site é um envio, ou uma falha à qual o handler também responde com 200 (ele tem dois caminhos de falha assim), não um e-mail entregue, e eu não verifiquei o log do meu provedor de e-mail. Toda contagem de e-mails deste post é, portanto, um teto.2428
Como era o normal
Antes das enxurradas, o formulário era tranquilo. Nos oito dias cobertos pelas consultas, três endereços além dos dois scanners enviaram o formulário com sucesso, cada um exatamente duas vezes, um endereço por dia em 27, 28 e 29 de outubro.29 Não sei se eram pessoas. Dois envios de um mesmo endereço combinam com um clique duplo, e combinam com um bot que tenta de novo. Essa incerteza é o problema inteiro em miniatura: nada nos logs nem nos controles conseguia dizer.
2. Primeira enxurrada: 30 de outubro, bloqueada à mão
A primeira enxurrada veio de um único endereço em uma rede que o registro regional de internet lista sob BULLIONET FBW NETWORKS SAS, registrada na França.30 Sua primeira requisição caiu no minuto das 17:59 UTC, 10:59 no horário de verão do Pacífico, e a última no minuto das 18:51, 11:51 PDT: 53 intervalos de um minuto e 76.429 requisições a este site. Duas consultas independentes, uma por minuto para o endereço e outra varrendo todas as zonas da minha conta, concordam nesse total, e o endereço não enviou nada antes nem depois.13
O formato da varredura
O formulário foi uma parte pequena dela. Antes do bloqueio, o endereço fez em média 1.389,7 requisições por minuto, com pico de 3.219 no minuto das 18:36, e percorreu o site inteiro: pelo menos 2.881 caminhos distintos, um limite inferior porque a consulta de caminhos bateu no seu limite de linhas.1331 Na varredura inteira, os resultados se dividiram assim:13
| Resultado | Requisições | Proporção |
|---|---|---|
| Servidas (2xx, 3xx) | 49.513 | 64,8% |
| Recusadas pela aplicação (4xx), incluindo 10.019 da barreira de agentes do site | 18.990 | 24,8% |
| Erros de origem (520) | 2.339 | 3,1% |
| Bloqueadas na edge (403) | 5.587 | 7,3% |
Os 7,3% da edge são quase todos o bloqueio: 5.234 de suas 5.587 recusas vieram depois do minuto do bloqueio.13 Nos cinquenta minutos anteriores, só a regra de caminhos de exploit agiu. Essa regra funcionou desde o primeiro minuto, com o primeiro evento de firewall às 17:59:53 UTC, mas ela casa com trechos de caminho como .php e /wp-, e a maior parte desta varredura pedia páginas reais.109
O que fazia disso uma varredura e não um rastreamento era o que ela colocava nas requisições. Quase todo o tráfego, pelo menos 69.322 requisições, usava um único user agent comum do Chrome 141 para Windows. Pelo menos mais 2.755 se diziam Googlebot, e a regra de bots verificados da edge não as tratou como Google.32 Pelo menos 3.154 requisições traziam um payload de injeção de SQL no próprio cabeçalho User-Agent, em pelo menos 396 strings distintas, e pelo menos 2.680 requisições traziam um no caminho, em pelo menos 810 caminhos distintos.3231 Os payloads pertencem a famílias documentadas. PG_SLEEP(15), waitfor delay '0:0:15' e XOR(if(now()=sysdate(),sleep(15),0)) são sondas baseadas em tempo: a folha de referência da PortSwigger lista a sintaxe de atraso para Oracle, Microsoft SQL Server, PostgreSQL e MySQL, e as definições de payload do próprio sqlmap contêm as mesmas funções.3334 A PortSwigger explica por que elas funcionam: “As SQL queries are normally processed synchronously by the application, delaying the execution of a SQL query also delays the HTTP response.” (como a aplicação normalmente processa as consultas SQL de forma síncrona, atrasar a execução de uma consulta também atrasa a resposta HTTP).35 Uma resposta lenta prova pouco; um atraso de cerca de 15 segundos que volta toda vez que o payload o pede, e não aparece nas requisições de controle sem ele, é evidência de uma possível injeção.
O app de pintura deste site tem um endpoint de compartilhamento, e o scanner o encontrou. Ele enviou 320 POSTs para o caminho de compartilhamento, o único endereço a fazer isso nos oito dias cobertos pelas consultas, e o endpoint recusou todos os 320 e não gravou nada. Em volta deles, enviou POSTs para variantes do caminho com o payload embutido, como /paint/share'||DBMS_PIPE.RECEIVE_MESSAGE(CHR(98)||CHR(98)||CHR(98),15)||' e /paint/(select 198766*667891 from DUAL), às quais o site respondeu com 404.31 O endereço também tocou uma outra zona da minha conta, enviando 387 GETs para kiradex.app, todos respondidos com 200, e nenhuma outra.13 Minha leitura é que ele seguiu links deste site para aquele; não verifiquei os links.
O catálogo de ameaças automatizadas da OWASP tem um nome para essa atividade: OAT-014, Vulnerability Scanning, “Crawl and fuzz application to identify weaknesses and possible vulnerabilities.” (rastrear a aplicação e aplicar fuzzing para identificar fraquezas e possíveis vulnerabilidades).36 O formulário de contato era só mais uma entrada para o fuzzing.
O formulário, minuto a minuto
O endereço enviou o formulário de contato 1.065 vezes, e todos os envios chegaram à aplicação; a edge não barrou nenhum.1 O primeiro envio e o primeiro e-mail saíram no minuto das 18:00 UTC, 11:00 PDT. Aqui está cada minuto em que um envio do formulário chegou à aplicação, pelo status que a aplicação devolveu:1
| UTC | PDT | Enviados (200) | Recusados por CSRF (403) | Campo ausente (422) | Recusados pelo limitador (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 |
Os totais: 101 enviados, 830 recusados pelo limitador da aplicação, 130 recusados porque faltava um campo obrigatório e 4 recusados pela verificação CSRF, os quatro no minuto das 18:03.1 Os 429 eram da aplicação. A regra de limite de taxa da edge ainda não existia, não há nenhum evento de firewall de limite de taxa para o endereço, e onde a Cloudflare registrou o status da origem para esses envios, o 429 veio da origem.1037 Os 422 vieram da validação de formulários do FastAPI, que roda antes do limitador, então nunca gastaram uma vaga do limitador; conferi a ordem com um app FastAPI de dez linhas, no qual um envio sem um campo obrigatório devolveu 422 sem nunca chamar o wrapper.38 A rajada de 129 às 18:37 parece o scanner aplicando fuzzing aos campos do formulário ao omiti-los.
Leia a coluna “Enviados” contra o limitador. O limitador permite cinco envios por endereço por minuto de relógio, e em oito desses minutos saíram mais de cinco, 18 deles às 18:06.13 A seção 4 explica como.
A percepção e o bloqueio
Percebi às 11:46 PDT e anotei que alguma coisa batia no formulário de contato do site sem parar.26 A essa altura o primeiro envio do formulário tinha cerca de 46 minutos, e a enxurrada no formulário em si tinha terminado às 11:37 PDT.1 Adicionei o endereço à mão à lista de bloqueio da conta com a qual a primeira regra personalizada da zona já casa, e o primeiro evento dessa regra veio às 18:49:53 UTC, 11:49:53 PDT.10 O bloqueio veio 3,9 minutos depois que percebi, cerca de 50 minutos depois do primeiro envio do formulário e 50,9 minutos depois do primeiro minuto da varredura.113 Todos os 101 envios já tinham saído antes dele; o último envio do formulário a chegar à aplicação veio cerca de 12 minutos antes do bloqueio.1 Depois do minuto do bloqueio, o endereço enviou mais 5.234 requisições, e a edge recusou todas.13
No mesmo segundo, 18:49:53 UTC, criei a única regra de limite de taxa da zona: POSTs de formulário para os caminhos de contato e de compartilhamento do app de pintura, 3 a cada 10 segundos por endereço, com bloqueio por 10 segundos.9 Às 18:52:38 UTC eu a testei da minha própria máquina. Nas minhas anotações desse teste, seis envios com um segundo de intervalo, com user agent de navegador e sem um token CSRF válido, “returned 403 403 403 429 429 429 (the app’s CSRF rejection, then the edge’s rate limit)” (devolveram 403 403 403 429 429 429: a recusa CSRF da aplicação e depois o limite de taxa da edge), e envios simples com curl nunca chegaram à regra, porque uma regra personalizada existente serviu um desafio gerenciado a eles.26 Minhas anotações dizem que os envios não tinham token, mas um envio sem esse campo leva um 422 antes de o handler rodar, como mostrou meu teste de 3 de novembro, então os 403 indicam um token presente e inválido.18 Os eventos de firewall concordam: três eventos de limite de taxa do meu endereço de teste, o primeiro naquele segundo, e 24 desafios gerenciados para meus envios com curl.39 Nada do meu tráfego de teste está em nenhuma contagem acima.
Meu monitor não estava rodando naquele dia. Eu o reativei em 31 de outubro, e o detector de tráfego dele veio depois desta enxurrada.19 O bloqueio de 30 de outubro foi feito por uma pessoa lendo a caixa de entrada.
30 de outubro: 76.429 requisições de um único endereço, por minuto; até eu bloquear o endereço à mão, a edge quase não recusou nenhuma.1310
3. Segunda enxurrada: 2 de novembro, bloqueada pelo meu monitor
O segundo endereço pertence à NovoServe B.V., um provedor de hospedagem na Holanda.40 Ele enviou dois GETs às 07:49 UTC, 23:49 no horário padrão do Pacífico de 1º de novembro, e depois nada por 11,1 horas. A varredura propriamente dita foi do minuto das 18:56 UTC ao das 19:17, das 10:56 às 11:17 PST: 22 intervalos de um minuto e 10.907 requisições, ou 10.909 com os dois GETs iniciais. Quatro consultas independentes concordam no total.14
Parecia a mesma ferramenta. Usou a mesma string de user agent do Chrome 141 para Windows que o endereço de 30 de outubro em 9.725 requisições, a mesma string de Googlebot em 942, as mesmas famílias de injeção, e tocou as mesmas duas zonas e nenhuma outra, com 368 GETs para kiradex.app.414214 Dois endereços em duas redes de dois países não provam nada sobre quem os operava, então paro em “parecia”.
A linha do tempo, nos dois fusos horários
| UTC | PST | O que aconteceu |
|---|---|---|
| 18:56 | 10:56 | A varredura começa: 29 requisições no minuto.1419 |
| 18:57:00 | 10:57:00 | Começa uma execução do meu monitor. A janela dela fecha às 18:57:22, então ela vê cerca de um minuto da varredura, e não encontra nada.19 |
| 18:57 | 10:57 | O minuto mais movimentado antes do bloqueio, 637 requisições. A regra de caminhos de exploit da edge começa a bloquear às 18:57:11.144 |
| 18:58 | 10:58 | Os três primeiros envios do formulário. A aplicação envia os três.2 |
| 19:00 | 11:00 | Mais quatro envios do formulário, os quatro enviados.2 |
| 19:02 | 11:02 | 32 envios do formulário chegam à aplicação: 9 enviados, 23 recusados pelo limitador da aplicação.2 |
| 19:02:46 | 11:02:46 | Começa a execução seguinte do monitor.19 |
| 19:03 | 11:03 | 10 enviados, 8 recusados.2 |
| 19:03:53 | 11:03:53 | Meu monitor levanta um achado crítico de enxurrada de requisições: 1.914 requisições do endereço.19 |
| 19:04:01 | 11:04:01 | Meu monitor adiciona o endereço à lista de bloqueio da conta.19 |
| 19:04:03 | 11:04:03 | O primeiro evento da regra da lista de bloqueio.4 |
| 19:04 | 11:04 | Os últimos cinco envios e 22 envios do formulário recusados na edge, todos no próprio minuto do bloqueio; o intervalo do minuto não diz o que veio primeiro.2 |
| 19:07 | 11:07 | O minuto mais movimentado da varredura, 1.107 requisições, todas bloqueadas.14 |
| 19:17 | 11:17 | O último minuto, 300 requisições, todas bloqueadas.14 |
Os oito minutos do meu monitor
Meu monitor levantou seu primeiro achado 7,9 minutos depois do primeiro minuto da varredura, e o bloqueio entrou em vigor 8,0 minutos depois dele, ou 6,8 minutos depois do primeiro evento de firewall.194 A essa altura, 26 envios tinham saído nos minutos anteriores ao bloqueio, e os últimos 5 caíram no próprio minuto do bloqueio, que os dados por minuto não conseguem situar antes ou depois do segundo do bloqueio; nenhum veio nos minutos seguintes.2 Oito minutos é rápido para um bloqueio e lento para uma caixa de entrada.
A execução que começou às 10:57:00 PST teve a varredura à vista por cerca de um minuto, e na janela dela o endereço fez algo entre 29 e 666 requisições; o número maior conta o minuto inteiro das 18:57, do qual a janela viu só 22 segundos. O detector não registrou nenhum achado.19 A execução seguinte, às 11:02:46 PST, encontrou a enxurrada e a bloqueou, e essa execução mostra por que a primeira podia deixá-la passar. A janela dela terminava no momento em que consultava a Cloudflare, antes de os eventos mais recentes chegarem: o log dela registra 1.914 requisições do endereço, enquanto minha reprodução da mesma janela de cinco minutos em 3 de novembro, sobre dados completos, conta 2.143; a reprodução está salva com os comandos e a saída completa, e suas contagens são linhas do anexo do registro.1943 A reprodução também levanta um achado de enxurrada no formulário que a execução real nunca levantou, a partir de 39 envios, 23 deles recusados, com severidade alta, que teria sido uma linha no resumo diário e não um e-mail nem um bloqueio.43 Reproduzi a janela da segunda execução, não a da primeira, então para a execução das 10:57 PST o mesmo atraso é a causa provável, não uma causa medida. No mesmo dia mudei as janelas do monitor para terminarem 90 segundos antes de cada execução e começarem onde a anterior terminou.43 No total, seis achados dispararam sobre o endereço, e meu monitor incorporou todos ao resumo diário em vez de me mandar e-mail, porque o endereço já estava no registro de bloqueios dele.19 Como meu monitor deveria alertar, e este scanner visto do lado dele, é o assunto de um post à parte; para este, o que importa é que um bloqueio é uma reação, e a reação veio depois de pelo menos 26 dos 31 envios.
O formulário, por status
O endereço enviou o formulário 500 vezes.2
| UTC | PST | Enviados (200) | Recusados pelo limitador (429) | Bloqueados na edge (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 |
Os totais: 31 enviados, 31 recusados pelo limitador da aplicação e 438 recusados na edge, todos no minuto do bloqueio ou depois. Então 62 envios chegaram à aplicação.2 Cada envio que saiu prova que o scanner tinha um token CSRF válido, porque o handler verifica o token antes de enviar. Os 429 não provam nada em nenhum sentido, porque o limitador recusa um envio antes de o handler ler o token.2416 Onde a Cloudflare registrou o status da origem para esses 429, era o da origem, o do limitador da aplicação, e não há nenhum evento de firewall de limite de taxa para o endereço.374
O resto da varredura
Por resultado, a varredura de 2 de novembro se dividiu assim:14
| Resultado | Requisições | Proporção |
|---|---|---|
| Servidas (2xx, 3xx) | 1.388 | 12,7% |
| Recusadas pela aplicação (4xx), incluindo 420 da barreira de agentes | 1.473 | 13,5% |
| Erros de origem (520) | 116 | 1,1% |
| Bloqueadas na edge (403) | 7.930 | 72,7% |
Os 72,7% da edge são principalmente o bloqueio: 2.986 requisições vieram antes do minuto do bloqueio e 7.445 depois, e o site não serviu nenhuma destas últimas.14 A consulta de caminhos deste dia foi completa: 2.173 caminhos distintos, e 778 requisições em 542 deles traziam um payload de injeção de SQL.44 A consulta de user agents também foi completa, 73 strings distintas. Além da string comum do Chrome e da alegação de ser Googlebot, o scanner enviou 54 payloads SQL distintos em 149 requisições, três strings para quebrar aspas em 78 e 12 tokens marcadores, cada um enviado uma vez.41
Vale olhar algumas dessas strings, porque elas mostram o que o scanner perguntava. (select 198766*667891 from DUAL) é uma expressão aritmética dentro de um select sem tabela; FROM dual é a sintaxe do Oracle para isso, como mostra a folha de referência da PortSwigger.33 if(now()=sysdate(),sleep(15),0) tem a mesma forma IF(condition, SLEEP(n), x) do atraso condicional da mesma folha de referência.33 @@qkLpP é um prefixo @@, o que a folha de referência usa em SELECT @@version para Microsoft e MySQL, seguido de cinco caracteres, e o scanner enviou uma dúzia parecidas.3341 O guia de testes da OWASP nomeia as duas técnicas a que essas strings servem: a injeção baseada em erro, que “forces the database to generate an error” (força o banco de dados a gerar um erro), e os atrasos de tempo que “use database commands (e.g. sleep) to delay answers in conditional queries.” (usam comandos do banco de dados, como sleep, para atrasar respostas em consultas condicionais).45 Nenhuma das páginas que consultei documenta essas strings exatas, então eu as descrevo e não as atribuo a uma ferramenta específica.
2 de novembro: 10.907 requisições de um único endereço, por minuto, com as duas execuções do meu monitor e o bloqueio que ele aplicou às 11:04:01 PST.1419
4. As cinco camadas, e o que cada uma conseguia distinguir
Cinco tipos de controle ficavam entre esses bots e minha caixa de entrada. A cada um fiz a mesma pergunta: ele conseguia distinguir um bot de uma pessoa? Um desafio gerenciado nas regras da edge pode fazer essa pergunta a um visitante, mas só às requisições cuja string de user agent está na lista dele, e as dos scanners não estavam; um segundo, baseado em uma pontuação de ameaça, nunca poderia disparar, então nada fez essa pergunta a cada envio. Dois dos controles contam, um compara nomes e listas, um prova que um cliente tem um token emitido pelo site e um pega só os bots que preenchem um campo oculto. Aqui está cada um, o que ele verifica, o que barrou e o motivo exato pelo qual deixou passar o resto.
Regras personalizadas da edge
O que verificam. Neste site, a primeira regra personalizada bloqueia qualquer endereço de uma lista no nível da conta, mais sete redes de hospedagem por ASN. Outra bloqueia trechos de caminhos de exploit como .php, /wp- e /.env. Mais duas servem um desafio gerenciado no formulário de contato e no caminho de administração: uma para um user agent vazio ou que contenha curl, wget, python-requests, httpx, scrapy, HeadlessChrome, PhantomJS ou Selenium, e outra para uma pontuação de ameaça alta da Cloudflare. O site do estúdio tem regras do mesmo tipo, com os dois desafios aplicados ao site inteiro e um bloqueio nos caminhos de API que isenta o formulário de contato.9 Os desafios por pontuação de ameaça, um em cada site, eram regras mortas: a referência de campos da Cloudflare diz que a pontuação ia antes de 0 a 100, e “Now, the threat score is always 0 (zero).” (agora a pontuação de ameaça é sempre 0). Uma regra que pede uma pontuação alta não casa com nada, e desativei as duas em 3 de novembro, o dia em que li isso; os desafios por user agent continuam.1146 O plano Free da Cloudflare permite cinco regras personalizadas, que “work as customized web application firewall (WAF) rules that you can use to perform actions like Block or Managed Challenge on incoming requests.” (funcionam como regras personalizadas de firewall de aplicações web com as quais você pode executar ações como Block ou Managed Challenge sobre as requisições recebidas).47
O que barraram. Os caminhos de exploit, desde o primeiro minuto nos dois dias.104 Em 30 de outubro, nenhum envio do formulário.110 Em 2 de novembro, os 438 envios do formulário posteriores ao bloqueio do meu monitor.2 No site do estúdio, nenhum envio do formulário: o firewall não registrou nenhum evento sobre um POST ao formulário nos oito dias.7
Por que o resto passou. Nenhuma regra olha para o que uma requisição é. A regra da lista só casa com um endereço depois que algo o coloca na lista, uma pessoa em 30 de outubro e meu monitor em 2 de novembro.91019 O desafio por user agent compara uma string, e os dois scanners enviaram uma string de navegador, pelo menos 69.322 vezes no primeiro dia e 9.725 vezes no segundo.3241 O desafio por user agent funciona, sim, contra ferramentas que se anunciam: meu próprio teste com curl em 30 de outubro provocou 24 desafios gerenciados.39 O desafio por pontuação de ameaça não podia disparar com ninguém, e o firewall não registrou nenhum desafio no formulário para nenhum dos dois endereços.11104 Saídas do Tor e redes de hospedagem fora dos sete ASNs citados não estão em nenhuma lista.
Uma regra da edge consegue distinguir um endereço conhecido, uma rede conhecida ou uma ferramenta honesta do resto, e o desafio dela pergunta se há uma pessoa só às requisições que sua expressão seleciona; ela não consegue distinguir uma string de navegador enviada por um scanner de um navegador.
Limite de taxa na edge
O que verifica. O texto da regra é curto:9
(http.request.method eq "POST" and http.request.uri.path in {"/api/contact" "/paint/share"})
Ela conta as requisições que casam por data center da Cloudflare e endereço do cliente, permite 3 a cada 10 segundos e bloqueia por 10 segundos. Distribuído de forma uniforme, esse teto equivale nominalmente a 18 por minuto por endereço por data center; a regra em si conta em períodos de 10 segundos.916 O plano Free fixa o período, o tempo de bloqueio e a chave de contagem, e dá à zona uma única regra desse tipo: um período de contagem de 10 segundos, um tempo de mitigação de 10 segundos e contagem por IP.48 O data center também não é opcional. “Every rate limiting rule includes the Cloudflare data center ID (cf.colo.id) as a mandatory characteristic” (toda regra de limite de taxa inclui o ID do data center da Cloudflare como característica obrigatória), diz a documentação da Cloudflare, e “Counters are not shared across data centers, with the exception of data centers associated with the same geographical location.” (os contadores não são compartilhados entre data centers, exceto entre os associados à mesma localização geográfica).6
O que barrou. Nenhuma requisição dos scanners em nenhum dos dois dias. Seus únicos eventos na janela do incidente foram os três do meu próprio teste.10439 O site do estúdio tem uma regra de limite de taxa própria para o site inteiro, contada do mesmo jeito sobre todas as requisições exceto as de clientes que a Cloudflare lista como bots verificados, e ela não recusou nenhum dos envios do gotejamento: o firewall dela não registrou nenhum evento sobre um envio ao formulário nos oito dias. Essa regra conta cada requisição desse tipo que um endereço envia ao site, e meus dados contam só os envios do formulário, então não medem quanta folga restava ao contador.97
Por que o resto passou. Em 30 de outubro, a regra não existiu até 18:49:53 UTC, o mesmo segundo do primeiro evento do bloqueio manual e cerca de 12 minutos depois que o último envio do formulário chegou à aplicação.91 Os 1.065 envios daquele scanner vieram todos por um único data center, Paris, então uma regra em vigor teria mantido um único contador para eles.5 Em 2 de novembro a regra existia, e os envios do formulário feitos pelo scanner vieram por dois data centers, Amsterdã com 44 e Londres com 41, entre 18:56 e 19:05:00 UTC. No minuto mais movimentado, 19:02, Londres levou 18 e Amsterdã 14.5 Dezoito em um minuto é o equivalente nominal do teto da regra, mas meus dados têm totais por minuto, não horários de chegada, então não podem mostrar se algum período de 10 segundos em um data center teve mais de três. A Cloudflare é clara quanto à precisão: “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.” (as regras de limite de taxa não foram projetadas para deixar chegar um número preciso de requisições ao servidor de origem; pode haver um atraso de até alguns segundos entre detectar uma requisição e atualizar os contadores).48 No intervalo de sete minutos entre o primeiro envio a chegar à aplicação e o último, o scanner fez em média 8,86 por minuto.2 Minhas anotações do dia chamaram isso de “about nine a minute” (cerca de nove por minuto) e pararam aí; a divisão entre data centers foi o que escapou a elas.23
Eu queria uma prova de que a regra funciona, então na manhã de 3 de novembro eu a testei da minha própria máquina. Às 06:36:49 PST enviei dez POSTs com 0,4 segundo de intervalo sem token CSRF: quatro voltaram com 422, porque faltava um campo obrigatório e o FastAPI os recusou, e depois seis voltaram com 429. Às 06:38:03 PST enviei oito com todos os campos presentes: três voltaram com 403, a recusa CSRF da origem, e depois cinco voltaram com 429 e o corpo “error code: 1015” da Cloudflare. O firewall registrou 11 eventos no formulário naqueles minutos, todos bloqueios da regra de limite de taxa.18 A regra dispara, e a Cloudflare registra. Minha leitura de 2 de novembro é uma hipótese: dividido entre dois contadores e rodando perto do teto nominal de cada um, o scanner nunca acionou nenhum deles. É a explicação mais forte que se encaixa nos totais por minuto, não uma demonstração; os totais não mostram como os envios se espaçaram dentro de cada minuto, e a Cloudflare admite que requisições excedentes podem passar enquanto seus contadores se atualizam.548
Um limite de taxa consegue distinguir rápido de lento, para um endereço em um data center; ele não consegue dizer quem está enviando.
Token CSRF
O que verifica. O formulário deste site carrega um token oculto, inserido na página, com três partes: um carimbo de tempo, um valor aleatório e uma assinatura HMAC sobre os dois primeiros. O servidor o aceita se a assinatura confere e o carimbo de tempo tem menos de uma hora.15 O formulário do site do estúdio não tem token CSRF.25
O que barrou. Quatro envios em 30 de outubro, todos no minuto das 18:03, e nenhum em 2 de novembro.12
Por que o resto passou. O token não tem estado. Ele não está ligado a uma sessão, a um cookie nem a um endereço, então qualquer cliente que baixa a página recebe um válido, e qualquer cliente que recebe um token de outro pode apresentá-lo enquanto ele durar. Os dois scanners de fato baixaram as páginas do site; é isso que os logs de requisições mostram. O que a verificação mostra é mais restrito: todo envio dos dois dias carregava um token válido e não expirado emitido pelo meu site, porque o handler verifica o token antes de enviar.1513142416 Um token CSRF existe para impedir que outro site faça o navegador de um visitante enviar meu formulário. Ele nunca foi pensado para barrar um cliente que quer enviar o formulário por conta própria, e não barrou.
A mesma verificação tinha um segundo defeito, e ele recaía sobre pessoas, não bots. Quando o token falhava, o handler lançava um 403 em JSON que o formulário HTMX nunca exibia, então uma pessoa com um token expirado teria clicado em Enviar e não teria visto nada.12 Corrigi isso, embora minha primeira correção também não tenha funcionado; a seção 6 conta como.
Um token CSRF válido prova que o cliente tinha um token não expirado emitido pelo meu site; ele não pode provar quem preencheu o formulário, nem mesmo que esse cliente carregou a página.
Limitador de taxa da aplicação
O que verifica. O handler deste site fica atrás de um decorator que conta os envios do formulário por endereço de cliente por minuto de relógio no Cloudflare KV, com um fallback em memória, e recusa a partir de cinco com um 429.17 O site do estúdio tem um limitador em memória por processo para clientes anônimos, contado por endereço e por classificação do cliente, a classe que o limitador atribui a partir do user agent e do endereço, sobre todos os caminhos exceto health checks e arquivos estáticos.25
O que barrou. 830 envios em 30 de outubro e 31 em 2 de novembro. Nenhum no site do estúdio.127
Por que o resto passou. Por projeto, ele deixa passar cinco por minuto para qualquer um, e como a janela é o minuto de relógio, uma rajada que atravessa a virada do minuto ganha duas cotas.17 Isso eu sabia. O que eu não sabia até ler as contagens contra o código é que ele vaza, e o código tem pelo menos três jeitos de vazar. O limitador lê o contador do KV por HTTP, compara com o limite e depois grava a contagem mais um em uma requisição separada, então dois envios que leem antes de qualquer um gravar passam os dois. O incremento lê o contador uma segunda vez antes de gravar, e se essa segunda leitura falha, a gravação volta a contagem para um, então o minuto recomeça. E sempre que a leitura do KV não devolve nada ou a gravação falha, a decisão passa para uma lista guardada na memória de um único processo, que os outros processos não compartilham.17
As contagens mostram o vazamento. Em 30 de outubro, oito minutos tiveram mais de cinco envios, e o minuto das 18:06 teve 18.1 Um limitador exato de cinco por minuto teria permitido no máximo 54 envios nos minutos em que o scanner enviou, e saíram 101, 47 acima do teto. Em 2 de novembro, os minutos das 19:02 e 19:03 deixaram passar 9 e 10; um limitador exato teria permitido no máximo 22 dos 62 envios que viu, e saíram 31, 9 acima do teto.3 Elas não mostram qual caminho vazou. Minha leitura é que envios leram a mesma contagem antes de qualquer um gravar, ou que chamadas ao KV falharam sob a carga, zerando a contagem ou entregando a decisão às listas por processo, ou alguma mistura disso; os logs que tenho registram o status que a aplicação devolveu, não qual caminho o decidiu, então não conseguem separar os caminhos.173 O remédio é o mesmo para todos, e não é uma mudança de uma linha: um armazenamento de contadores com incremento atômico, algo que uma leitura do KV seguida de uma gravação separada não oferece. O vazamento é um bug meu. Ele também deixa o argumento deste post mais afiado do que eu conseguiria de propósito: um controle que diz cinco e deixa passar dezoito nunca ia ser o que decide quem pode me escrever.
No site do estúdio, o limitador da aplicação não recusou nenhum dos envios observados, e o endereço mais ativo enviou 13 em uma hora. Os contadores dele somam as requisições de um endereço a todos os outros caminhos sob a mesma classificação de cliente, e meus dados cobrem só os envios do formulário, então as contagens não medem quanta folga esses contadores tinham.725
Um limitador de taxa consegue distinguir cinco de seis quando as requisições se revezam; ele não consegue distinguir uma pessoa de um script em contagem nenhuma.
Honeypot
O que verifica. O formulário do site do estúdio tem um campo de texto chamado website que uma pessoa nunca vê: ele tem tabindex="-1" e aria-hidden, então nem o teclado nem um leitor de tela chegam a ele. Se ele volta com qualquer coisa escrita, o handler responde com a mesma mensagem de sucesso que uma pessoa veria e não grava nem envia nada.25 Esse ramo está no handler desde que construí o formulário, em 5 de junho.25 O formulário deste site não tem honeypot.24
Ned Batchelder descreveu a técnica em 2007: “These fields are called honeypots and are validated when the form data is posted. If they contain any text, then the submitter must be a bot” (esses campos se chamam honeypots e são validados quando os dados do formulário são enviados; se contiverem qualquer texto, quem enviou deve ser um bot).49 A razão que ele dá para funcionar é que os bots que preenchem formulários “cannot distinguish invisible fields from visible ones.” (não conseguem distinguir campos invisíveis de visíveis).49
O que barrou. 42 dos 52 envios nos oito dias, 80,8%, incluindo todo envio vindo de uma saída do Tor, pela hora em que chegaram, e, pela hora e pelo padrão da mensagem, todos os 13 de uma rajada de um autodenominado SQL spider.720
Por que o resto passou. Um bot que preenche só os campos visíveis nunca toca em website, e os remetentes cujos envios foram gravados, e portanto podiam chegar à minha caixa, o deixaram vazio.825 Que eles tenham escolhido os campos visíveis de propósito é minha leitura; o handler registra só que o campo oculto voltou vazio. Batchelder previu isso: “In theory, it is possible for a spambot to defeat any of these measures. But in practice, bots are very stupid” (em teoria, um spambot pode derrotar qualquer uma dessas medidas; mas na prática os bots são muito burros).49 Alguns são menos burros que outros, e os que vendem geração de leads são os menos burros de todos.
Um honeypot só prova que o cliente preencheu um campo que uma pessoa não consegue ver; um vazio não prova nada.
Até onde cada fluxo chegou: os envios dos dois scanners neste site e oito dias do gotejamento no site do estúdio.1278
O que as cinco tinham em comum
Leia as cinco frases juntas e a lacuna fica clara. Limites de taxa limitam o volume por endereço. Um token CSRF prova que um cliente tinha um token emitido pelo site. Um honeypot prova que o cliente preencheu um campo oculto. As regras da edge conhecem nomes e listas, e a que poderia perguntar se há uma pessoa do outro lado, o desafio por user agent, só pergunta às requisições cuja string de user agent está na lista dela; o desafio por pontuação de ameaça ao lado dela nunca poderia disparar. Nenhum dos scanners casou, então nada fez essa pergunta a cada envio, e os scanners não fizeram nada que um navegador não possa fazer: baixaram a página, guardaram o token dela e enviaram em um ritmo que os limites deixaram passar em parte. O guia da OWASP contra credential stuffing faz a mesma observação sobre outro formulário, aconselhando defesas que “not rely on a single predictable volume limit” (não dependam de um único limite de volume previsível).50
5. O gotejamento lento no site do estúdio
O formulário do site do estúdio nunca teve uma enxurrada. Teve um gotejamento, e o gotejamento é outro bicho. A janela aqui vai da primeira hora com um envio do formulário na consulta de oito dias, 17:00 UTC de 26 de outubro, até o momento em que salvei os dados, 13:59:50 UTC de 3 de novembro; o primeiro e o último dia são parciais.7
Envios contra linhas gravadas
O formulário recebeu 52 POSTs nessa janela e respondeu a todos com 200. A tabela de mensagens ganhou 10 linhas na mesma janela. Como o ramo do honeypot é o único caminho do handler que responde 200 e não grava nada, o honeypot pegou os outros 42, ou 80,8%.7825 Por dia UTC:78
| Data (UTC) | Envios (200) | Linhas gravadas | Pegos pelo honeypot |
|---|---|---|---|
| 26 out (parcial) | 4 | 2 | 2 |
| 27 out | 5 | 0 | 5 |
| 28 out | 3 | 2 | 1 |
| 29 out | 5 | 1 | 4 |
| 30 out | 16 | 1 | 15 |
| 31 out | 8 | 2 | 6 |
| 1º nov | 4 | 0 | 4 |
| 2 nov | 5 | 0 | 5 |
| 3 nov (parcial) | 2 | 2 | 0 |
O pico de 30 de outubro é um endereço na Grã-Bretanha que enviou 13 vezes em uma única hora com o user agent Mozilla/5.0 (compatible; SQLSpider/1.5). Pela hora e pelo padrão da mensagem, a tabela não gravou nenhum deles: a única linha gravada naquela hora é uma oferta de geração de leads, o padrão do endereço americano que também enviou naquela hora.7820
Quem estava enviando
A Cloudflare marca as requisições da rede Tor com um código de país especial, T1. A referência de cabeçalhos dela diz “T1 - Used for clients using the Tor network” (T1: usado para clientes que usam a rede Tor), e a página de regras de IP Access diz “T1: Tor exit nodes (country name: Tor)” (T1: nós de saída do Tor, com Tor como nome do país).5152 Por esse código, 23 dos 52 envios vieram do Tor, 44,2%, e 23 dos 35 endereços distintos eram saídas do Tor, 65,7%.7 Como verificação cruzada, comparei cada endereço que enviou o formulário com a lista em massa de saídas do Tor Project, que o projeto chama de “a nearly real time authoritative source for Tor exits that allow contacting your server on port 80” (uma fonte autorizada, quase em tempo real, das saídas do Tor que permitem contatar seu servidor na porta 80). Todos os 23 endereços que a Cloudflare codificou como T1 estavam na lista que baixei em 3 de novembro, e nenhum dos outros 12 estava.5354 As duas fontes concordam. Essa lista é a das saídas de 3 de novembro, porém, então confirma o código para aquele dia e não prova quais endereços eram saídas no dia em que cada envio chegou. O arquivo CollecTor do projeto guarda listas de saídas antigas e resolveria a pertinência no dia do evento; não o consultei.55
A maioria dos endereços nessa janela enviou uma única vez: 32 dos 35, 91,4%, e todas as saídas do Tor entre eles.7 Os três que enviaram mais de uma vez foram o SQL spider com 13, um endereço na Polônia com dois em uma hora e um endereço nos Estados Unidos com cinco. Os cinco envios deste último caem nas mesmas cinco horas que as cinco ofertas de geração de leads que a tabela gravou na janela, e ele mudou de user agent a cada vez: Chrome 135 no Linux, depois Firefox 128, 137, 138 e 137 de novo.720
Os user agents se dividiram pela mesma linha. Dos envios do Tor, 18 citavam um aparelho Android e 5 um Chrome de desktop, e 21 dos 23 citavam Chrome 81 ou anterior. Os envios fora do Tor citavam navegadores de desktop muito mais novos, 10 Chrome de desktop do 125 ao 135 e 6 Firefox do 128 ao 138, mais os 13 do spider com o próprio nome.7
Quem foi gravado
As duas metades se encontraram no honeypot. Nenhuma linha gravada na janela cai em uma hora que só teve envios do Tor, e todas as 10 caem em uma hora com um envio de fora do Tor. 22 dos 23 envios do Tor caíram em horas sem nenhuma linha gravada, e o 23º divide a hora com o endereço americano de geração de leads.20 Fiz a correspondência por hora porque os logs de requisições e a tabela de mensagens não compartilham nenhum ID de requisição, então a correspondência é uma inferência: pela hora em que chegaram, todo envio do Tor na janela caiu no honeypot.20 O que foi gravado, e portanto podia chegar à caixa de entrada, veio de endereços fora do Tor, que enviavam strings de navegador de desktop mais novas e deixavam o honeypot vazio.
Os números batem com o velho padrão do spam em formulários. A taxonomia de Batchelder, de 2007, divide os criadores de spam em “Playback spambots, form-filling spambots, and humans.” (spambots que reproduzem envios, spambots que preenchem formulários e humanos).49 O gotejamento do Tor se comporta como os que preenchem formulários, que preenchem todos os campos e caem no honeypot. Os remetentes que passaram se comportam como algo mais próximo de uma pessoa diante de um teclado, ou de um script feito para parecer uma. A OWASP classifica a atividade como OAT-017, Spamming: “Malicious or questionable information addition to public or private content, or messages.” (adição de informação maliciosa ou duvidosa a conteúdo público ou privado, ou a mensagens).56 A OWASP também deixa uma coisa de fora: “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” (o abuso em massa de funções quebradas de formulário para e-mail e formulário para SMS, para enviar mensagens a destinatários não pretendidos, não está incluído nesta ameaça).56 Meus formulários só escrevem para mim, então são o caso incluído, spam endereçado ao dono do site, e não aquele outro.
O gotejamento também derrota o limite de taxa por projeto. O guia da OWASP contra credential stuffing nomeia o efeito de espalhar as requisições por muitos endereços: “This may defeat both IP block-lists and rate limiting, as per IP request volume may remain relatively low” (isso pode derrotar tanto listas de bloqueio de IP quanto limites de taxa, porque o volume por IP pode continuar relativamente baixo).50 Um envio por endereço é o volume mais baixo possível.
O que dizem as mensagens gravadas
A tabela de mensagens vai até 6 de junho e tinha 108 linhas quando a li; a mais recente chegou às 07:24 UTC de 3 de novembro.8 Separei as linhas por padrão com regras fixas, a primeira correspondência vence:57
| Padrão | Tabela inteira | Outubro |
|---|---|---|
| “Hi, I wanted to know your price” (oi, eu queria saber o seu preço), um idioma por linha, um único nome de remetente | 49 | 10 |
| Ofertas de fornecedores: leads da web, crescimento no Instagram, vídeo explicativo, SEO, dados B2B, um diretório, design | 22 | 4 |
| Iscas de prêmio: jackpot, código promocional, PlayStation, Lamborghini, links de Rolls-Royce, um único nome de remetente | 16 | 3 |
| Um link de “$50,000,000 per month” (50 milhões de dólares por mês) no telegra.ph | 9 | 9 |
| Uma oferta de geração de leads: “If I were a real customer, how soon would I get a call back?” (se eu fosse um cliente de verdade, em quanto tempo receberia uma ligação de volta?) | 6 | 5 |
| Outros: duas sequências sem sentido, um pedido de lista de e-mails, um recado pedindo que eu escrevesse em outro lugar | 4 | 1 |
| Meu próprio teste do notificador por e-mail, 21 de setembro | 1 | 0 |
| Uma consulta real, pela minha leitura | 1 | 0 |
Alguns deles merecem um olhar mais atento. As nove linhas dos 50 milhões chegaram com um mesmo nome de remetente e nove endereços de remetente diferentes, cada um uma pequena variação da mesma manchete.8 A pergunta do preço aparece em 25 idiomas na tabela inteira: africâner, armênio, basco, bielorrusso, bengali, búlgaro, catalão, croata, dinamarquês, inglês, georgiano, grego, húngaro, islandês, indonésio, irlandês, italiano, latim, letão, lituano, luxemburguês, espanhol, vietnamita, galês e zulu. Só em outubro ela veio em nove: armênio, basco, dinamarquês, indonésio, latim, letão, lituano, espanhol e vietnamita.57 Duas das identificações de idioma são minhas e apertadas: a frase que li como croata pode ser bósnio, e a que li como indonésio pode ser malaio.57 Alguém está passeando um guia de conversação por um formulário de contato, um idioma por vez.
O ritmo é a outra assinatura do gotejamento. Outubro gravou 32 linhas, 1,03 por dia, em 21 dos seus 31 dias, e nunca mais de três em um dia.8 Nenhum limite de taxa por endereço dispara com uma por dia.
Quais chegaram à caixa de entrada
O formulário só envia e-mails para a caixa do estúdio desde 21 de setembro, às 17:46:47 UTC. Desde então ele gravou, e portanto enviou, 43 linhas, um limite superior porque a chamada de notificação é de melhor esforço e não verifiquei o horário do deploy; as 65 linhas gravadas antes dessa data nunca foram enviadas por e-mail.258 Toda linha na janela de evidência é spam.57
Além do meu próprio teste, uma linha da tabela inteira não é. Em 14 de setembro, um cliente de um dos produtos do estúdio escreveu sobre um erro específico, com voz de gente, e a mensagem ficou na tabela sem ninguém ler.57 Quando liguei o formulário ao e-mail uma semana depois, a mensagem do commit dizia o porquê com todas as letras: “a paying customer’s message sat unread in D1 for a week” (a mensagem de um cliente pagante ficou sem leitura no D1 por uma semana).25 Essa mensagem é a razão de o notificador existir, e é a razão pela qual não posso resolver o spam desligando o notificador. Mantenho o remetente anônimo aqui; o ponto é que uma pessoa real está na mesma tabela que o guia de conversação.
6. O que é uma verificação de bots, e o que foi para o ar
O Turnstile, nas palavras da Cloudflare
A Cloudflare chama o Turnstile de sua “smart CAPTCHA alternative” (alternativa inteligente ao CAPTCHA) e diz que ele “can be embedded into any website without sending traffic through Cloudflare and works without showing visitors a CAPTCHA.” (pode ser incorporado a qualquer site sem passar o tráfego pela Cloudflare e funciona sem mostrar um CAPTCHA aos visitantes).21 O mecanismo é a parte que responde à minha pergunta. “First, we run a series of small non-interactive JavaScript challenges to gather signals about the visitor or browser environment” (primeiro, executamos uma série de pequenos desafios de JavaScript não interativos para reunir sinais sobre o visitante ou o ambiente do navegador), diz a visão geral, 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.” (esses desafios incluem prova de trabalho, com quebra-cabeças computacionais, prova de espaço, sondagem de APIs web e vários outros desafios para detectar peculiaridades do navegador e comportamento humano).21 Um limite de taxa pergunta quantos. Um token CSRF pergunta se o cliente tem um token emitido pelo site. O Turnstile pergunta que tipo de cliente está do outro lado. Meu desafio por user agent também podia perguntar isso, mas só às requisições cuja string de user agent estava na lista dele, e meu desafio por pontuação de ameaça nunca poderia disparar; a diferença que importa aqui é a de um desafio acionado de forma seletiva contra uma verificação exigida de cada envio.
O widget tem três modos. Uso o gerenciado, que a Cloudflare descreve como um que “Automatically chooses between non-interactive or checkbox challenge based on visitor risk level. No images or text to decipher.” (escolhe automaticamente entre um desafio não interativo ou uma caixa de seleção conforme o nível de risco do visitante; sem imagens nem texto para decifrar).58 Em termos simples, o visitante vê uma verificação curta e nada mais, a menos que os sinais peçam mais, e então recebe uma caixa para marcar. O modo invisível esconde até isso, e a Cloudflare o condiciona: um site que o ativa precisa citar o Turnstile Privacy Addendum da Cloudflare na própria política de privacidade.58 O Turnstile tem um plano gratuito próprio, de até 20 widgets, com desafios ilimitados, e a Cloudflare diz que ele “can be used independently without requiring other Cloudflare services.” (pode ser usado de forma independente, sem exigir outros serviços da Cloudflare).59
A metade do servidor, que é a que conta
O widget sozinho não faz nada por um formulário, e a Cloudflare diz isso nas primeiras linhas do guia do lado do servidor: “You must call the Siteverify API to complete your Turnstile implementation. The client-side widget alone does not protect your forms.” (você precisa chamar a API Siteverify para completar sua implementação do Turnstile; o widget do lado do cliente sozinho não protege seus formulários). E depois, de forma mais direta: “Tokens can be forged. An attacker can submit any string to your form endpoint without completing a challenge.” (tokens podem ser forjados; um atacante pode enviar qualquer string ao endpoint do seu formulário sem completar um desafio).60 O servidor envia o token para https://challenges.cloudflare.com/turnstile/v0/siteverify com sua chave secreta, e pode incluir o endereço IP do visitante como remoteip.60
Duas propriedades do token moldam o código em volta dele. “Each token is valid for 300 seconds (5 minutes) after generation.” (cada token vale por 300 segundos, 5 minutos, depois de gerado). E “Tokens are single-use. Each token can only be validated once. A replayed token will be rejected with the timeout-or-duplicate error code.” (os tokens são de uso único; cada token só pode ser validado uma vez, e um token reutilizado será recusado com o código de erro timeout-or-duplicate).60 Um scanner que tivesse colhido um token não poderia enviar o formulário 1.065 vezes com ele. A Cloudflare também limita o token a 2.048 caracteres e manda o servidor “Validate the action and hostname when specified.” (validar a ação e o hostname quando especificados).60 Um alerta da mesma página: ela nomeia timeout-or-duplicate para um token expirado em um lugar e descreve invalid-input-response como “Token is invalid, malformed, or expired” (o token é inválido, malformado ou expirado) na tabela de erros, então um servidor deveria tratar qualquer falha como recusa em vez de basear sua lógica em um único código.60
O que ele processa, nas palavras da Cloudflare
A Cloudflare não afirma que o Turnstile não usa dados pessoais, e eu também não vou afirmar. O adendo de privacidade do Turnstile nomeia os sinais: “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.” (o Turnstile processa diversos sinais do lado do cliente, como o endereço IP do cliente, a impressão digital TLS, o cabeçalho User-Agent e a sitekey com a origem associada). Declara o propósito: “The purpose of collecting these Signals is not to identify, profile or target any individuals but solely to detect and block bots.” (o propósito de coletar esses sinais não é identificar, perfilar nem direcionar nenhuma pessoa, mas apenas detectar e bloquear bots). E diz que a Cloudflare também os usa para melhorar o produto: “Cloudflare is a data controller of Signals that we process to improve Turnstile’s bot detection capabilities.” (a Cloudflare é controladora dos sinais que processa para melhorar a detecção de bots do Turnstile).22 O que a Cloudflare diz, sim, deixar em paz é o conteúdo: “Turnstile does not access, store, or transmit user communications, form entries, or other page inputs.” (o Turnstile não acessa, armazena nem transmite comunicações do usuário, o que é digitado nos formulários nem outras entradas da página).21 A página de marketing acrescenta que “Turnstile never harvests data for ad retargeting.” (o Turnstile nunca coleta dados para retargeting de anúncios).61 Minha leitura: o Turnstile vê o mesmo tipo de dado de conexão que minha edge já registra para cada requisição, e nunca vê o que uma pessoa digita no meu formulário.
A CSP de que ele precisa
O Turnstile carrega a partir da origem de desafios da Cloudflare, e uma Content Security Policy precisa permitir isso. A Cloudflare lista duas diretivas: “script-src: https://challenges.cloudflare.com” e “frame-src: https://challenges.cloudflare.com”.62 Minhas duas zonas definem a política em uma regra de cabeçalhos de resposta da Cloudflare. Neste site, a lista de frames já existia e ganhou a origem de desafios. O site do estúdio não tinha nenhum frame-src, então o default-src 'self' dele teria bloqueado o iframe do widget; ele ganhou frame-src 'self' mais a origem de desafios, e a política da própria aplicação no código ganhou o mesmo.12
Por que não um desafio gerenciado no próprio POST do formulário
O atalho óbvio já estava na minha zona: uma regra personalizada que responde a um POST para o formulário com um desafio gerenciado. Minhas regras atuais fazem isso para curl e seus parentes, e funciona, porque nenhuma pessoa usa curl para me mandar uma mensagem.939 Estender isso a todo POST teria quebrado o formulário para todo mundo. A Cloudflare documenta a limitação diretamente: “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.” (as páginas de desafio interrompem o fluxo da requisição devolvendo uma página HTML completa para o navegador renderizar e resolver; esse mecanismo falha quando o navegador espera uma resposta que não é HTML, como uma requisição AJAX ou XHR).63 O formulário de contato deste site é um formulário HTMX, que envia com hx-post e troca a resposta dentro da página, então o POST dele é exatamente esse tipo de requisição; a documentação não cita o HTMX pelo nome, e a ligação é minha leitura.24 Um POST desafiado voltaria como uma página de desafio em HTML, que a Cloudflare marca com um cabeçalho cf-mitigated “set to challenge” (com o valor challenge), e “Regardless of the requested resource-type, the content-type of a challenge will be text/html.” (seja qual for o tipo de recurso pedido, o content-type de um desafio será text/html).64 O formulário trocaria uma página de desafio dentro da caixa de resposta, ou nada.
A resposta da própria Cloudflare para esse caso é outro recurso: “To ensure your API calls are protected without breaking single-page applications (SPAs) or API integrations, Cloudflare recommends using Turnstile Pre-clearance.” (para proteger suas chamadas de API sem quebrar aplicações de página única nem integrações de API, a Cloudflare recomenda usar o Turnstile Pre-clearance).63 Com a pré-liberação, um widget resolvido também emite um cookie cf_clearance que “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.” (permite aos visitantes passar pelos desafios do WAF em todas as requisições seguintes na zona, incluindo requisições de API, conforme o nível de liberação definido pelo cliente). O cookie não substitui o token: a Cloudflare diz que ele “is issued to the visitor in addition to the default Turnstile token” (é emitido ao visitante além do token padrão do Turnstile), e a mesma página continua exigindo a verificação do servidor: “You must call Siteverify to complete your Turnstile configuration.” (você precisa chamar o Siteverify para completar sua configuração do Turnstile).65 O que a pré-liberação acrescenta é um desafio na edge ao qual um POST via XHR consegue sobreviver, para um visitante cujo cookie tem uma liberação válida no nível do desafio ou acima. Eu usei o padrão: um widget no formulário e um token verificado pelo meu próprio servidor.
O que foi para o ar em 3 de novembro
Nos dois sites criei um widget em modo gerenciado pela API da Cloudflare, restrito ao domínio do próprio site. O segredo de cada widget foi da resposta da API para o chaveiro do meu Mac e para as variáveis do serviço de hospedagem no mesmo processo, e nunca para uma linha de comando nem para um arquivo.12
Este site. Um novo verificador envia o token ao siteverify com o endereço do visitante. Um token ausente, um token com mais de 2.048 caracteres, um token recusado ou uma falha de transporte devolvem false, então o formulário falha fechado sempre que o segredo está configurado; sem segredo, como no desenvolvimento local e na suíte de testes, o formulário funciona como antes.12 Um token que a Cloudflare aceita também precisa ter sido servido em um dos hostnames deste site, e quando a Cloudflare devolve a ação do widget, ela precisa ser a do próprio formulário, como aconselha o guia do lado do servidor; uma resposta sem hostname é uma recusa. Assim, um token válido gerado em outro site, ou por outro widget, não passa. Essa verificação entrou como uma segunda mudança na mesma manhã, com build a partir das 07:20 PST; às 07:25 PST, depois de limpar o cache, um envio com um token CSRF válido da página no ar e sem token do Turnstile voltou com 403 de novo. Isso prova que o formulário continua falhando fechado depois do deploy; um envio sem token não consegue exercitar a verificação de hostname em si, que se apoia no código e nos testes.6660 Até uma terceira mudança, os dois widgets e os dois verificadores também admitiam localhost, e como uma sitekey é pública, um token gerado com a minha no localhost de qualquer pessoa teria passado tanto pelo siteverify quanto pela verificação de hostname. A página de testes da Cloudflare desaconselha isso: “Cloudflare recommends that sitekeys used in production do not allow local domains” (a Cloudflare recomenda que sitekeys usadas em produção não permitam domínios locais), e cita localhost entre eles. Às 08:12 PST tirei localhost dos dois widgets, e os dois verificadores agora admitem por padrão só o site e o host www dele, então a verificação aceita os hostnames deste site e nada mais; o desenvolvimento local nomeia seus hostnames em uma configuração. Depois dessa mudança, os dois sites passaram de novo nas mesmas verificações no ar, às 08:15 e às 08:18, horário do Pacífico: o widget na página e um envio sem token recusado com 403. Nenhuma verificação no ar consegue exercitar a regra de hostname em si, porque um token ausente ou lixo falha antes de o hostname ser lido; essa regra se apoia na configuração do widget, no código e nos testes.4667 A ordem das verificações no handler agora é CSRF, depois Turnstile, depois o envio, atrás do mesmo limitador por endereço de antes. As duas recusas devolvem o próprio fragmento de erro da página com um 403, e o HTMX não troca uma resposta 4xx dentro da página por padrão, então o formulário precisa de um handler que deixe passar um 403. Minha primeira versão desse handler não funcionou. Eu a anexei ao formulário, mas o HTMX dispara o evento no elemento que recebe a resposta, e nesta página esse elemento é o irmão do formulário, então o evento nunca chegava ao handler e uma pessoa recusada continuava sem ver nada. Uma segunda revisão do rascunho deste post pegou isso lendo o HTMX empacotado contra o template. A correção, uma quarta mudança naquela manhã, põe o listener no body da página, restrito à caixa de resposta, e um teste verifica que ele é renderizado. Às 08:21 PST, depois desse deploy, rodei o formulário no Chromium do Playwright com o token do widget trocado por uma string lixo: a caixa de resposta mostrou “error: Verification failed. Reload the page and try again.” (erro: a verificação falhou; recarregue a página e tente de novo), e nada foi enviado, porque o siteverify recusa um token lixo antes de o handler chegar ao envio.68 A antiga recusa CSRF, o 403 em JSON que o HTMX nunca mostrava, não existe mais. O formulário reinicia o próprio widget depois de cada requisição HTMX, nomeando o contêiner do widget do jeito que a chamada de reinício da Cloudflare o recebe, então uma nova tentativa leva um token novo, porque cada token é de uso único e vive 300 segundos.126069 O guia do lado do cliente da Cloudflare dá a razão e a chamada: “To reset the widget if the given widget timed out or expired, you can use the function:” (para reiniciar o widget se ele excedeu o tempo ou expirou, você pode usar a função:) turnstile.reset(widgetId);.69 Minha primeira versão chamava o reinício sem argumento, o que o guia não documenta; uma quarta rodada de revisão pegou isso, a chamada agora nomeia o contêiner nos dois sites, e uma verificação no navegador em 3 de novembro enviou um token lixo em cada formulário, viu a recusa e depois encontrou um token novo no campo oculto: em até oito segundos no site do estúdio, em até vinte e cinco neste, onde o widget refez o desafio primeiro.12 A div do widget e o script só são renderizados quando a chave do site está configurada. Onze testes passam: sem segredo, envia como antes; com segredo, o handler recusa um token ausente antes de qualquer envio e recusa um token recusado, um token aceito envia, e o CSRF continua rodando primeiro; e o verificador aceita o hostname deste site e recusa outro hostname, localhost por padrão, outra ação ou nenhuma.126646
O site do estúdio. O honeypot continua na frente. Um campo oculto preenchido continua recebendo a mensagem de sucesso e nada é gravado, então os bots mais baratos nunca custam uma chamada à Cloudflare. Depois vem o Turnstile, e um token ausente, recusado ou impossível de verificar recebe um 403 com o fragmento de erro. Depois a validação, a inserção na tabela de mensagens e a notificação.12 O verificador dele confere o hostname (agora só os dois hostnames do próprio site) e a ação da mesma forma; essa mudança foi para o ar às 07:19 PST, e às 07:22 PST a página no ar tinha o widget e a chave do site, um envio sem token e um envio com token lixo voltaram com 403, e um envio ao honeypot continuou respondendo 200.66 Antes do deploy, rodei o handler localmente com as chaves de teste documentadas da Cloudflare. Com o segredo que sempre aprova, um envio sem token voltou com 403 e o token fictício passou pelo Turnstile e chegou à validação; com o segredo que sempre recusa, os dois voltaram com 403; o honeypot respondeu 200.1267 A página de testes da Cloudflare diz por que um token fictício não pode passar em produção: “Production secret keys: Only accept real tokens, reject dummy tokens.” (chaves secretas de produção só aceitam tokens reais e recusam os fictícios).67
A verificação no ar. Mais cedo naquela manhã, depois que o deploy de cada site deu certo e de uma limpeza de cache, às 06:35 PST, cada página inicial tinha uma div do widget, uma tag de script do Turnstile e a chave do site, e a política no ar de cada zona listava a origem de desafios em frame-src. Um envio ao formulário deste site com um token CSRF lixo voltou com 403, mas isso é a verificação CSRF recusando, e não prova nada sobre o Turnstile. A verificação do Turnstile em si eu testei às 06:43 PST: um envio com um token CSRF válido tirado da página no ar e sem token do Turnstile voltou com 403 e a mensagem “error: Verification failed. Reload the page and try again.”, e o mesmo envio com um token do Turnstile lixo também voltou com 403. Então o segredo está no serviço e a verificação roda depois do CSRF. No site do estúdio, um envio sem token e um envio com token lixo voltaram com 403, e um envio ao honeypot voltou com 200 sem gravar nem enviar nada.70
A verificação no navegador. Em um Chromium de verdade controlado pelo Playwright, o widget foi renderizado nas duas páginas, o script do Turnstile carregou e o campo oculto cf-turnstile-response tinha um token de 730 caracteres nas duas páginas em até seis segundos.70 Isso prova menos do que parece. A página de testes da Cloudflare avisa que “Automated testing suites (like Selenium, Cypress, or Playwright) are detected as bots by Turnstile” (o Turnstile detecta como bots as suítes de testes automatizados, como Selenium, Cypress ou Playwright), e só o siteverify decide se um token passa.67 O que a verificação prova é que o widget roda sob as duas políticas e preenche seu campo. O que não fiz é o teste que mais importa para quem lê este post: uma pessoa preenchendo o formulário e recebendo uma resposta. Esse teste envia um e-mail, então cabe a mim fazê-lo, e ainda não o fiz.
7. Para que o limite de taxa ainda serve
O Turnstile substituiu os limites de taxa como portão. Não os substituiu como cerca. Uma verificação de bots responde quem; alguma coisa ainda precisa responder quantos, porque cada envio que chega à verificação com um token não vazio de tamanho válido agora custa uma chamada à API siteverify da Cloudflare, e cada envio que passa ainda custa um envio de e-mail.12 O próprio guia do lado do servidor da Cloudflare lista o limite de taxa entre suas recomendações para a chamada ao siteverify em si: “Rate limit to protect against validation flooding.” (aplique limite de taxa para se proteger contra enxurradas de validação).60 Meus dois limites ficam: a regra da edge de 3 envios a cada 10 segundos por endereço por data center, e o limitador da aplicação de cinco envios por minuto por endereço.917
A regra da edge merece uma descrição honesta agora que sei como ela conta. A Cloudflare expõe o projeto sem rodeios: “Cloudflare tracks request rates by maintaining separate counters for each unique combination of values in a rule’s characteristics” (a Cloudflare acompanha as taxas de requisição mantendo contadores separados para cada combinação única de valores das características de uma regra), e “Cloudflare does not support global rate limiting counters across the entire network.” (a Cloudflare não oferece contadores globais de limite de taxa na rede inteira).6 Um endereço que chega à minha zona por dois data centers recebe duas cotas, e os envios do scanner de 2 de novembro de fato chegaram por dois; essa é minha explicação mais forte para a regra ter ficado em silêncio, coerente com os totais por minuto mas não provada por eles.5 Some o alerta de precisão da Cloudflare, de que as regras “are not designed to allow a precise number of requests to reach your origin server” (não foram projetadas para deixar chegar um número preciso de requisições ao servidor de origem), e a melhor descrição da regra é um orçamento nominal, não um teto comprovado: 3 envios do formulário a cada 10 segundos por endereço por data center, o equivalente nominal de 18 por minuto, aplicado de forma aproximada.489 No plano Free ela é também a única regra de limite de taxa que tenho, com um período fixo de 10 segundos e contagem só por IP.48
Um detalhe do plano Free importa se você ficou tentado, como eu fiquei, a fazer do próprio limite de taxa o desafio. A página de parâmetros da Cloudflare diz que nos planos Free, Pro e Business uma regra de limite de taxa com ação de desafio sempre faz throttling em vez de manter uma duração, e que “When visitors pass a challenge, their corresponding request counter is set to zero.” (quando os visitantes passam por um desafio, o contador de requisições deles é zerado).71 Um desafio no POST via XHR do formulário também falharia pelo motivo da seção 6.63 Então a regra da edge continua sendo um bloqueio, e o desafio mora no widget.
O limitador da aplicação precisa da correção que o vazamento dele pede: um armazenamento de contadores com incremento atômico, para que dois envios não possam ler a mesma contagem, e um caminho de falha que não divida a contagem entre processos. O código atual, uma leitura do KV seguida de uma gravação separada com um fallback por processo, não oferece nenhuma das duas coisas, então isso é uma troca de armazenamento e não um patch de uma linha.173 Com o Turnstile como condição adicional para cada envio que poderia chegar à caixa de entrada, um envio que o vazamento deixe passar ainda precisa carregar um token que a Cloudflare aceite, então o vazamento custa volume e dinheiro, e a decisão sobre quem pode enviar não depende mais da contagem. Um limitador é um orçamento. Um orçamento que erra por 47 em uma enxurrada é um orçamento ruim, mas nunca foi um guarda.
O guia de autenticação da OWASP coloca a mesma estratificação nos próprios termos. Ele trata um CAPTCHA “as a defense-in-depth control to make brute-force attacks more time-consuming and expensive, rather than as a preventative.” (como um controle de defesa em profundidade para tornar ataques de força bruta mais demorados e caros, e não como medida preventiva).72 E o guia contra credential stuffing observa que “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” (a maioria das ferramentas usadas nesses ataques faz requisições POST diretas ao servidor e lê as respostas, mas não baixa nem executa JavaScript).50 Meus dois scanners de fato baixaram as páginas do site, o caminho mais provável pelo qual chegaram a ter tokens CSRF válidos. Um cliente que envia o formulário sem rodar o widget não tem token do Turnstile para enviar, e os dois formulários agora recusam um envio sem ele: neste site, um envio com um token CSRF válido e sem token do Turnstile voltou com 403, e o mesmo aconteceu com um envio sem token no site do estúdio.70
O checklist
Para um formulário de contato atrás da Cloudflare, na ordem em que eu faria hoje:
- Coloque uma verificação de bots no formulário e verifique cada token no servidor. Nas palavras da própria Cloudflare: “The client-side widget alone does not protect your forms.” (o widget do lado do cliente sozinho não protege seus formulários).60 Confira também o hostname e a ação na resposta, como a mesma página aconselha, para que um token gerado em outro lugar não passe, e deixe os domínios locais fora da sitekey de produção, porque “Cloudflare recommends that sitekeys used in production do not allow local domains”.606667
- Falhe fechado. Um token ausente, grande demais, recusado ou impossível de verificar é uma recusa, porque “Tokens can be forged.” (tokens podem ser forjados).6012
- Reinicie o widget depois de cada envio. Os tokens são de uso único e valem por 300 segundos, então uma nova tentativa sem reinício é uma nova tentativa com um token gasto.6069
- Libere a origem de desafios nas duas diretivas da CSP. A Cloudflare pede
script-srceframe-srcparahttps://challenges.cloudflare.com, e uma política só comdefault-src 'self'bloqueia o iframe.6212 - Não coloque um desafio gerenciado em um formulário que envia via XHR ou fetch. A Cloudflare diz que a página de desafio “fails when the browser expects a non-HTML response” (falha quando o navegador espera uma resposta que não é HTML), e um envio HTMX é esse tipo de requisição.63
- Devolva um erro que uma pessoa consiga ver. Minha recusa CSRF era um 403 em JSON que o formulário HTMX nunca exibia, então uma pessoa com um token expirado teria clicado em Enviar e não teria visto nada, e minha primeira correção escutava no formulário enquanto o HTMX dispara o evento de troca no alvo da resposta, o irmão do formulário; então teste a recusa em um navegador.1268
- Mantenha o honeypot na frente da verificação de bots. Ele pegou 42 de 52 envios no site do meu estúdio ao custo de um campo oculto e um
if.725 - Mantenha o limite de taxa, e dimensione-o como teto de custo, não como portão. A Cloudflare conta por endereço e por data center, e o scanner de 2 de novembro chegou por dois, o motivo mais provável de a regra nunca ter disparado, embora os totais por minuto não possam prová-lo.65
- Apoie o limitador da aplicação em um armazenamento que incremente de forma atômica. O meu lia a contagem e a gravava de volta em uma requisição separada, voltava a contagem para um quando a segunda leitura falhava, recorria à memória de um único processo quando o KV falhava, e deixou passar 47 e 9 envios acima do próprio teto; os logs não dizem qual caminho vazou.173
- Não confie em uma regra de user agent para barrar um scanner. Os dois scanners enviaram uma string comum do Chrome, pelo menos 69.322 vezes em 30 de outubro e 9.725 vezes em 2 de novembro.3241
- Conte os e-mails no provedor de e-mail, não no formulário. Meu handler responde 200 nos caminhos de falha, e o evento
email.sentdo Resend só significa que “the API request was successful” (a requisição à API deu certo); o campolast_eventde um e-mail consultado diz o que aconteceu depois.242873 - Consulte as análises dentro de um mês. Os conjuntos de dados adaptativos da Cloudflare guardam “at least 31 days of data on every plan” (pelo menos 31 dias de dados em todos os planos).27
- Conte de novo depois que a janela fechar. Minhas anotações escritas minutos depois da enxurrada de 30 de outubro contaram a menos os envios do formulário, porque a Cloudflare ainda não tinha terminado de ingerir os últimos minutos.16
- Trate um bloqueio de endereço como limpeza. O meu veio cerca de 50 minutos depois do primeiro envio do formulário em 30 de outubro e 8 minutos depois do início da varredura em 2 de novembro, e nenhum envio de nenhum dos dois dias veio depois do minuto do bloqueio: 101 antes dele em 30 de outubro, e 26 antes e 5 dentro dele em 2 de novembro.1192
O que poderia mudar
São condições, não previsões. Cada uma diz o que teria de acontecer e o que eu faria a respeito.
Se um scanner começar a passar pelos desafios gerenciados. As verificações do Turnstile rodam no navegador e procuram automação; uma ferramenta que rode um navegador completo bem o bastante para passar por elas, em volume, transformaria o widget em mais um custo para ela, e não em um muro. O sinal seriam envios do formulário com tokens que o siteverify aceita, vindos de endereços que também enviam strings de injeção no cabeçalho User-Agent. Batchelder escreveu a versão honesta disso para os honeypots em 2007: “If and when they do, we can switch back to CAPTCHAs.” (se e quando fizerem isso, podemos voltar aos CAPTCHAs).49 Meu plano B seria o mesmo em espírito, um desafio mais forte, e enquanto isso o honeypot e os limites de taxa continuam de pé.
Se eu adicionar uma verificação na edge com pré-liberação. A Cloudflare recomenda a pré-liberação do Turnstile para requisições que não conseguem renderizar uma página de desafio, e um cookie cf_clearance permite que um visitante “bypass WAF Challenges on all subsequent requests on the zone, including API requests” (passe pelos desafios do WAF em todas as requisições seguintes na zona, incluindo requisições de API).6563 Se eu adotar isso, a edge poderia desafiar o próprio POST do formulário sem quebrar o HTMX para um visitante cujo cookie tenha uma liberação válida no nível do desafio ou acima, e a página de CSP da Cloudflare acrescenta um requisito para esse modo: “your connect-src directive must include ‘self’.” (sua diretiva connect-src precisa incluir ‘self’).62 O cookie vem “in addition to the default Turnstile token” (além do token padrão do Turnstile), e o siteverify continua obrigatório, então o desafio na edge seria uma segunda verificação na frente da do meu servidor, não uma substituta.65 Não fiz isso, e o post descreve o desenho de widget e siteverify que está no ar.
Se eu exportar o log do Resend para as duas janelas. Toda contagem de e-mails deste post é um teto, porque um 200 do formulário é um envio, ou uma falha à qual meu handler também responde com 200.24 O endpoint de consulta do Resend devolve um last_event para cada e-mail, como delivered, e a lista de eventos dele distingue “sent - The email was sent successfully.” (enviado: o e-mail foi enviado com sucesso) de “delivered - Resend successfully delivered the email to the recipient’s mail server.” (entregue: o Resend entregou o e-mail ao servidor de e-mail do destinatário).7374 Com essa exportação, 101 e 31 virariam contagens exatas, e só poderiam diminuir.
Se uma pessoa enviar o spam à mão. O Turnstile pergunta se há um navegador e, às vezes, uma pessoa. Não pergunta por quê. Uma pessoa paga para colar uma oferta em formulários de contato consegue passar, assim como os remetentes de geração de leads do site do meu estúdio, se houver uma pessoa por trás deles. Para esse caso, as defesas são as antigas: um limite de taxa para o volume e meu próprio julgamento na caixa de entrada.
Se o gotejamento do Tor precisar de uma resposta dia a dia. A lista de saídas que conferi é a que baixei em 3 de novembro, e o serviço CollecTor do Tor Project guarda listas antigas.5355 Se eu consultá-lo para os oito dias, a atribuição ao Tor passaria a se apoiar nas listas dos dias em que os envios chegaram, e não no código da Cloudflare mais a lista de 3 de novembro. A Cloudflare também oferece Onion Routing em todos os planos, que atende usuários do Tor diretamente pela rede Tor; a Cloudflare diz que com ele “Human Tor users and bots can be distinguished by our Onion services, such that interactive challenges are only served to malicious bot traffic.” (nossos serviços Onion conseguem distinguir usuários humanos do Tor de bots, de modo que desafios interativos só são servidos ao tráfego de bots maliciosos).75 Não o ativei, e eu ia querer medir o tráfego do Tor no site do estúdio depois do Turnstile antes de fazer isso.
Principais conclusões
Se você mantém um formulário de contato:
- Um token CSRF e um limite de taxa juntos ainda deixam um scanner escrever para você. Meus dois scanners tinham tokens válidos, e meu limitador deixou passar 101 e 31 envios.12
- Verifique no servidor o token de uma verificação de bots, falhe fechado e reinicie o widget depois de cada requisição; o widget sozinho “does not protect your forms.” (não protege seus formulários).60
- Teste seu limitador sob carga e sob falha, não uma requisição de cada vez. Um contador de ler e depois gravar com fallback por processo deixou passar 18 envios em um minuto configurado para permitir cinco.117
- Mantenha o honeypot. É o filtro mais barato da pilha, e ele pegou 80,8% desses envios no site do meu estúdio, em oito dias, antes de o handler checar um endereço, gravar uma linha ou enviar um e-mail.725
Se você cuida da edge do site:
- Saiba o que seu limite de taxa conta. Na Cloudflare toda regra conta por data center, e um scanner que chega por dois recebe dois contadores.65
- Teste suas próprias regras e leia os eventos de firewall depois. Onze eventos de firewall do meu teste me disseram mais sobre a regra do que um dia raciocinando sobre por que ela tinha ficado em silêncio.18
- Não desafie um POST via XHR ou fetch com uma página de desafio; a Cloudflare documenta que isso falha e recomenda a pré-liberação para esse caso.63
- Espere que regras de user agent peguem só ferramentas honestas. Os scanners daqui enviaram uma string de navegador; meu teste com curl provocou desafios.3239
Se é você quem lê a caixa de entrada:
- Quando a caixa encher, confira o remetente antes do assunto. Três dos meus fluxos de e-mail compartilham um endereço de remetente, e por isso meu primeiro palpite estava errado.23
- Trate os 200 de um formulário como um teto de e-mails, não como uma contagem, até o log do provedor de e-mail dizer outra coisa.2428
- Leia as mensagens gravadas antes de desligar o notificador. A mensagem de um cliente real ficou sem leitura por uma semana uma vez, e essa mensagem é o motivo de o formulário do estúdio me mandar e-mail.25
Perguntas frequentes
Por que meu formulário de contato recebe spam mesmo com a Cloudflare?
O mais provável é que nada na frente do formulário pergunte a cada envio se foi uma pessoa que o mandou. As regras personalizadas da Cloudflare comparam endereços, redes, caminhos e strings de user agent, e os limites de taxa dela contam requisições por endereço por data center; um scanner que envia um user agent de navegador, carrega a página primeiro e envia em ritmo moderado passa por todos eles. No meu site, dois scanners fizeram exatamente isso e emplacaram 101 e 31 envios, e o spam do site do meu estúdio chegou quase sempre a um envio por endereço nos oito dias que medi, algo que nenhuma contagem por endereço consegue pegar.96127
Um limite de taxa barra bots?
Um limite de taxa limita a velocidade de um endereço; ele não distingue um bot de uma pessoa. A própria documentação da Cloudflare diz que as regras dela “are not designed to allow a precise number of requests to reach your origin server” (não foram projetadas para deixar chegar um número preciso de requisições ao servidor de origem) e que os contadores não são compartilhados entre data centers. Minha regra da edge nunca disparou com nenhum dos dois scanners, e o limitador da minha aplicação, configurado para cinco por minuto, deixou passar 18 em um minuto: o contador dele é uma leitura seguida de uma gravação separada, uma segunda leitura que falha o volta para um, e um fallback por processo assume quando o armazenamento falha; qualquer um desses caminhos pode deixar passar envios a mais.48641173
Um campo honeypot basta?
Basta para os bots que preenchem o campo oculto, e no site do meu estúdio isso foi 80,8% desses envios: o meu pegou 42 de 52 envios em oito dias, incluindo, pela hora em que chegaram, todo envio de uma saída do Tor. Ele não faz nada contra um remetente que deixa o campo oculto vazio, e os 10 que ele deixou passar, gravados e portanto no máximo 10 e-mails, fizeram exatamente isso.720 Ned Batchelder, ao descrever a técnica em 2007, disse isso mesmo: “In theory, it is possible for a spambot to defeat any of these measures.” (em teoria, um spambot pode derrotar qualquer uma dessas medidas).49
Turnstile ou CAPTCHA: qual é a diferença?
Um CAPTCHA clássico pede ao visitante que resolva um quebra-cabeça. O Turnstile executa “a series of small non-interactive JavaScript challenges to gather signals about the visitor or browser environment” (uma série de pequenos desafios de JavaScript não interativos para reunir sinais sobre o visitante ou o ambiente do navegador), e no modo gerenciado só mostra uma caixa de seleção quando o risco do visitante pede, com “No images or text to decipher.” (sem imagens nem texto para decifrar).2158 A Cloudflare diz que seus desafios não usam “CAPTCHA puzzles or visual tests like selecting objects or typing distorted characters.” (quebra-cabeças CAPTCHA nem testes visuais como selecionar objetos ou digitar caracteres distorcidos).76 Os dois ainda precisam que o servidor verifique o resultado; no caso do Turnstile, essa é a chamada ao siteverify.60
Por que a proteção CSRF não barra o spam?
Uma defesa CSRF bem implementada serve para impedir que outro site faça o navegador de um visitante enviar o seu formulário. Meu token é mais fraco que isso: ele não tem estado e é assinado, vale por uma hora e não está ligado a uma sessão, a um cookie nem a um endereço, então só prova que o cliente tem um token emitido pelo meu site na última hora, não de onde veio a requisição. Nenhum dos dois tipos barra um cliente que carrega sua página e envia o formulário por conta própria, porque carregar a página é justamente como um cliente obtém um token válido, e os dois scanners juntaram tokens válidos; a verificação recusou quatro dos envios deles nas duas enxurradas.1512
Como distinguir um scanner de uma pessoa nos logs?
Olhe tudo o que o endereço fez, não só o formulário. Meus dois scanners rastrearam milhares de caminhos, puseram strings de injeção de SQL nos caminhos e no cabeçalho User-Agent, enviaram um user agent do Chrome na maioria das requisições e uma alegação de ser Googlebot em algumas que a regra de bots verificados da Cloudflare não aceitou, e enviaram o formulário em rajadas de dezenas a centenas por minuto.31324441 Payloads baseados em tempo como sleep(15) e waitfor delay '0:0:15' pertencem a famílias de sondas documentadas.3377 Uma pessoa envia uma mensagem, a partir de uma página que de fato leu, e não pede ao seu servidor que durma.
O Turnstile coleta dados pessoais?
A Cloudflare não diz que ele não coleta nenhum. O adendo de privacidade do Turnstile nomeia os sinais que ele processa, “such as client IP address, TLS Fingerprint, User-Agent Header and Sitekey and associated origin” (como o endereço IP do cliente, a impressão digital TLS, o cabeçalho User-Agent e a sitekey com a origem associada), diz que o propósito é “solely to detect and block bots” (apenas detectar e bloquear bots) e diz que a Cloudflare é controladora desses sinais para melhorar a detecção de bots. A visão geral diz que o Turnstile “does not access, store, or transmit user communications, form entries, or other page inputs.” (não acessa, armazena nem transmite comunicações do usuário, o que é digitado nos formulários nem outras entradas da página).2221 Então ele vê dados de conexão, não o que uma pessoa digita.
Relacionados neste site. O modo auto do Claude Code não é uma fronteira de segurança faz o mesmo argumento sobre outro controle: um classificador que julga cada ação não é o muro. Agents.txt não é controle de acesso traça a linha entre declarar uma política aos bots e fazê-la valer. Fadiga de alertas: 82 e-mails em um dia do meu monitor de segurança trata do scanner de 2 de novembro do lado do meu monitor: um bloqueio oito segundos depois do primeiro achado, oito minutos depois do início da varredura, e zero e-mails sobre ele. O que eu rodo antes de dormir é outro tipo de rotina de operações, uma checagem noturna de 15.000 páginas quanto a status, tempo de resposta e cache, e O que realmente quebra quando você roda agentes de IA sem supervisão cataloga os modos de falha das sessões de agentes sem supervisão, o tipo de automação que opera minha infraestrutura.
Fontes
Notas do autor sobre os dados: toda contagem dos meus próprios sistemas vem da GraphQL Analytics API da Cloudflare para as duas zonas (os conjuntos de dados de requisições e de eventos de firewall), da tabela de mensagens do site do estúdio, do log do meu monitor e do código dos dois formulários, tudo salvo em 3 de novembro de 2026 e transformado em um registro por script. Os horários estão em UTC, salvo quando marcados como PDT ou PST. Nenhum endereço, identificador de regra ou de lista, chave ou caminho do sistema de arquivos dessa evidência aparece neste post.
-
Dados do autor. Análises de requisições da Cloudflare para a zona deste site, os POSTs do scanner de 30 de outubro ao formulário de contato por minuto e por status na edge, consultadas em 3 de novembro de 2026 sobre a janela fechada: 1.065 envios, dos quais 101 responderam 200, 830 responderam 429, 130 responderam 422 e 4 responderam 403, todos chegando à aplicação; primeiro envio e primeiro 200 no minuto das 18:00, pico de 208 envios no minuto das 18:03, o maior número de 200 em um minuto 18 (18:06), oito minutos com mais de cinco 200, o último 200 às 18:36 e o último envio a chegar à aplicação às 18:37. Derivado no registro: 101 envios antes do bloqueio, o primeiro envio cerca de 46 minutos antes de eu relatar a enxurrada e 49,88 minutos antes do bloqueio, o último envio a chegar à aplicação 11,88 minutos antes do bloqueio. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
Dados do autor. Análises de requisições da Cloudflare para a zona deste site, os POSTs do scanner de 2 de novembro ao formulário de contato por minuto e por status na edge, consultadas em 3 de novembro de 2026: 500 envios, dos quais 31 responderam 200, 31 responderam 429 e 438 responderam 403 no minuto do bloqueio ou depois; 62 chegaram à aplicação; primeiro envio no minuto das 18:58, pico de 32 envios chegando à aplicação no minuto das 19:02, o maior número de 200 em um minuto 10 (19:03), último 200 no minuto das 19:04; uma média de 8,86 envios por minuto no intervalo de sete minutos entre o primeiro envio a chegar à aplicação, no minuto das 18:58, e o último, no minuto das 19:04; dos 31 envios, 26 nos minutos anteriores ao minuto do bloqueio e 5 no próprio minuto do bloqueio, 19:04, o minuto em que o endereço entrou na lista às 19:04:01, que os intervalos por minuto não conseguem ordenar em relação a esse segundo; nenhum depois. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
Análise do autor no registro: para cada minuto, o menor entre cinco e os envios que o limitador da aplicação viu (os 200, os 429 e os 403 de CSRF da aplicação; não os 422, que o FastAPI recusa antes de o limitador rodar), somado sobre os minutos. 30 de outubro: no máximo 54 envios para um limitador exato contra 101 medidos, 47 acima, de 935 envios que o limitador viu. 2 de novembro: no máximo 22 contra 31, 9 acima, de 62. Um teto contrafactual, não uma medição; os envios medidos são as contagens do formulário acima. ↩↩↩↩↩↩↩
-
Dados do autor. Eventos de firewall da Cloudflare para o endereço de 2 de novembro, consultados em 3 de novembro de 2026 (um conjunto de dados amostrado, usado para saber qual regra agiu e quando, nunca para volumes): o primeiro evento às 18:57:11 da regra de caminhos de exploit, o primeiro evento da regra da lista de bloqueio às 19:04:03; toda linha um bloqueio, nenhuma um desafio gerenciado; 310 linhas no formulário de contato, todas bloqueios, a primeira às 19:04:05; e nenhum evento da regra de limite de taxa para o endereço. ↩↩↩↩↩↩↩↩↩↩
-
Dados do autor, anexo do registro. Análises de requisições da Cloudflare para os POSTs dos dois scanners ao formulário agrupados por data center da Cloudflare, minuto e status, consultadas em 3 de novembro de 2026: em 2 de novembro, das 18:56 às 19:05:00, o fim da consulta, então a contagem inclui só o primeiro envio do minuto das 19:05, 44 envios por Amsterdã e 41 por Londres; no minuto das 19:02, 18 por Londres (4 enviados, 14 recusados pela aplicação) e 14 por Amsterdã (5 enviados, 9 recusados); a maior contagem em um minuto em um único data center, 19, foi a de Amsterdã no minuto das 19:04, 15 deles recusados na edge depois do bloqueio. Em 30 de outubro, todos os 1.065 envios vieram por Paris. ↩↩↩↩↩↩↩↩
-
Cloudflare, “How Cloudflare determines the request rate”, documentação do WAF da Cloudflare, acessada em 3 de novembro de 2026, 14:08:57 UTC. Contadores separados por combinação de características; contadores não compartilhados entre data centers, exceto os da mesma localização geográfica;
cf.colo.idcomo característica obrigatória; sem contadores globais. ↩↩↩↩↩↩↩ -
Dados do autor. Análises de requisições da Cloudflare para a zona do site do estúdio, POSTs ao formulário de contato por hora, endereço, país e user agent, das 17:00 UTC de 26 de outubro às 13:59:50 UTC de 3 de novembro de 2026: 52 envios, todos respondidos com 200, de 35 endereços; 23 envios e 23 endereços com o código de país do Tor da Cloudflare (44,2% e 65,7%); 32 endereços com exatamente um envio (91,4%), todos os endereços do Tor entre eles; os três endereços repetidos com 13 (Grã-Bretanha, uma hora, o user agent SQLSpider), 2 (Polônia, uma hora) e 5 (Estados Unidos, um user agent novo a cada vez); user agents do Tor: 18 Android e 5 Chrome de desktop, 21 citando Chrome 81 ou anterior; user agents fora do Tor: 10 Chrome de desktop do 125 ao 135, 6 Firefox do 128 ao 138 e 13 outros. O firewall da zona não registrou nenhum evento sobre um POST ao formulário na janela. As quedas no honeypot são os envios menos as linhas gravadas: 42, ou 80,8%. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
Dados do autor. A tabela de mensagens de contato do site do estúdio, lida em 3 de novembro de 2026 às 14:04 UTC: 108 linhas de 6 de junho às 07:24 UTC de 3 de novembro, 10 delas na janela posterior à consulta; por dia como na tabela da seção 5; 32 linhas em outubro em 21 de 31 dias, 1,03 por dia, nunca mais de três em um dia; as nove linhas de 50.000.000 de dólares com um mesmo nome de remetente e nove endereços de remetente; 43 linhas desde o commit do notificador e 65 antes. ↩↩↩↩↩↩↩↩↩↩↩
-
Dados do autor. A configuração de zona dos dois sites, exportada pela API da Cloudflare em 3 de novembro de 2026: a regra de limite de taxa deste site (versão 1, última atualização 2026-10-30 às 18:49:53 UTC; expressão como citada; características data center e endereço do cliente; 3 requisições por período de 10 segundos; tempo de mitigação de 10 segundos, o equivalente nominal de 18 por minuto por endereço por data center) e suas cinco regras personalizadas (um bloqueio por lista da conta e sete ASNs, um bloqueio de caminhos de exploit, uma isenção para bots verificados e dois desafios gerenciados no formulário de contato e nos caminhos de administração, por string de user agent e por pontuação de ameaça, ambos ativos na exportação e o de pontuação de ameaça desativado mais tarde naquele dia); as regras do mesmo tipo do site do estúdio, com o desafio no site inteiro e um bloqueio nos caminhos de API que isenta o formulário, e uma regra de limite de taxa para o site inteiro contada por data center e endereço sobre as requisições de clientes que não são bots verificados. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
Dados do autor. Eventos de firewall da Cloudflare para o endereço de 30 de outubro, consultados em 3 de novembro de 2026 (3.932 linhas amostradas, abaixo do teto da consulta, usadas só para atribuição e horários): o primeiro evento às 17:59:53 da regra de caminhos de exploit, que agiu desde o primeiro minuto da varredura; cada um dos 111 eventos amostrados antes das 18:49:53 dessa regra; o primeiro evento da regra da lista de bloqueio às 18:49:53; nenhum evento da regra de limite de taxa e nenhum evento no formulário de contato para o endereço. ↩↩↩↩↩↩↩↩↩↩↩
-
Cloudflare, “cf.threat_score”, referência de campos do Ruleset Engine, acessada em 3 de novembro de 2026, 16:12:44 UTC. A descrição do campo: uma pontuação que antes ia de 0 a 100, e “Now, the threat score is always 0 (zero).” ↩↩↩
-
Registro do autor do que foi para o ar em 3 de novembro de 2026, escrito a partir da saída das ferramentas da sessão: os dois widgets (modo gerenciado, criados pela API da Cloudflare por volta das 14:13 UTC), as mudanças de CSP nas duas zonas, o verificador deste site e a ordem do handler (falha fechado, CSRF, depois Turnstile, depois o envio, fragmentos de erro com 403, reinício do widget depois de cada requisição HTMX), seus dez testes novos (10 aprovados; um décimo primeiro veio com a verificação do nome de host), a ordem do handler do site do estúdio (honeypot, depois Turnstile, depois validação, gravação e notificação) e as verificações locais com as chaves de teste da Cloudflare; nos dois sites o verificador recusa um token ausente, ou um com mais de 2.048 caracteres, sem chamar o siteverify (lido do código). Os dois sites foram para o ar entre 14:32 e 14:36 UTC. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
Dados do autor. Análises de requisições da Cloudflare para o endereço de 30 de outubro, por minuto e em uma varredura de todas as zonas da minha conta, consultadas em 3 de novembro de 2026: 76.429 requisições a este site do minuto das 17:59 ao das 18:51 (53 intervalos), o mesmo total nas duas consultas; 69.484 antes do minuto do bloqueio com média de 1.389,7 por minuto, pico de 3.219 às 18:36, 5.234 depois do minuto do bloqueio sem nenhuma servida; por resultado, 49.513 servidas (64,8%), 18.990 recusadas pela aplicação (24,8%, incluindo 10.019 GET 418 da barreira de agentes do site), 2.339 erros 520 de origem (3,1%), 5.587 bloqueadas na edge (7,3%); 50,88 minutos do primeiro minuto da varredura até o bloqueio; 387 GETs para kiradex.app, todos respondidos com 200, e nenhuma outra zona tocada. ↩↩↩↩↩↩↩↩↩↩
-
Dados do autor. Análises de requisições da Cloudflare para o endereço de 2 de novembro, por minuto, por caminho, por user agent e em uma varredura de todas as zonas, consultadas em 3 de novembro de 2026: dois GETs às 07:49 UTC, 11,1 horas antes da varredura; 10.907 requisições do minuto das 18:56 ao das 19:17 (22 intervalos), 10.909 com os GETs iniciais, o mesmo total em quatro consultas; 29 requisições no primeiro minuto, 637 no minuto mais movimentado antes do bloqueio (18:57), 1.107 no minuto mais movimentado (19:07), 300 no último; 2.986 antes do minuto do bloqueio (média de 373,2 por minuto), 7.445 depois sem nenhuma servida; por resultado, 1.388 servidas (12,7%), 1.473 recusadas pela aplicação (13,5%, incluindo 420 GET 418), 116 erros 520 de origem (1,1%), 7.930 bloqueadas na edge (72,7%); 8,02 minutos do primeiro minuto da varredura até o bloqueio; 368 GETs para kiradex.app e nenhuma outra zona tocada. ↩↩↩↩↩↩↩↩↩↩↩
-
Leitura de código do autor. O módulo CSRF deste site como estava em produção em 30 de outubro e 2 de novembro: um token de carimbo de tempo, valor aleatório e assinatura HMAC-SHA256 sobre os dois; válido se a assinatura confere e o carimbo de tempo tem menos de 3.600 segundos; não ligado a sessão, cookie nem endereço. Lido do código-fonte em 3 de novembro de 2026. ↩↩↩↩
-
Dados do autor. O registro construído por script a partir dos resultados salvos das consultas (484 linhas, cada uma com sua fonte e método) e seu livro de códigos, 3 de novembro de 2026, incluindo as regras sobre qual camada respondeu cada status, a regra de que eventos de firewall dão atribuição e carimbos de tempo e os conjuntos de dados de requisições dão contagens, a regra de que só um 200 prova um token CSRF válido, e a tabela de onde minhas anotações do dia diferem das consultas de 3 de novembro (as anotações de 30 de outubro foram escritas às 11:58 PDT, antes de a Cloudflare ingerir os últimos minutos, e contam a menos os envios do formulário, os 422, os envios ao compartilhamento do app de pintura e as requisições da hora). ↩↩↩↩↩↩↩↩
-
Leitura de código do autor. O limitador de taxa do formulário deste site como estava em produção em 30 de outubro e 2 de novembro: um padrão de cinco envios do formulário por endereço de cliente por janela, uma janela de 60 segundos baseada no minuto de relógio, uma leitura do contador no KV, uma segunda leitura dentro do incremento que conta uma leitura com falha como zero, e uma gravação separada no KV dessa contagem mais um, um 429 quando a contagem lida está no limite, e um fallback para uma lista em memória por processo quando a chamada ao KV falha. Lido do código-fonte em 3 de novembro de 2026. ↩↩↩↩↩↩↩↩↩↩
-
Dados do autor, anexo do registro. Meu teste da regra de limite de taxa deste site em 3 de novembro de 2026 a partir da minha própria máquina: às 14:36:49 UTC, dez POSTs sem token com 0,4 segundo de intervalo (quatro 422, depois seis 429); às 14:38:03 UTC, oito com todos os campos presentes (três 403 da origem, depois cinco 429 com o corpo “error code: 1015”); eventos de firewall da Cloudflare para o formulário das 14:35 às 14:40 UTC, consultados às 14:40:36 UTC: 11 eventos, todos bloqueios da regra de limite de taxa. ↩↩↩↩
-
Dados do autor. O log do meu monitor de 2 de novembro de 2026, das 10:40 às 11:32 PST, e seus registros de achados e bloqueios, salvos em 3 de novembro: uma execução iniciada às 10:57:00 PST com uma janela de detector terminando às 18:57:22 UTC e sem achado, com entre 29 e 666 requisições do endereço nessa janela pelos dados por minuto; a execução seguinte às 11:02:46 PST; o primeiro achado às 11:03:53 PST (uma enxurrada de requisições crítica, 1.914 requisições contadas para o endereço), 7,88 minutos depois do primeiro minuto da varredura; o endereço adicionado à lista de bloqueio da conta às 11:04:01 PST; seis achados no total, todos incorporados ao resumo diário e nenhum enviado por e-mail. O monitor não estava rodando em 30 de outubro; eu o reativei em 31 de outubro, e o detector de tráfego dele veio depois. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
Análise do autor. Correspondência por intervalos de uma hora entre as linhas gravadas do site do estúdio e os dados horários de POST, 3 de novembro de 2026: nenhuma linha gravada em uma hora só com envios do Tor; todas as 10 em horas com um envio de fora do Tor; 22 de 23 envios do Tor em horas sem nenhuma linha gravada, o 23º em uma hora compartilhada com o endereço americano de geração de leads; as cinco linhas de geração de leads da janela nas cinco horas em que esse endereço enviou. Uma inferência por hora, não um join por ID de requisição. ↩↩↩↩↩↩↩
-
Cloudflare, “Overview”, visão geral da documentação do Turnstile, acessada em 3 de novembro de 2026, 14:08:57 UTC. “Cloudflare’s smart CAPTCHA alternative”; incorporação sem tráfego pela Cloudflare; os desafios de JavaScript não interativos e a descrição da prova de trabalho e da prova de espaço; a afirmação de que o Turnstile não acessa, armazena nem transmite o que é digitado nos formulários. ↩↩↩↩↩↩
-
Cloudflare, “Turnstile Privacy Addendum”, última atualização em 18 de junho de 2025, acessado em 3 de novembro de 2026, 14:08:57 UTC. Os sinais que o Turnstile processa, o propósito declarado e o papel da Cloudflare como controladora para melhorar a detecção de bots, tudo citado. ↩↩↩
-
Captain’s Log do autor, 2 de novembro de 2026: minha pergunta à sessão de agente que opera minha infraestrutura, se era ela que enviava os e-mails, os três fluxos de e-mail que compartilham um endereço de remetente, os assuntos “Contact Form:” dos e-mails com os payloads do scanner, e minha anotação de que os envios do formulário feitos pelo scanner “ran about nine a minute” (iam a cerca de nove por minuto). ↩↩↩↩↩
-
Leitura de código do autor. O handler de contato e o template do formulário deste site como estavam em produção em 30 de outubro e 2 de novembro: um formulário HTMX (
hx-postpara o caminho do formulário) com o token CSRF inserido; o decorator do limitador antes do handler; a verificação CSRF, que lança um 403, antes do envio; o envio pelo Resend com o assunto “Contact Form:” e o nome do remetente; o fragmento de sucesso em um envio aceito; e dois caminhos de falha, uma configuração de e-mail ausente e um envio com falha ou sem id, que devolvem o fragmento de erro com o status padrão do FastAPI, 200. Sem campo honeypot. Lido do código-fonte em 3 de novembro de 2026. ↩↩↩↩↩↩↩↩↩ -
Leitura de código e histórico do repositório do site do estúdio, pelo autor, como estava em produção antes de 3 de novembro de 2026: o ramo honeypot do handler de contato (um campo oculto
websitenão vazio responde com o fragmento de sucesso e não grava nem envia nada; é a primeira verificação do handler, antes da verificação de endereço, da inserção e do notificador), presente desde o commit que construiu o formulário em 5 de junho de 2026; otabindex="-1"e oaria-hiddendo campo oculto; sem token CSRF; o notificador de melhor esforço, adicionado em 21 de setembro de 2026 às 17:46:47 UTC em um commit cuja mensagem diz “a paying customer’s message sat unread in D1 for a week”; e um limitador de taxa em memória por processo para clientes anônimos, com chave por endereço do cliente e classificação do cliente (o nível de bot que o limitador atribui a partir do user agent e do endereço), sobre todos os caminhos exceto health checks e arquivos estáticos. Lido do repositório em 3 de novembro de 2026. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Captain’s Log do autor, 30 de outubro de 2026, escrito às 11:58 PDT: meu relato da enxurrada às 11:46 PDT, o bloqueio manual e a regra de limite de taxa das 11:48 às 11:49 PDT, e meu teste às 11:52 PDT, citado. As contagens dele foram feitas enquanto a Cloudflare ainda ingeria os últimos minutos da enxurrada, e este post usa no lugar delas as consultas de 3 de novembro. ↩↩↩
-
Cloudflare, “GraphQL API - Limits”, documentação da GraphQL Analytics API, acessada em 3 de novembro de 2026, 14:08:57 UTC. Os conjuntos de dados adaptativos “retain at least 31 days of data on every plan” (guardam pelo menos 31 dias de dados em todos os planos). ↩↩
-
Resend, “Event Types”, documentação de webhooks do Resend, acessada em 3 de novembro de 2026, 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.deliveredpara a entrega bem-sucedida. ↩↩↩ -
Dados do autor. Análises de requisições da Cloudflare para todos os outros endereços que enviaram o formulário de contato deste site na consulta de oito dias, excluindo os dois scanners e meu endereço de teste, consultadas em 3 de novembro de 2026: três endereços (Coreia do Sul, Japão, Índia), cada um com exatamente dois 200 em um único dia, 27, 28 e 29 de outubro. ↩
-
RIPE NCC, RIPEstat network-info, whois e as-overview para o endereço de 30 de outubro e seu AS, acessados em 3 de novembro de 2026, 14:10:34 e 14:11:34 UTC: AS34534, titular “BULLIONET FBW NETWORKS SAS”, uma alocação registrada com país FR. ↩
-
Dados do autor. Análises de requisições da Cloudflare para o endereço de 30 de outubro agrupadas por caminho, método e status, consultadas em 3 de novembro de 2026; a consulta devolveu seu limite de 3.000 linhas, cobrindo 74.125 de 76.429 requisições, então as contagens de caminhos são limites inferiores: pelo menos 2.881 caminhos distintos e pelo menos 2.680 requisições com um payload de injeção de SQL no caminho em pelo menos 810 caminhos distintos; 320 POSTs para o caminho de compartilhamento do app de pintura, todos recusados (422 ou 400), e nenhum de outro endereço em oito dias; as variantes com payload do caminho de compartilhamento citadas na seção 2, respondidas com 404. ↩↩↩↩
-
Dados do autor. Análises de requisições da Cloudflare para o endereço de 30 de outubro agrupadas por user agent, consultadas em 3 de novembro de 2026; a consulta devolveu seu limite de 500 linhas, cobrindo 76.095 de 76.429 requisições, então toda contagem aqui é um limite inferior: pelo menos 69.322 requisições com uma string comum do Chrome 141 para Windows, pelo menos 2.755 com uma alegação de ser Googlebot e pelo menos 3.154 com um payload de injeção de SQL em pelo menos 396 strings distintas. ↩↩↩↩↩↩
-
PortSwigger Web Security Academy, “SQL injection cheat sheet”, acessada em 3 de novembro de 2026, 14:10:34 UTC. Atrasos de tempo para Oracle (
dbms_pipe.receive_message), Microsoft (WAITFOR DELAY), PostgreSQL (pg_sleep) e MySQL (SLEEP); a forma condicionalIF(condition, SLEEP(10), 'a');SELECT @@versionpara Microsoft e MySQL; oFROM dualdo Oracle. ↩↩↩↩↩ -
Projeto sqlmap, “time_blind.xml”, as definições de payloads cegos baseados em tempo da ferramenta, acessadas em 3 de novembro de 2026, 14:12:27 UTC. Contém payloads
SLEEP,PG_SLEEP,WAITFOR DELAYeDBMS_PIPE.RECEIVE_MESSAGE. Citado pelas famílias, não como a ferramenta que algum dos scanners usou. ↩ -
PortSwigger Web Security Academy, “Blind SQL injection”, acessada em 3 de novembro de 2026, 14:10:34 UTC. Por que um atraso de tempo em uma consulta atrasa a resposta HTTP, citado. ↩
-
OWASP Automated Threats to Web Applications, “OAT-014 Vulnerability Scanning”, acessado em 3 de novembro de 2026, 14:13:41 UTC. O resumo, citado. ↩
-
Dados do autor, anexo do registro. Análises de requisições da Cloudflare para os POSTs dos dois scanners ao formulário agrupados por status na edge e status da origem, consultadas em 3 de novembro de 2026: em 30 de outubro, 395 dos 830 429 têm um status de origem 429 e os outros 435 nenhum registrado; em 2 de novembro, 16 dos 31 429 têm um 429 de origem e 15 nenhum registrado. Nenhum 429 tem um status só da edge onde o da origem foi registrado. ↩↩
-
Teste do autor, 3 de novembro de 2026: um app FastAPI de dez linhas rodado com o ambiente do próprio site (FastAPI 0.128.0), com um decorator no estilo do limitador no endpoint. Um POST sem um campo obrigatório devolveu 422 sem que o wrapper fosse chamado; um POST completo chegou ao wrapper. ↩
-
Dados do autor. Eventos de firewall e análises de requisições da Cloudflare para meu próprio endereço de teste em 30 de outubro de 2026, consultados em 3 de novembro: três eventos da regra de limite de taxa, o primeiro às 18:52:38 UTC, com um user agent de navegador, e 24 desafios gerenciados da regra de desafio por user agent para meus envios com curl. Excluídos de todas as contagens do incidente. ↩↩↩↩↩
-
RIPE NCC, RIPEstat network-info, whois e as-overview para o endereço de 2 de novembro e seu AS, acessados em 3 de novembro de 2026, 14:10:34 e 14:11:34 UTC: AS24875, titular “NOVOSERVE-AS NovoServe B.V.”, uma rota registrada em nome da NovoServe B.V., país NL. A descrição da subatribuição nomeia uma pessoa física e não é repetida aqui. ↩
-
Dados do autor. Análises de requisições da Cloudflare para o endereço de 2 de novembro agrupadas por user agent, consultadas em 3 de novembro de 2026, completas (73 strings cobrindo todas as 10.909 requisições): 9.725 requisições com uma string comum do Chrome 141 para Windows, 942 com uma alegação de ser Googlebot, 54 payloads de injeção de SQL distintos em 149 requisições, 3 strings para quebrar aspas em 78, 12 tokens marcadores
@@em 12, e outras 2 comuns. ↩↩↩↩↩↩ -
Dados do autor. As tabelas de user agents dos dois dias, construídas pelo script do registro em 3 de novembro de 2026: a string comum do Chrome 141 para Windows e a string do Googlebot são idênticas byte a byte em 30 de outubro e 2 de novembro. ↩
-
Reprodução do autor, em 3 de novembro de 2026, dos próprios detectores do meu monitor sobre dados completos da Cloudflare para 18:58:23 a 19:03:23 UTC de 2 de novembro, a janela da execução que começou às 11:02:46 PST, rodada às 15:55:04 UTC e salva como registro de reprodução com os comandos exatos e a saída completa dos detectores; cinco linhas do anexo do registro derivam desse registro por script: 2.143 requisições do endereço no achado de enxurrada de requisições, 1.055 requisições recusadas no achado de scanner, e um achado de enxurrada no formulário a partir de 39 envios com 23 recusados, com severidade alta, que vai para o resumo diário e nem envia e-mail nem bloqueia. Os 1.914 da execução real, em uma janela que terminava no momento em que a execução consultava, são o que o log do meu próprio monitor registrou, e a execução real não levantou nenhum achado de enxurrada no formulário. A mudança de janela feita no mesmo dia: cada janela real agora termina 90 segundos antes da execução e começa onde a anterior terminou, com uma retrospectiva simples de cinco minutos quando o registro anterior falta ou é antigo. ↩↩↩
-
Dados do autor. Análises de requisições da Cloudflare para o endereço de 2 de novembro agrupadas por caminho, consultadas em 3 de novembro de 2026, completas (2.318 linhas cobrindo todas as 10.909 requisições): 2.173 caminhos distintos, e 778 requisições em 542 caminhos distintos com um payload de injeção de SQL. ↩↩
-
OWASP Web Security Testing Guide v4.2, “Testing for SQL Injection”, acessado em 3 de novembro de 2026, 14:12:12 UTC. As técnicas baseadas em erro e em atraso de tempo, citadas. ↩
-
Registro do autor da terceira mudança de 3 de novembro de 2026, feita depois de uma revisão do rascunho deste post: às 16:12 UTC, os domínios permitidos dos dois widgets definidos como o site e seu host www, sem localhost; as listas padrão de hostnames dos dois verificadores definidas com esses mesmos dois hostnames, com o desenvolvimento local definindo a própria lista; os testes deste site verificam que localhost é recusado por padrão (11 aprovados); o site do estúdio foi para o ar de novo e, rodado localmente com a chave de teste da Cloudflare que sempre aprova, um token fictício continuou chegando à validação com o hostname de exemplo dessa chave na lista e foi recusado sem ele. Às 16:13 UTC as duas regras de desafio gerenciado baseadas na pontuação de ameaça, uma em cada zona, foram desativadas, com o motivo nas descrições; os desafios por user agent continuam. Verificações no ar depois dessa mudança: o site do estúdio às 16:15 UTC (widget, envios sem token e com token lixo 403, honeypot 200), este site às 16:18 UTC (widget, envio sem token 403); nenhuma exercita a regra de hostname. ↩↩↩
-
Cloudflare, “Custom rules”, documentação do WAF da Cloudflare, acessada em 3 de novembro de 2026, 14:10:34 UTC. O que as regras personalizadas fazem, citado, e as cinco regras do plano Free com todas as ações exceto Log. ↩
-
Cloudflare, “Rate limiting rules”, documentação do WAF da Cloudflare, acessada em 3 de novembro de 2026, 14:08:57 UTC. Os parâmetros da regra; o alerta de precisão e o atraso na atualização dos contadores, citados; a tabela de disponibilidade, que dá ao plano Free um período de contagem de 10 segundos, um tempo de mitigação de 10 segundos, contagem por IP e uma regra. ↩↩↩↩↩↩
-
Ned Batchelder, “Stopping spambots with hashes and honeypots”, criado em 21 de janeiro de 2007, acessado em 3 de novembro de 2026, 14:10:34 UTC. A definição de honeypot, a razão pela qual os bots caem em campos invisíveis, a taxonomia de reprodução e preenchimento de formulários, e as duas frases dele sobre bots que derrotam as medidas e sobre voltar aos CAPTCHAs, tudo citado. A página descreve a técnica como o jeito como ele protege o próprio site; não afirma tê-la inventado. ↩↩↩↩↩↩
-
OWASP Cheat Sheet Series, “Credential Stuffing Prevention Cheat Sheet”, acessado em 3 de novembro de 2026, 14:10:34 UTC. Não depender de um único limite de volume previsível; volume baixo por endereço derrotando listas de bloqueio e limites de taxa; ferramentas que enviam diretamente sem executar JavaScript, tudo citado. ↩↩↩
-
Cloudflare, “Cloudflare HTTP headers”, documentação do Cloudflare Fundamentals, acessada em 3 de novembro de 2026, 14:14:20 UTC. Os códigos especiais do cabeçalho CF-IPCountry, incluindo “T1 - Used for clients using the Tor network.” ↩
-
Cloudflare, “IP Access rules parameters”, documentação do WAF da Cloudflare, acessada em 3 de novembro de 2026, 14:14:20 UTC. “T1: Tor exit nodes (country name: Tor)”. ↩
-
The Tor Project, “Bulk Tor Exit Exporter”, check.torproject.org, acessado em 3 de novembro de 2026, 14:10:34 UTC. A descrição da lista, citada, e a indicação do CollecTor para dados antigos; a lista em si, baixada de https://check.torproject.org/torbulkexitlist no mesmo momento, tinha 1.239 endereços. ↩↩
-
Dados do autor, anexo do registro. Cada endereço distinto que enviou o formulário do site do estúdio na consulta de oito dias, dividido pelo código de país da Cloudflare, comparado por script com a lista em massa de saídas do Tor Project baixada às 14:10:34 UTC de 3 de novembro de 2026 (1.239 endereços): 23 dos 23 endereços com código T1 na lista, e 0 dos outros 12. Só a lista de 3 de novembro; os dias dos eventos não foram conferidos contra o arquivo. ↩
-
The Tor Project, “Sources”, Tor Metrics, acessado em 3 de novembro de 2026, 14:10:34 UTC. Listas de saídas do TorDNSEL “containing the IP addresses of relays that it found when exiting through them” (contendo os endereços IP dos relays que ele encontrou ao sair por eles), e o arquivo delas. ↩↩
-
OWASP Automated Threats to Web Applications, “OAT-017 Spamming”, acessado em 3 de novembro de 2026, 14:10:34 UTC. O resumo, os outros nomes, incluindo spam de formulário, e a exclusão do abuso de formulário para e-mail voltado a destinatários não pretendidos, citados. ↩↩
-
Análise do autor. As 108 mensagens gravadas do site do estúdio separadas por regras fixas, a primeira correspondência vence (correspondência por nome de remetente para a pergunta do preço e as iscas de prêmio, texto da mensagem para o resto, uma linha colocada à mão como consulta real), 3 de novembro de 2026: as contagens da tabela da seção 5; os 25 idiomas da pergunta do preço na tabela inteira e 9 em outubro, pela minha leitura de cada frase fixa, com croata (ou bósnio) e indonésio (ou malaio) como casos apertados; toda linha da janela posterior à consulta classificada como spam. ↩↩↩↩↩
-
Cloudflare, “Turnstile widgets”, documentação do Turnstile, acessada em 3 de novembro de 2026, 14:08:57 UTC. Os modos gerenciado, não interativo e invisível e o que o visitante vê em cada um, citado para o gerenciado; a condição de política de privacidade do modo invisível; a origem challenges.cloudflare.com. ↩↩↩
-
Cloudflare, “Cloudflare Turnstile plans”, documentação do Turnstile, acessada em 3 de novembro de 2026, 14:08:57 UTC. Free e Enterprise; até 20 widgets; desafios ilimitados; uso sem outros serviços da Cloudflare, citado. ↩
-
Cloudflare, “Validate the token”, documentação de validação do lado do servidor do Turnstile, acessada em 3 de novembro de 2026, 14:08:57 UTC. A exigência de chamar o siteverify; tokens forjados; o endpoint e seus campos
secret,responseeremoteip; a validade de 300 segundos e o uso único; o máximo de 2.048 caracteres; a verificação de ação e hostname; a tabela de códigos de erro e suas duas descrições de expiração; o limite de taxa contra enxurradas de validação. Todos os trechos citados são desta página. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Cloudflare, “Cloudflare Turnstile - Easy CAPTCHA Alternative”, página de produto, acessada em 3 de novembro de 2026, 14:08:57 UTC. “Turnstile never harvests data for ad retargeting.” ↩
-
Cloudflare, “Content Security Policy”, documentação do Turnstile, acessada em 3 de novembro de 2026, 14:08:57 UTC.
script-srceframe-srcpara https://challenges.cloudflare.com; a abordagem baseada em nonce; a exigência deconnect-src 'self'para a pré-liberação, citada. ↩↩↩ -
Cloudflare, “Interstitial Challenge Pages”, documentação do Cloudflare Challenges, acessada em 3 de novembro de 2026, 14:08:57 UTC. Como uma página de desafio retém a requisição; desafios gerenciados recomendados para a maioria das regras do WAF; a limitação com AJAX e XHR (fetch) e a recomendação da pré-liberação do Turnstile, ambas citadas. ↩↩↩↩↩↩
-
Cloudflare, “Detect a Challenge Page response”, documentação do Cloudflare Challenges, acessada em 3 de novembro de 2026, 14:10:34 UTC. O cabeçalho
cf-mitigatedcom o valor challenge e o content-type text/html de todo desafio, citados. ↩ -
Cloudflare, “Clearance”, documentação do Cloudflare Challenges, acessada em 3 de novembro de 2026, 14:10:34 UTC. O cookie
cf_clearancee a pré-liberação, citados: o cookie é emitido além do token de uso único padrão, o Siteverify continua obrigatório, e um cookie permite passar pelos desafios até o nível de liberação definido no widget. ↩↩↩ -
Registro do autor da segunda mudança de 3 de novembro de 2026, feita na mesma manhã: o site do estúdio foi para o ar às 15:19 UTC e foi verificado no ar às 15:22 UTC (widget, script, chave do site e a origem de desafios no
frame-srcno ar; um envio sem token e um envio com token lixo recusados com 403; um envio ao honeypot respondido com 200); este site com build a partir das 15:20 UTC, cache limpo e verificado no ar às 15:25 UTC (as mesmas verificações de página, um envio sem token recusado com 403, depois um envio com um token CSRF válido da página no ar e sem token do Turnstile recusado com 403 de novo, o que mostra que o formulário falha fechado depois do deploy mas não consegue exercitar a verificação de hostname); os dois verificadores agora exigem que o hostname na resposta da Cloudflare seja um dos do próprio site (uma resposta sem ele é recusada) e, quando a Cloudflare devolve uma ação, que seja a do próprio widget; os onze testes deste site passam, e o novo aceita o hostname do site e recusa outro hostname, outra ação e um hostname ausente; no site do estúdio, rodado localmente com a chave de teste da Cloudflare que sempre aprova, que responde com um hostname de exemplo e sem ação, um token fictício chegou à validação com esse hostname na lista e foi recusado sem ele. ↩↩↩↩ -
Cloudflare, “Test your Turnstile implementation”, documentação do Turnstile, acessada em 3 de novembro de 2026, 14:08:57 UTC. As chaves de teste que sempre aprovam e sempre recusam e o token fictício; segredos de produção recusam tokens fictícios; suítes de testes automatizados detectadas como bots, citado. ↩↩↩↩↩
-
Registro do autor da quarta mudança de 3 de novembro de 2026, feita depois de uma segunda revisão do rascunho deste post: o handler da primeira versão para o evento before-swap do HTMX, que deixa passar um fragmento 403 contra o padrão do HTMX de não trocar uma resposta 4xx, estava anexado ao formulário de contato, enquanto o HTMX dispara esse evento no alvo da troca, o irmão do formulário, então um fragmento 403 nunca era exibido; a revisão encontrou isso lendo o HTMX empacotado e o template. A correção é um listener no body da página restrito ao id do alvo da resposta, e o teste da página inicial verifica que ele é renderizado (11 testes aprovados). Depois de o deploy ser verificado às 16:18 UTC, uma verificação no navegador às 16:21 UTC no Chromium do Playwright trocou o token de 730 caracteres do widget por uma string lixo, preencheu os campos obrigatórios com valores de exemplo e enviou o formulário pelo HTMX; o alvo da resposta mostrou o fragmento de erro, “error: Verification failed. Reload the page and try again.”, e nenhum fragmento de sucesso, e nada foi enviado. Não feito: o envio completo de uma pessoa. ↩↩
-
Cloudflare, “Embed the widget”, documentação do Turnstile, acessada em 3 de novembro de 2026, 14:08:57 UTC. O script api.js, o contêiner do widget, o campo oculto
cf-turnstile-responsecriado dentro de um formulário, eturnstile.reset(widgetId)para widgets que excederam o tempo ou expiraram, citado. ↩↩↩ -
Registro do autor das verificações no ar de 3 de novembro de 2026, cada uma depois que o deploy do próprio site tinha dado certo, e depois da limpeza de cache: às 06:35:24 e 06:35:54 PST, uma div do widget, uma tag de script e a chave do site em cada página inicial, a origem de desafios em cada
frame-srcno ar, e neste site um envio com todos os campos presentes e um token CSRF lixo recusado com 403 pela verificação CSRF; às 14:43 UTC, neste site, um envio com um token CSRF válido de 108 caracteres da página no ar e sem token do Turnstile recusado com 403 e “Verification failed. Reload the page and try again.”, e o mesmo envio com um token do Turnstile lixo recusado com 403; no site do estúdio, um envio sem token e um envio com token lixo recusados com 403 e um envio ao honeypot respondido com 200 sem gravar nem enviar nada; no Chromium do Playwright às 14:39:50 e 14:40:04 UTC, o widget renderizado (602 por 73 e 557 por 71 pixels), o objeto da API do Turnstile carregado e o campo oculto de resposta com um token de 730 caracteres nas duas páginas em até seis segundos. Não feito: o envio completo de uma pessoa. ↩↩↩ -
Cloudflare, “Rate limiting parameters”, documentação do WAF da Cloudflare, acessada em 3 de novembro de 2026, 14:08:57 UTC. Os nomes dos parâmetros; nos planos Free, Pro e Business uma ação de desafio sempre faz throttling, parafraseado; um desafio superado zera o contador, citado. ↩
-
OWASP Cheat Sheet Series, “Authentication Cheat Sheet”, acessado em 3 de novembro de 2026, 14:10:34 UTC. O CAPTCHA como controle de defesa em profundidade, citado. ↩
-
Resend, “Retrieve Sent Email”, referência da API do Resend, acessada em 3 de novembro de 2026, 14:14:47 UTC. O campo
last_eventde um e-mail consultado, por exemplo “delivered”. ↩↩ -
Resend, “View and manage emails”, documentação do Resend, acessada em 3 de novembro de 2026, 14:15:12 UTC. A lista de eventos de e-mail, incluindo “sent” e “delivered”, citados, e “queued” só para Broadcasts e Batches. ↩
-
Cloudflare, “Onion Routing and Tor support”, documentação do Cloudflare Network, acessada em 3 de novembro de 2026, 14:08:57 UTC. Onion Routing em todos os planos e a distinção entre usuários humanos do Tor e bots, citada. ↩
-
Cloudflare, “Challenges”, documentação do Cloudflare Challenges, acessada em 3 de novembro de 2026, 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”, acessado em 3 de novembro de 2026, 14:10:34 UTC. A injeção cega baseada em tempo que depende de o banco de dados fazer uma pausa, com
WAIT FOR DELAYepg_sleep()como exemplos. ↩