SSD Nodes Learn 🎉 VPS desde $5.50/mês
Guias Matt ConnorPor Matt Connor

Como impedir bombardeio de assinaturas no cadastro

Ataques enviam centenas de confirmações para o email de uma vítima. Use double opt-in e limites de taxa para impedir que seu servidor participe do bombardeio.

O que é o bombardeamento de subscrições?

O bombardeamento de subscrições é um ataque que utiliza o seu formulário de registo para inundar a caixa de entrada de outra pessoa. O atacante obtém o endereço de email de uma vítima e submete-o a centenas ou milhares de formulários desprotegidos num curto período. Cada um desses sites envia uma mensagem de boas-vindas ou de confirmação para esse endereço. Em conjunto, essas mensagens ocultam o email que a vítima realmente precisa de ler.

O alvo é a pessoa proprietária dessa caixa de entrada. Enquanto ela recebe confirmações de subscrição, o atacante está a gastar dinheiro no cartão dessa pessoa ou a redefinir a palavra-passe de uma das suas contas. O alerta de fraude do banco continua a chegar. Chega por baixo de outras duas mil mensagens recebidas na mesma hora, por isso ninguém o vê a tempo.

O seu servidor é a ferramenta utilizada para construir o ataque. Nada no seu servidor está avariado. Nenhuma das suas contas foi comprometida. Alguém introduziu um endereço num formulário público e o seu software fez aquilo para que foi escrito: enviou uma mensagem para esse endereço. É isso que torna o ataque difícil de detetar. Não existe uma intrusão nos seus logs, porque não houve intrusão.

Como o ataque aparece do seu lado

Ele surge de uma de duas formas.

A forma ruidosa é um pico repentino. Várias centenas de pedidos POST atingem um formulário em poucos minutos, a partir de muitos endereços IP de origem diferentes, usando endereços de domínios para os quais nunca enviou mensagens. Este caso é fácil de identificar quando o procura.

A forma silenciosa é a que costuma passar despercebida. O atacante mantém uma lista com milhares de formulários vulneráveis, por isso o seu formulário só precisa de receber um ou dois envios por hora. Jye Cusch descreveu um ataque exatamente com este formato num site que gere: não houve um pico de tráfego, apenas registos constantes a chegar em horas que não correspondiam ao comportamento do seu público. Um único formulário parece inofensivo porque quase não está a fazer nada. Os danos correspondem à soma de todos os formulários presentes na lista do atacante.

Depois, ambas as formas deixam o mesmo sinal: nada acontece. Os endereços nunca são confirmados. Nunca abrem uma mensagem nem clicam numa ligação. Numa lista com confirmação de subscrição, permanecem para sempre com o estado unconfirmed, e esse conjunto é a evidência mais clara que obterá.

Comece por contar os envios por minuto no seu access log.

sudo awk '/POST \/subscription\/form/ {print substr($4, 2, 17)}' \
  /var/log/nginx/access.log | uniq -c | sort -rn | head

$4 no formato de log combinado predefinido corresponde ao timestamp entre parênteses retos. Assim, este comando apresenta uma contagem para cada minuto, começando pelo valor mais alto. Um formulário que normalmente recebe quatro subscrições por dia e apresenta sessenta num minuto não está a ter um bom dia.

Opt-in confirmado: a defesa com maior efeito

O opt-in confirmado, normalmente chamado de double opt-in, significa que um endereço não se torna subscritor até uma pessoa clicar numa ligação enviada para esse endereço. Ative esta opção e cada endereço submetido produzirá exatamente uma mensagem, uma única vez. O endereço nunca entra na lista, por isso nunca recebe uma campanha nem uma sequência de boas-vindas.

No listmonk, o servidor de newsletters self-hosted, esta é uma definição por lista: uma lista usa single opt-in ou double opt-in. A documentação é clara sobre a diferença. Numa lista com double opt-in, os subscritores "aceitam explicitamente a subscrição ao clicar na mensagem de confirmação que recebem. Até esse momento, não recebem mensagens de campanha." Um subscritor permanece em unconfirmed, passa para confirmed depois do clique e apenas os subscritores confirmed de uma lista com opt-in recebem mensagens de campanha.

