Segurança da Informação

Como detectamos bots e usuários maliciosos em produção sem comprometer a performance

Análise de risco baseada no comportamento das requisições nas rotas do backend. Um middleware Django que pontua sessões em tempo real e decide bloquear sem tocar no banco de dados.

Por admin
9 de abril de 2026
12 min de leitura

A gente não começou querendo construir um sistema de detecção de bots. Começou porque precisávamos entender o que estava acontecendo nas rotas do backend.

Tínhamos aplicações Django em produção com tráfego crescente, e junto com os usuários reais vinham requisições que não faziam sentido: varreduras de paths que não existem, sessões que mudavam de user-agent no meio do caminho, picos de requests vindos do mesmo IP em intervalos perfeitos demais. Nada disso gerava erro. Tudo passava silenciosamente.

O que faltava era uma forma de olhar para o comportamento de cada sessão nas rotas e dizer: "isso aqui não parece um usuário real".


Por que rate limiting não resolvia

Rate limiting foi a primeira coisa que tentamos. Funciona para o caso óbvio — o IP que manda 200 requests por minuto. Mas os casos que nos preocupavam eram outros.

Um bot que faz 25 requests por minuto com intervalos regulares, sem cookie de sessão, acessando endpoints autenticados com um user-agent de Chrome 110 (lançado no início de 2023) não vai cair em nenhum rate limiter razoável. Ele parece tráfego normal se você olha só o volume.

O problema é que rate limiting toma decisão com base em um número. Precisávamos tomar decisão com base no comportamento — o que a sessão está fazendo, como está fazendo, e se esse padrão faz sentido para um humano.


Análise de risco por comportamento nas rotas

A ideia central é simples: cada requisição que chega no backend carrega sinais sobre quem (ou o que) está do outro lado. Nenhum sinal sozinho prova nada, mas a combinação deles conta uma história.

Uma sessão que acessa /api/auth/login 40 vezes em 3 minutos, com intervalos de exatamente 1.2 segundos entre cada tentativa, sem nunca ter acessado a página de login no frontend, está se comportando de um jeito que nenhum usuário real se comporta.

Em vez de criar regras do tipo "se X então bloqueia", montamos um sistema de score de risco: cada sinal contribui pontos, e o bloqueio acontece quando o acumulado passa de um threshold. Isso evita falsos positivos de sinais isolados e ainda pega os bots que seriam invisíveis para qualquer regra individual.


Como funciona na prática

O que não podia acontecer era essa análise adicionar latência perceptível. As rotas do backend precisam responder rápido — a análise de risco tem que ser quase invisível no tempo de resposta.

O middleware faz tudo dentro do ciclo da requisição, em duas etapas:

Antes da view: consulta o Redis pra ver se aquele IP já tá bloqueado. Se tiver a chave rg:blocked:{ip}, retorna 429 imediatamente — sem rodar analyzers, sem tocar na view. É um lookup no Redis que leva menos de 1ms.

Depois da view: a requisição passou, a view executou, e agora o middleware registra o que aconteceu no histórico deslizante (path, método, status code, duração, user-agent). Com o histórico atualizado, os analyzers rodam e calculam o score. Se passar do threshold de bloqueio (padrão: 80), o IP é marcado no Redis com TTL de 1 hora e as próximas requisições são cortadas antes de chegar na view.

Tem um detalhe importante aqui: os analyzers avaliam o histórico das requisições anteriores, não a requisição atual. Isso significa que o primeiro request de um IP desconhecido sempre passa. O bloqueio acontece quando o padrão se acumula — o que na prática leva poucos segundos pra um bot agressivo.

Além do bloqueio, existe um threshold intermediário de challenge (padrão: 50). Quando o score fica entre 50 e 80, a requisição não é bloqueada, mas o middleware seta request.risk.challenged = True. A view pode usar isso pra decidir o que fazer — exigir CAPTCHA, limitar funcionalidade, ou só logar.

Os logs estruturados vão pro Grafana via Loki, onde a gente acompanha scores, bloqueios e sinais em tempo real.


Os analyzers

Cada analyzer olha pra um aspecto diferente do comportamento nas rotas. São seis no total — cinco que rodam no middleware a cada requisição HTTP e um que roda no login.

RateAnalyzer — calcula requests por minuto na janela deslizante de 5 minutos. Três faixas: acima de 30 RPM soma +15, acima de 60 soma +30, acima de 120 soma +50. Se todas as requisições caem no mesmo milissegundo (span zero), é +50 direto.

UserAgentAnalyzer — user-agent ausente dá +30. Ferramentas conhecidas de automação (curl, scrapy, python-requests, wget, go-http-client) dão +40. Versão do Chrome abaixo de 120 dá +20. Ele extrai a versão com regex, então não depende de lista externa.

