Rate limiting e proteção contra força bruta em OTP: boas práticas
Os ataques que atingem fluxos de OTP (adivinhação, reenvio em massa, SMS pumping) e os limites, bloqueios e alertas que os neutralizam, com código e checklist.
Um código de 6 dígitos tem um milhão de combinações. Parece muito, mas um script que tenta 100 códigos por segundo percorre tudo em menos de três horas. A força bruta em OTP só não funciona porque alguém colocou limites no caminho: tentativas por código, envios por telefone, cooldown no reenvio e bloqueios progressivos.
O problema é que esses limites raramente vêm "de fábrica". Um endpoint de verificação sem contador aceita tentativas infinitas; um endpoint de envio sem teto vira ferramenta de spam ou de SMS pumping à custa do seu saldo. Os dois erros são comuns, e os dois são baratos de corrigir.
A seguir: os ataques, os limites que os neutralizam, como detectar abuso antes da fatura e um checklist para revisar o que já está no ar.
Os ataques: adivinhação, reenvio em massa e SMS pumping
Três famílias de ataque cobrem quase tudo o que acontece com um endpoint de OTP exposto:
Adivinhação do código. O atacante pede um código para a vítima (ou para si mesmo, em um fluxo de recuperação de conta) e dispara tentativas de verificação até acertar. Sem limite, é questão de tempo; com 5 tentativas por código, a chance de acerto cai para 5 em um milhão.
Reenvio em massa. Em vez de adivinhar, o atacante usa o seu botão "reenviar" como arma: inunda a caixa de uma vítima, esgota a cota do provedor ou simplesmente faz você pagar por milhares de envios.
SMS pumping (fraude de tráfego). Um grupo em conluio com operadoras de certas faixas de números pede milhares de códigos para esses números e lucra com a terminação do tráfego. Para você, é só custo: nenhum desses "usuários" vai verificar nada. O mecanismo e os sinais estão em SMS pumping: o que é e como se proteger.
Os três compartilham uma característica: ficam visíveis em contadores simples muito antes de causarem dano, desde que esses contadores existam.
Limites por telefone, IP e conta
Rate limiting em OTP funciona em camadas, e cada camada responde a um ataque diferente. Os valores abaixo são pontos de partida; ajuste com os seus dados de uso legítimo.
| Escopo | O que limita | Ponto de partida | Contra o quê |
|---|---|---|---|
| Por telefone | Envios para o mesmo número | 5 por hora, com cooldown de 60 s entre envios | Reenvio em massa contra uma vítima |
| Por IP | Envios a partir do mesmo endereço | 20 por hora (maior, por causa de NAT e redes corporativas) | Scripts de um único ponto |
| Por conta | Envios por usuário autenticado | 10 por dia | Abuso por quem já tem conta |
| Por prefixo ou país | Envios para faixas de números | Alerta ou bloqueio de prefixos sem clientes reais | SMS pumping |
| Global | Envios totais da aplicação | Teto por minuto compatível com o pico normal | Qualquer coisa que escape das camadas acima |
Contadores de janela fixa no Redis resolvem os três primeiros escopos:
// limites.js — contadores por janela fixa no Redis
export async function dentroDoLimite(redis, chave, maximo, janelaSegundos) {
const total = await redis.incr(chave);
if (total === 1) await redis.expire(chave, janelaSegundos); // primeira ocorrência abre a janela
return total <= maximo;
}
export async function podeEnviar(redis, { telefone, ip, usuarioId }) {
if (await redis.exists(`otp:cooldown:${telefone}`)) return false;
const checagens = [
dentroDoLimite(redis, `otp:tel:${telefone}`, 5, 3600),
dentroDoLimite(redis, `otp:ip:${ip}`, 20, 3600),
usuarioId ? dentroDoLimite(redis, `otp:usuario:${usuarioId}`, 10, 86400) : true,
];
if ((await Promise.all(checagens)).includes(false)) return false;
await redis.set(`otp:cooldown:${telefone}`, 1, "EX", 60);
return true;
}
Dois cuidados. Primeiro, normalize o telefone antes de usá-lo como chave: +55 (31) 98765-4321 e +5531987654321 precisam contar como o mesmo número, ou o limite é contornado com formatação. Segundo, leia o IP certo: atrás de um proxy ou load balancer, use o X-Forwarded-For definido pelo proxy, nunca o IP da conexão.
Para uma camada genérica na API inteira, além dos contadores de OTP, o guia de rate limiting em Node.js mostra como aplicar um middleware por rota e devolver os cabeçalhos de limite ao cliente.
Tentativas de validação e bloqueio progressivo
Limitar envios protege o saldo; limitar tentativas de validação é o que de fato impede a força bruta em OTP. As regras:
- Cinco tentativas por código. Depois disso, o código é descartado e a pessoa precisa pedir outro. Com a aut2fa, isso já vem pronto: a quinta tentativa errada devolve
verification: "locked"eattempts_left: 0, e oiddo envio deixa de aceitar códigos. - Tentativas contadas por código, não por sessão. Pedir um novo código não pode "recarregar" as tentativas indefinidamente. Combine com o limite de envios por telefone: 5 códigos por hora com 5 tentativas cada são 25 palpites por hora, contra um milhão de combinações.
- Bloqueio progressivo por telefone. A cada código bloqueado, o próximo pedido de envio espera mais: 1 minuto, depois 5, depois 30. Scripts desistem; pessoas reais raramente esgotam cinco tentativas duas vezes seguidas.
- Código novo invalida o anterior. Só um código ativo por telefone e finalidade. Dois códigos válidos ao mesmo tempo dobram a chance de acerto.
- Uso único. Depois de verificado, o mesmo código retorna
valid: false, mesmo dentro do prazo.
Na interface, mostre as tentativas restantes a partir da segunda falha ("restam 3 tentativas") e, ao bloquear, explique o próximo passo. A tela descrita em UX da tela de código de verificação cobre as mensagens de cada estado.
Um erro frequente é contar a tentativa depois da comparação. Se a comparação falha por timeout de banco ou exceção, a tentativa não é registrada e o atacante ganha palpites grátis. Incremente antes de comparar, na mesma operação atômica.
Captcha e sinais de risco
Limites fixos são a base, mas não enxergam contexto. Dois complementos reduzem falsos positivos e fecham brechas:
Captcha sob demanda. Não coloque captcha para todo mundo; coloque quando um sinal de risco aparece. Exemplos: terceiro envio na mesma hora para o mesmo IP, telefone de um prefixo sem histórico de verificação, dispositivo sem cookie de sessão anterior. Muitos serviços de captcha têm modo invisível, que só mostra desafio em caso de risco.
Sinais de risco para pontuar antes de enviar:
- Proporção envios/verificações. Uso legítimo verifica a maior parte dos códigos. Um IP, prefixo ou país com dezenas de envios e zero verificações é pumping ou script.
- Sequência de números. Pedidos para
+55 11 9xxxx-0001,0002,0003em segundos não são pessoas. - Velocidade de digitação. Código enviado e verificado em menos de um segundo é automação.
- Geografia incoerente. Telefone de um país, IP de outro e idioma de um terceiro merecem desafio adicional.
- Horário e volume fora do padrão. Um pico às 3h da manhã, concentrado em um prefixo, é o caso clássico de SMS pumping.
Quando a pontuação estoura, há três respostas, em ordem de atrito: exigir captcha, trocar o canal (WhatsApp em vez de SMS, imune a pumping por operadora) ou recusar o envio. Guarde a decisão com o motivo para revisar depois.
Alertas e monitoramento
Proteção sem monitoramento é fé. Os números a acompanhar:
- Envios por minuto, com alerta quando ultrapassar um múltiplo do pico histórico.
- Taxa de verificação (verificados ÷ enviados) por canal, país e prefixo, com alerta de queda.
- Códigos bloqueados por tentativas e respostas 429 do seu backend por hora.
- Saldo e gasto diário no provedor de envio, com alerta de consumo acima do normal.
Webhooks evitam polling: APIs de OTP costumam emitir eventos de envio, entrega, falha, verificação e expiração com o id do envio e o destino, o que permite calcular a taxa de verificação em tempo real e cruzar com os contadores do Redis.
Os limites do provedor também entram no monitoramento. Uma API de OTP tem tetos por conta e, opcionalmente, por aplicação e por destinatário; o excesso volta como HTTP 429 com Retry-After. Repasse esse prazo ao usuário e nunca retente em loop, o que só agrava o bloqueio. As práticas de integrar APIs em produção com retry seguro valem aqui: backoff exponencial, respeito ao Retry-After e chave de idempotência em toda retentativa.
Por fim, defina um disjuntor: se o gasto de uma hora passar de um valor definido com antecedência, o envio para e alguém é avisado. Descobrir pumping na fatura do mês é o pior cenário, e é evitável.
Checklist de proteção contra força bruta em OTP
Antes de colocar um fluxo de OTP em produção, confira:
- Código de 6 dígitos gerado com aleatoriedade criptográfica.
- Máximo de 5 tentativas por código; depois, bloqueio e novo envio obrigatório.
- Tentativa contada antes da comparação, em operação atômica.
- Um único código ativo por telefone e finalidade; uso único.
- Cooldown de 60 s no reenvio, visível no botão.
- Teto de envios por telefone (5/h), por IP (20/h) e por conta (10/dia).
- Bloqueio progressivo após códigos bloqueados.
- Telefone normalizado antes de virar chave de contador.
- IP real lido do proxy, não da conexão.
- Captcha acionado por sinal de risco, não para todos.
- Alertas de pico de envio, queda na taxa de verificação e gasto diário.
- HTTP 429 do provedor tratado com
Retry-After, sem retentativa em loop. - Disjuntor de gasto por hora com responsável definido.
Perguntas frequentes
Quantas tentativas de verificação devo permitir por código?
Cinco é o valor mais usado e equilibra erro de digitação com proteção contra força bruta em OTP: com um milhão de combinações, a chance de acerto é de 5 em um milhão por código. Combine com o teto de envios por telefone para limitar os palpites por hora.
Rate limit por IP não bloqueia usuários legítimos em redes compartilhadas?
Pode bloquear, se o limite for apertado. Por isso o teto por IP é maior que o por telefone, e o primeiro recurso ao estourar deve ser captcha, não bloqueio. Operadoras móveis e empresas colocam centenas de pessoas atrás do mesmo IP.
Devo revelar quantas tentativas restam?
Sim, a partir da segunda falha. O atacante já sabe o limite depois de testar uma vez; esconder a informação só prejudica quem errou um dígito. Nunca revele, porém, se o telefone existe na base nem qualquer parte do código.
Bloqueio progressivo não vira uma forma de negar serviço à vítima?
Vira, se o bloqueio for só por telefone: alguém pode esgotar tentativas no número de outra pessoa de propósito. Por isso o bloqueio progressivo deve pesar também IP e dispositivo, e o usuário legítimo deve ter outro caminho, como verificação por e-mail ou suporte.
Como diferenciar SMS pumping de uma campanha que deu certo?
Pela taxa de verificação. Uma campanha real traz muitos envios e muitas verificações; pumping traz muitos envios para prefixos ou países específicos e quase nenhuma verificação. Monitorar essa taxa por prefixo, em janelas de minutos, é o sinal mais confiável.
Próximos passos
Nada disso exige infraestrutura nova: um Redis, alguns contadores, um webhook e três alertas cobrem a maior parte dos ataques. Comece pelo que protege o saldo (envios e cooldown), depois pelo que protege a conta (tentativas e bloqueio), e só então refine com sinais de risco.
Se usar a aut2fa, as 5 tentativas por código, a expiração e o uso único já vêm aplicados na API, e os limites por aplicação e por destinatário são ativados no painel; para o seu backend ficam os contadores por IP e por conta e os alertas.