Seja claro sobre o que isto resolve. O opt-in confirmado não reduz a sua contribuição a zero. Limita-a a uma mensagem por endereço. A vítima continua a receber essa mensagem, e uma mensagem de cada um de mil sites é suficiente para formar todo o ataque. O opt-in confirmado elimina tudo o que acontece depois disso: a sua lista mantém-se limpa e nunca envia uma segunda mensagem a alguém que nunca pediu a primeira.

Há mais duas definições importantes, e é fácil esquecer ambas. Primeiro, limite os reenvios da confirmação. Se o mesmo endereço puder ser submetido novamente e receber outra mensagem de confirmação a cada submissão, o atacante não precisa de mil formulários, porque o seu próprio formulário enviará mil mensagens. Um endereço que já esteja em unconfirmed nessa lista não deve receber mais nada durante pelo menos um dia. Segundo, elimine periodicamente as linhas não confirmadas. Um endereço que não confirmou a subscrição durante trinta dias não é um subscritor pendente. Mantê-lo apenas cria a possibilidade de algo lhe ser enviado por engano mais tarde.

Limitar a taxa do formulário de registo no reverse proxy

Coloque o limite à frente da aplicação, e não dentro dela. Um pedido bloqueado no proxy nunca abre uma ligação à base de dados nem inicia uma conversação SMTP (simple mail transfer protocol). Um limite dentro da aplicação é executado depois de o pedido já lhe ter custado um processo worker e uma consulta. Em muitas stacks, a mensagem é colocada numa fila antes de qualquer verificação de abuso. O limite no proxy também permanece depois de uma atualização da aplicação, porque não fica no código que substitui.

O exemplo abaixo usa nginx. A ideia aplica-se ao reverse proxy que executa à frente da aplicação, embora os nomes das diretivas sejam diferentes.

Coloque isto no bloco http, num ficheiro como /etc/nginx/conf.d/signup-limit.conf:

map $request_method $signup_key {
    POST    $binary_remote_addr;
    default "";
}

limit_req_zone $signup_key zone=signup:10m rate=2r/m;
limit_req_status 429;
limit_req_log_level warn;

O map executa trabalho real. O nginx não contabiliza um pedido cuja chave seja uma string vazia, pelo que apenas os pedidos POST entram na zona. Um utilizador que carregue várias vezes na página de registo não gasta nada. Sem esse map, quem atualizasse a página duas vezes gastaria o próprio limite antes de submeter o formulário.

$binary_remote_addr é o endereço do cliente em formato compacto. Por isso, uma zona de 10 megabytes comporta aproximadamente 160,000 endereços. rate=2r/m permite uma submissão a cada trinta segundos. limit_req_status 429 devolve HTTP 429 Too Many Requests em vez do 503 predefinido do nginx. Este é o código correto e o que uma biblioteca cliente espera.

Depois, no bloco server do seu site:

location = /subscription/form {
    limit_req zone=signup burst=3 nodelay;
    proxy_pass http://127.0.0.1:9000;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

burst=3 nodelay permite a passagem de uma pessoa que faça duplo clique no botão e rejeita imediatamente o quarto pedido, em vez de o colocar numa fila.

sudo nginx -t && sudo systemctl reload nginx

nginx -t deve apresentar configuration file /etc/nginx/nginx.conf test is successful. Agora submeta o formulário cinco vezes rapidamente e monitorize o error log:

sudo tail -f /var/log/nginx/error.log

Um pedido bloqueado escreve uma linha. Esta é a string que procura:

2026/08/13 09:14:22 [warn] 812#812: *4412 limiting requests, excess: 3.400 by zone "signup", client: 203.0.113.10, server: news.example.com, request: "POST /subscription/form HTTP/1.1", host: "news.example.com"

A ausência total de linhas significa que o limite não está a ser aplicado. A causa habitual é limit_req estar num bloco location ao qual o pedido nunca chega. Por isso, verifique com curl -si -X POST https://news.example.com/subscription/form várias vezes seguidas e confirme que obtém um 429.

Há duas armadilhas que deve conhecer antes de confiar num limite por IP.

Atrás de uma CDN ou de outro proxy, $binary_remote_addr é esse proxy. Todos os visitantes entram no mesmo bucket, pelo que as primeiras submissões de cada minuto bloqueiam todos os restantes. Corrija isto com o módulo de IP real: set_real_ip_from para cada um dos intervalos publicados pela sua CDN (a Cloudflare publica os seus em cloudflare.com/ips) e real_ip_header CF-Connecting-IP. Confirme a correção lendo $remote_addr no access log e verificando que é o endereço de um visitante, e não o da sua CDN.

O IPv6 torna fraco um limite por endereço. $binary_remote_addr contém o /128 completo, e uma atribuição IPv6 residencial é normalmente um /64 ou maior. Isso representa muito mais endereços do que um atacante consegue percorrer, cada um com o seu próprio limite disponível. Adicione uma segunda zona como teto para o próprio endpoint, usando uma constante como chave, para que o formulário tenha uma taxa total independentemente da quantidade de endereços de origem em utilização:

map $request_method $signup_total_key {
    POST    "signup";
    default "";
}

limit_req_zone $signup_total_key zone=signup_total:1m rate=30r/m;

Adicione limit_req zone=signup_total burst=10 nodelay; ao mesmo location. Defina a taxa acima da sua hora real mais movimentada, com margem suficiente. Este é um controlo abrangente: durante um ataque, também rejeita registos legítimos. Esta é a compensação correta, porque a alternativa é o seu servidor enviar o email.

Por que o limite por endereço não pode ficar no proxy

O endereço de e-mail chega no corpo da requisição, e o nginx não analisa corpos de requisição. Todas as variáveis em que limit_req_zone pode basear uma chave vêm da linha da requisição, dos cabeçalhos ou da conexão. Portanto, uma regra como "este endereço pode receber no máximo uma confirmação por dia" precisa ficar no primeiro componente que lê o corpo: a sua aplicação.

Não contorne essa limitação colocando o endereço na string de consulta para tornar $arg_email disponível. Isso grava o endereço de cada subscritor em texto simples no log de acesso e em qualquer sistema de envio de logs a jusante. Você trocaria um limite de taxa por um problema de privacidade.

Existe uma exceção real. O módulo JavaScript do nginx, njs, pode ler o corpo da requisição e definir uma variável a partir dele. Isso permite criar uma chave por endereço no proxy. É uma opção legítima, mas também adiciona código novo ao caminho da requisição. Na maioria dos sites, o limite por endereço deve ficar junto da base de dados que já sabe se esse endereço tem uma confirmação pendente. O proxy deve tratar os limites por IP e por endpoint, para os quais é adequado.

Não repita o texto enviado na mensagem

Mantenha todas as strings fornecidas pelo atacante fora da mensagem que enviar. Existem duas razões distintas, e ambas já foram exploradas na prática.

Se o seu e-mail de confirmação cumprimentar o destinatário com um nome obtido do formulário, o atacante pode escrever a própria mensagem no campo do nome. O seu servidor entregará esse texto à vítima a partir do seu domínio, assinado com a sua chave DKIM (DomainKeys Identified Mail). O seu site terá sido transformado num serviço de entrega para o abuso de outra pessoa, e o provedor do destinatário verá o seu domínio nessa mensagem.

A segunda razão é mais grave. Se algum campo enviado for concatenado manualmente num cabeçalho de e-mail, um caractere de nova linha nesse campo adicionará cabeçalhos escolhidos pelo atacante, incluindo Bcc. As bibliotecas de e-mail modernas rejeitam novas linhas em valores de cabeçalho. O código que envia texto para sendmail a partir de um script shell muitas vezes não faz essa validação.

Uma mensagem de confirmação segura contém o nome do seu site e um link, com uma frase de explicação. O endereço aparece apenas onde o agente de transferência de e-mail precisa dele, no cabeçalho To. Teste isto: envie o formulário com um campo de nome que contenha uma nova linha e um link evidente. Depois, leia a mensagem recebida em formato bruto com less e confirme que nenhum dos dois elementos permaneceu.

Aproveite também para fazer com que a página de sucesso apresente a mesma mensagem para todos os endereços. Uma página que mostre "já está subscrito" para um endereço e "verifique a sua caixa de entrada" para outro transforma o formulário num verificador de subscrições para qualquer pessoa que tenha uma lista de endereços para testar.

Que verificação contra bots deve usar?

Escolha a opção de acessibilidade com o mesmo cuidado que escolhe a eficácia. Um CAPTCHA de seleção de imagens não pode ser resolvido por uma pessoa cega, e a alternativa de áudio é difícil para pessoas com audição comum. Uma verificação que faz uma pessoa legítima desistir do registo é uma defesa que também tem um custo. Existem quatro opções, pela ordem em que deve tentar usá-las.

Prova de trabalho no navegador. O navegador calcula um hash que o servidor pode verificar com baixo custo, sem exigir que uma pessoa resolva nada. O listmonk disponibiliza esta opção em Settings, depois Security, usando ALTCHA, que não precisa de um serviço de terceiros. Em agosto de 2026, esta é a recomendação do próprio listmonk, em vez da opção hCaptcha descontinuada. O custo recai sobre quem envia mais pedidos, que é o atacante.

Uma verificação gerida e não interativa. O Cloudflare Turnstile não mostra praticamente nada à maioria dos visitantes e só apresenta um desafio quando os seus sinais parecem suspeitos. É eficaz, mas coloca um terceiro no caminho do registo.

Um campo honeypot. É um campo de texto que uma pessoa nunca vê, mas que um bot ingénuo preenche. Dê-lhe um nome que o formulário não utilize de outra forma e defina autocomplete="off", tabindex="-1" e aria-hidden="true" para impedir que um gestor de palavras-passe o preencha e que um leitor de ecrã o anuncie. Um campo chamado email2 ou address é preenchido automaticamente pelo navegador, fazendo com que rejeite pessoas reais.

<div style="position:absolute; left:-9999px;" aria-hidden="true">
  <label for="hp_ref">Leave this field empty</label>
  <input type="text" id="hp_ref" name="hp_ref" autocomplete="off" tabindex="-1">
</div>

Uma verificação do tempo até ao envio. Coloque um timestamp assinado num campo oculto quando a página for apresentada e rejeite um envio que chegue menos de dois segundos depois. Uma pessoa não consegue ler um formulário e escrever um endereço tão depressa. Assine o timestamp; caso contrário, o bot pode simplesmente enviar um timestamp antigo.

Há um ponto que deve verificar qualquer que seja a opção escolhida: o token tem de ser consumido uma única vez. Se um script conseguir resolver a verificação uma vez e reutilizar esse token contra mil endereços, a verificação apenas provou que um navegador foi executado uma vez e nada mais.

Como descobrir isso antes de receber o relatório de abuso?

Você quer que os seus próprios gráficos mostrem o problema, e não o departamento de abuso do provedor de alojamento. Monitorize duas coisas.

Conte os envios por endereço de origem em todo o log:

sudo awk '/POST \/subscription\/form/ {print $1}' /var/log/nginx/access.log \
  | sort | uniq -c | sort -rn | head -20

Depois, faça o fail2ban ler as mesmas linhas limiting requests que o nginx já escreve e bloquear os infratores reincidentes. O fail2ban fornece um filtro específico para isso. Crie /etc/fail2ban/jail.d/nginx-limit-req.local:

[nginx-limit-req]
enabled  = true
filter   = nginx-limit-req
port     = http,https
logpath  = /var/log/nginx/error.log
findtime = 600
maxretry = 10
bantime  = 3600
sudo systemctl reload fail2ban
sudo fail2ban-client status nginx-limit-req

A saída de estado lista o filtro da jail e as contagens atuais de falhas e bloqueios. Currently banned: 0 num dia tranquilo está correto. Se a jail não aparecer, o fail2ban não carregou o ficheiro, e sudo fail2ban-client -d | grep nginx-limit-req mostra a configuração que foi efetivamente analisada. O filtro fornecido corresponde a todas as zonas limit_req. Restrinja-o à sua zona de inscrição definindo ngx_limit_req_zones = signup numa secção [Definition] de /etc/fail2ban/filter.d/nginx-limit-req.local. A estrutura dos ficheiros da jail e os comandos de bloqueio são explicados com mais detalhe no guia do fail2ban para Ubuntu 24.04.

O segundo sinal é uma proporção e não requer software novo: envios divididos por confirmações. Numa lista saudável, a maioria das pessoas que envia um endereço clica no link, normalmente bem mais de metade. Quando essa proporção cai enquanto os envios aumentam, o serviço está a ser utilizado de forma abusiva. Compare a contagem de subscritores unconfirmed criados na última hora com a contagem dos confirmed correspondentes, usando a periodicidade em que já executa os relatórios.

O custo: reputação do remetente e blocklists

Esta é a parte que transforma um incómodo numa fatura.

As listas de endereços usadas para o bombing são recolhidas, e as listas recolhidas contêm spamtraps: endereços que nunca se inscreveram em nada, publicados apenas para detetar remetentes que enviam mensagens sem autorização. A sua mensagem de confirmação chega a um deles. Para alguns operadores de blocklists, isso é suficiente.

Os destinatários que nunca pediram a sua mensagem não clicam em cancelar a subscrição. Clicam em "denunciar spam". As regras de envio em massa da Google, em vigor desde fevereiro de 2024, exigem que os remetentes de 5,000 ou mais mensagens por dia para o Gmail mantenham a taxa de spam denunciado no Postmaster Tools abaixo de 0.3%. Um remetente mais pequeno não é avaliado com base nesse número, mas o mesmo sinal de reclamação influencia as decisões de filtragem que colocam as suas mensagens na pasta de spam. Os endereços falsos incluídos no envio também geram hard bounces, e uma taxa crescente de hard bounces é um sinal de reputação independente em todos os grandes fornecedores.

Se executar o seu próprio servidor de correio num VPS com mailcow, a listagem afeta o seu endereço IP e o seu domínio. A remoção da listagem por um operador como a Spamhaus exige o preenchimento de um formulário e um período de espera. Durante esse período, as suas faturas e as reposições de palavra-passe também não são entregues. Se enviar através de um fornecedor partilhado, espere que suspenda primeiro a sua conta e leia a sua explicação depois, porque o seu tráfego representa um risco para todos os outros remetentes nesse IP.

Em comparação, o trabalho é pequeno. Ative hoje o opt-in confirmado, porque é uma definição por lista. Em seguida, adicione o limite de taxa no proxy, porque exige apenas um ficheiro e um reload. A verificação de bots e os alertas podem ser configurados ainda esta semana.

FAQ

O double opt-in impede o subscription bombing?

Impede que a sua lista fique poluída e limita a sua contribuição a uma mensagem por endereço submetido, que é a maior melhoria individual ao seu alcance. Não impede que a caixa de entrada da vítima fique cheia, porque o ataque é a soma de uma mensagem de cada um de mil sites. Combine-o com um limite de pedidos por IP no seu proxy e um limite para os reenvios de confirmação, para que submeter o mesmo endereço duas vezes não produza uma segunda mensagem.

Como distingo uma campanha de bombing de um bom dia de subscrições reais?

Observe o que acontece depois da submissão. As subscrições reais são confirmadas, normalmente no espaço de algumas horas. Uma campanha de bombing deixa um conjunto de endereços que nunca são confirmados, nunca abrem mensagens e nunca clicam. As submissões também se concentram de forma anormal: muitos endereços de origem que nunca viu, domínios de destinatários para os quais normalmente não envia mensagens e horas de chegada distribuídas uniformemente ao longo do dia, em vez de seguirem o horário habitual da sua audiência.

Devo eliminar os endereços submetidos?

Sim. Elimine os registos não confirmados com mais de cerca de trinta dias e faça-o segundo um agendamento, não manualmente. Nunca envie mais nada para esses endereços, incluindo um pedido de desculpa ou uma mensagem "foi você?", porque isso seria uma segunda mensagem não solicitada para alguém que já recebeu muitas mensagens desse tipo. Se algum desses endereços for um spamtrap, um contacto posterior será a confirmação que o operador da blocklist está a aguardar.

O rate limiting vai rejeitar subscritores reais?

Um limite por IP de uma submissão a cada trinta segundos, com um burst de três, é invisível para uma pessoa que preenche um formulário uma vez. Torna-se visível quando muitas pessoas reais partilham um endereço, por exemplo, num escritório atrás de um gateway NAT (network address translation), ou quando o seu proxy vê o endereço da sua CDN em vez do endereço do visitante. Leia $remote_addr no seu access log antes de apertar os limites e mantenha o limite máximo do endpoint acima da sua hora real mais movimentada.

O meu IP de envio está numa blocklist depois de uma campanha. O que faço primeiro?

Pare de enviar a partir dele antes de pedir qualquer remoção. Pause a fila da campanha, corrija o formulário e elimine os endereços não confirmados, porque uma remoção da blocklist seguida de mais tráfego do mesmo tipo fará com que o IP seja novamente listado mais depressa do que da primeira vez. Depois descubra em que lista está, pois a maioria dos operadores tem uma página de consulta baseada no seu endereço IP, e siga o respetivo processo de remoção. Conte com uma espera de vários dias e use esse tempo para confirmar que o seu registo SPF (sender policy framework) e a assinatura DKIM continuam a passar.

#email#double-opt-in#rate-limiting#abuse#deliverability