SessionAnalyzer — rastreia sessões por IP usando Sets no Redis. Mais de 10 sessões distintas do mesmo IP em 5 minutos dá +30. Mais de 3 user-agents diferentes dentro da mesma sessão dá +35. Acesso a rotas com prefixo /api/ ou /admin/ sem sessão autenticada dá +25.

PatternAnalyzer — mantém uma lista de paths de varredura: /.env, /wp-admin, /phpmyadmin, /.git, /.aws, /config.php. Qualquer hit dá +60 — é o analyzer que mais pontua num único sinal, porque acessar /.env numa aplicação Django não tem explicação inocente. Também monitora taxa de erros (mais de 50% de status 4xx dá +30) e diversidade de paths (mais de 40 paths distintos na janela dá +25).

TimingAnalyzer — calcula o coeficiente de variação dos intervalos entre requisições. Precisa de pelo menos 5 requisições pra ativar. Se o CV for menor que 0.05 (intervalos quase idênticos), soma +30. É o analyzer mais sutil — pega bots que controlam o volume mas não randomizam o timing.

EmailAnalyzer — esse não roda no middleware. Ele é acionado pelos signals user_logged_in e user_login_failed do Django. Analisa o email usado no login: domínio descartável (mailinator, guerrillamail, tempmail) dá +40, sufixo hexadecimal longo dá +30, proporção alta de dígitos dá +25, entropia de Shannon acima de 3.5 dá +30. Útil pra detectar cadastros automatizados.


Uso no signup: fricção proporcional ao risco

Um dos usos que mais gostamos é no fluxo de cadastro. A ideia é simples: usuário de baixo risco segue direto, usuário de alto risco precisa provar que é real antes de usar a conta.

Na prática, quando alguém submete o formulário de signup, a gente roda o EmailAnalyzer contra o email informado. Se o score ficar baixo — email de domínio corporativo, entropia normal, sem padrão de geração automática — a conta é criada ativa e o usuário segue pro onboarding sem nenhuma fricção extra.

Se o score ficar alto — email de domínio descartável, local part com cara de hash gerado, proporção alta de dígitos — a conta é criada mas fica inativa. O usuário recebe um email de verificação e só consegue acessar depois de confirmar. Dependendo do contexto, a gente adiciona um CAPTCHA no próprio formulário de signup quando o middleware já detectou que a sessão tá com score de challenge (acima de 50) antes mesmo de chegar no submit.

O ponto é que a decisão não é binária. Não é "todo mundo confirma email" (que adiciona fricção desnecessária pra usuários legítimos) nem "ninguém confirma" (que deixa a porta aberta pra cadastros em massa). É fricção proporcional ao risco.

Na prática isso matou a maioria dos cadastros automatizados que tínhamos. Bots de signup em massa tipicamente usam domínios descartáveis, emails com entropia alta, e fazem dezenas de cadastros do mesmo IP em minutos. O combo do EmailAnalyzer com o SessionAnalyzer pega isso nas primeiras tentativas — e o management command audit_emails ainda permite varrer retroativamente os cadastros que passaram antes do sistema entrar no ar.


Um caso real: score acumulando até o bloqueio

Pra ficar mais concreto, segue um caso que pegamos em produção. Os valores foram simplificados, mas o padrão é real.

Uma terça-feira de manhã, um IP começa a acessar a API de autenticação de uma das nossas aplicações. As primeiras requisições não chamam atenção — volume baixo, user-agent de Chrome recente.

Mas o padrão vai se revelando:

| Momento | O que aconteceu | Analyzer | Score acumulado | |---|---|---|---| | #1–#5 | POST /api/auth/login a cada 1.3s, sem sessão autenticada | SessionAnalyzer (auth sem sessão: +25) + TimingAnalyzer (CV < 0.05: +30) | 55 | | #6 | Mesmo IP, user-agent muda de Chrome pra Firefox | SessionAnalyzer (rotação de UA: +35, cap em 100) | 80 | | #7 | Score = 80. Threshold de bloqueio: 80. IP marcado no Redis. | — | bloqueado | | #8+ | Todas as requisições seguintes | Middleware retorna 429 direto do Redis | — |

Do primeiro request ao bloqueio: 8 segundos e 6 requisições. A partir da #7, o middleware bate no Redis, encontra rg:blocked:{ip}, e retorna 429 Too Many Requests sem rodar nenhum analyzer e sem a requisição chegar na view.

O que chama atenção é que entre a #5 e a #6, o score já tava em 55 — acima do threshold de challenge (50), mas abaixo do de bloqueio (80). Se a view usasse request.risk.challenged, já poderia ter exigido CAPTCHA nesse ponto. Quando o user-agent mudou na #6, o SessionAnalyzer adicionou +35 por rotação de UA e o score bateu 80.

Nenhum sinal sozinho teria bloqueado. Cinco tentativas de login sem sessão? Dá +25 — longe do bloqueio. Intervalos regulares? +30 — ainda não chega. Mas a combinação dos três, em 8 segundos, não deixa dúvida.


Falsos positivos e os casos que não são óbvios

O modelo de score funciona bem pra bots, mas traz uma preocupação inevitável: e quando bloqueia quem não deveria?

Mapeamos os cenários que mais geram falsos positivos e como tratamos cada um:

VPN corporativa e NAT — 50 usuários reais atrás do mesmo IP. O SessionAnalyzer vê dezenas de sessões novas do mesmo IP e soma +30 (excessive_sessions). Nos projetos onde isso é comum, a gente sobe o threshold de bloqueio ou escreve um analyzer customizado que pondera por sessão autenticada — se as sessões têm tokens válidos, o peso do IP cai. O pacote não resolve isso sozinho porque depende de como cada aplicação gerencia autenticação.

Health checks e monitoramento — um cliente rodando requests contra /health/ a cada 30 segundos com intervalos perfeitos. O TimingAnalyzer pontuava alto. A solução já vem no pacote: IGNORE_PATHS inclui /health/, /metrics/ e /__debug__/ por padrão. Requisições pra esses paths não passam pelos analyzers e não gravam histórico. Se o health check bate em outra rota, é só adicionar na lista.

Usuários com browsers antigos — Chrome abaixo da versão 120 dá +20 no UserAgentAnalyzer. Não bloqueia sozinho (threshold é 80), mas soma. Em órgãos públicos onde a TI controla a versão instalada, isso é comum. O min_chrome_version é configurável — dá pra ajustar pro perfil de usuário de cada projeto.

Bots legítimos — Googlebot, Bingbot, ferramentas de SEO. O UserAgentAnalyzer marca como bot (+40). O pacote não traz whitelist de IP embutida, então a gente trata isso numa camada acima — via signal handler que escuta risk_assessed e zera o score se o IP confere com os ranges do Google via DNS reverso. Quem faz spoofing de Googlebot não passa.

A calibragem dos primeiros dias é a parte mais importante. A gente sobe o threshold pra um valor inalcançável (tipo 200) e liga LOG_ALL_SCORES pra logar todo score, inclusive zero. Roda assim uma ou duas semanas só observando o perfil de tráfego real. Quando liga o bloqueio de fato, começa com o default de 80 e ajusta conforme o volume de falsos positivos.


O pacote open source

Quando percebemos que essa estrutura de análise era genérica o suficiente pra funcionar em qualquer projeto Django, extraímos num pacote separado.

O django-risk-guardian tá no GitHub com licença MIT. Adiciona o middleware, configura o cache backend pro Redis, e os 6 analyzers já rodam com defaults funcionais.

Além do middleware, o pacote inclui algumas peças que facilitam a integração:

  • Signalsip_blocked, risk_assessed e challenge_required. Dá pra plugar qualquer lógica customizada (notificação no Slack, whitelist dinâmica, log externo) sem mexer no middleware.
  • Decorators@require_risk_below(threshold=50) e @require_no_challenge pra proteger views específicas. Retornam 429 se o score não atende.
  • Management commandpython manage.py audit_emails varre a base de usuários e roda o EmailAnalyzer retroativamente. Útil pra identificar cadastros automatizados que já passaram.
  • request.risk — toda requisição ganha um objeto RiskAssessment com score, reasons, blocked, challenged e acesso ao history. A view consegue tomar decisões granulares com base nisso.

O que não tá no pacote são os thresholds específicos que usamos em cada projeto e os analyzers customizados que escrevemos pra domínios específicos. Cada aplicação tem um perfil de uso diferente, e os limites precisam refletir isso. É o mesmo modelo do fail2ban: motor público, configuração operacional privada.

👉 github.com/mupisystems/django-risk-guardian


O que muda no dia a dia

Antes a gente sabia que tinha tráfego suspeito, mas não tinha como quantificar nem reagir em tempo real. Agora, no Grafana, dá pra ver quais IPs estão acumulando score, por quais sinais, e em qual momento foram bloqueados.

O Nginx continua fazendo o trabalho dele na borda. O Django agora sabe não só o que cada requisição pediu, mas o quanto aquele padrão de acesso se parece com comportamento legítimo. E quando não parece, bloqueia antes de chegar na view.

Não é uma solução que resolve tudo. Mas fecha uma lacuna que existia entre o rate limiting genérico e o "torcer pra ninguém abusar".


Quer implementar análise de risco comportamental no seu backend Django? Fala com a gente.

Tags

#segurança de aplicação#Django#detecção de bots#rate limiting#open source#Redis#Grafana

Sobre o Autor

admin

admin

Especialista em transformação digital

Categorias

Segurança da Informação

Gostou deste conteúdo?

Assine nossa newsletter e receba mais insights como este diretamente no seu e-mail.

Falar com especialistas
Como detectamos bots e usuários maliciosos em produção sem comprometer a performance | Mupi Systems Blog | MUPI Systems