X-Forwarded-For (XFF) é o cabeçalho que diz ao seu app "o IP real do visitante é este" quando o tráfego passa por um proxy. O detalhe: o cliente pode forjar esse valor livremente. Eis um caso comum de dev independente — "fomos atingidos por um scan de falsificação de XFF" — transformado em lição, anonimizado, sem passos de ataque reproduzíveis.
- Tipo
- Falsificação de X-Forwarded-For + sondagem de injeção (scan automatizado)
- Impacto
- Nenhum (detido pela defesa em profundidade)
- Como surgiu
- Requisições de ataque deram 500; uma enxurrada de e-mails de exceção
- Proteções que funcionaram
- ① placeholders (valores vinculados) ② validação de charset do BD
- A configuração incorreta deixada
- "confiar em todos os proxies" — sem proxy de fato na frente
- Resposta
- Higienizar cabeçalhos de IP na fronteira (curto prazo) → corrigir a config de proxy confiável (causa)
Primeiro, separe: "ataque" ou "erro meu"?
Não pule para "ataque!" ao ver erros. Se você acabou de mudar a configuração, pondere "talvez minha mudança fez isso" com igual seriedade.
Suspeite da sua própria mudança
Deixe o valor ser a evidência
Direcionado ou um scan?
A visão deste site: ao trabalhar de trás para frente a partir dos sintomas, teste cada hipótese contra os dados
"A coisa que acabei de mexer parece suspeita" é um bom ponto de partida. Mas se a evidência (aqui, o próprio valor) a refuta, abandone-a. Forme várias hipóteses e descarte-as uma a uma com dados, não com palpites; você vai errar a causa muito menos vezes.
O que era o ataque: um XFF falsificado carregando sondagens de injeção
O bot coloca uma string de sondagem no XFF "em vez de um IP", com só um IP de aparência normal no fim. Ele está verificando uma coisa só:
"Este app confia no valor do XFF e o mistura cru em uma consulta de BD, ou o imprime direto no HTML?"
Se o app é desleixado, a injeção de SQL ou o XSS é bem-sucedido via XFF. Ele abusa do "cabeçalho que carrega um IP" como um lugar para enviar strings arbitrárias.
Por que não houve dano: duas proteções
Proteção ① valores vinculados (placeholders)
O valor do IP foi passado via um placeholder como dado, não concatenado em string. A entrada não pode ser interpretada como comando, então o SQLi não pode se formar por design. Esse é um dos principais benefícios de usar um framework.
Proteção ② validação de charset do BD
Os bytes malformados (UTF-8 longo demais etc.) são sequências inválidas que o charset da coluna não consegue armazenar. O BD rejeitou a gravação e lançou erro — o ataque falhou e foi detectado.
Tudo o que aconteceu: as próprias requisições do atacante deram 500 e uma enxurrada de e-mails de exceção saiu. Nenhum vazamento, nenhuma adulteração, nenhum login não autorizado. Um caso de manual de defesa em profundidade funcionando como pretendido.
O problema real: confiar em todos os proxies
Se parássemos aqui, não haveria problema, mas uma coisa não pode ser ignorada. Por que o XFF falsificado chegou ao tratamento de IP afinal?
✗ Confiar em todos os proxies (curinga)
XFF falso do cliente → adotado como o "IP real" → logs, sessões e verificações de IP recebem o valor falso
✓ Confiar em nada (padrão)
XFF falso ignorado → IP real do socket é usado → a falsificação não tem efeito
Esta configuração não tinha nenhum proxy reverso ou CDN — o servidor web estava diretamente exposto. Então o XFF nunca precisou ser confiado, mas estava definido como "confiar em todos". "Só permita tudo" é uma configuração comum de tempo de desenvolvimento que acaba ficando em produção.
Fácil de passar despercebido: a falsificação de XFF não é só sobre injeção
Qualquer coisa que confie no IP do cliente acaba usando o valor falso: burla de rate-limit, burla de lista de permissão de IP, evasão de banimento, logs de auditoria forjados. Mesmo com defesas de injeção fortes, isto é um problema separado. Inventarie "onde eu confio no IP?" e verifique a base dessa confiança (= sua config de proxy).
Correção: higienizar na fronteira → definir a confiança corretamente
Higienize os cabeçalhos de IP na fronteira (curto prazo, imediato)
Defina os proxies confiáveis corretamente (causa)
http/https, então mudanças podem causar laços de redirecionamento — verifique em um ambiente primeiro.Não confunda 'IP válido' com 'IP real'
Verifique: após corrigir, repita o ataque para confirmar
Transforme "acho que corrigi" em "definitivamente parou". ① aplique a um ambiente só → ② envie você mesmo o mesmo cabeçalho malformado → ③ confirme que as contagens de erro não sobem → ④ confirme que o acesso normal ainda funciona → ⑤ implante em todo lugar. A implantação em etapas evita acidentes.
Continuação: depois que o cabeçalho de IP foi bloqueado, o ataque passou para o User-Agent
Dezenas de minutos após adicionar a higienização, o mesmo tipo de erro voltou — em uma coluna diferente desta vez: user_agent, não ip_address. Bloqueado no cabeçalho de IP, o atacante moveu a mesma carga para o cabeçalho User-Agent. Isso exigiu uma abordagem diferente.
A pergunta certa não é "qual cabeçalho é perigoso?", mas "em qual coluna de BD a entrada não confiável aterrissa no fim?" Se os únicos sinks são ip_address e user_agent, higienizar as entradas que os alimentam (cabeçalhos de IP + User-Agent, mais Referer por garantia) detém os erros derivados de sessão por design; mover a carga para outros cabeçalhos não armazenados não causa erro. User-Agents legítimos são ASCII/UTF-8 válido, então retirar só os bytes inválidos não tem impacto.
A visão deste site: trabalhe de trás para frente a partir do sink em vez de bloquear entradas uma a uma
Bloquear cabeçalhos um de cada vez não funciona; um atacante sempre pode usar outro cabeçalho. Raciocine de trás para frente a partir do sink (a coluna armazenada / saída) e enumere toda entrada que chega a ele, de uma vez. Como este caso mostrou, quando o ataque passou do cabeçalho de IP para o User-Agent: quando você corrige um cabeçalho, trate ao mesmo tempo as outras entradas que chegam ao mesmo sink. E de novo — repetimos o segundo ataque e confirmamos que parou antes de implantar.
E mais uma vez: defender logo antes de o valor ser salvo
Logo após tratar também o User-Agent, o mesmo erro da coluna ip_address voltou. A higienização de entrada estava implantada, e repetir o mesmo cabeçalho malformado em um autoteste não o reproduzia — mas o erro continuava recorrendo em produção. Este é o ponto-chave do caso.
O motivo: a lógica do framework para "resolver o IP do cliente" é internamente complexa — múltiplas notações de cabeçalho (X-Forwarded-For, Forwarded, cabeçalhos customizados), combinações com configurações de confiança de proxy, ordem de parsing e cache. Em algum lugar desses caminhos internos, um valor malicioso voltou como o "IP do cliente" por uma rota que não havíamos higienizado. Verificar no ponto de entrada não consegue pegar tudo.
✗ Defender na entrada
Higienizar cabeçalhos da requisição → o IP malicioso do cliente volta por outro caminho interno → burlado
✓ Defender na saída
Verificar logo antes da gravação no BD → o único lugar por onde todo valor passa → sem burla
Então mudamos a abordagem e defendemos na saída (logo antes da gravação no BD). Subclasse a classe de armazenamento de sessão e sobrescreva só as partes que produzem o valor (conceito): o IP armazenado vira null a menos que seja um IP válido; o User-Agent armazenado tem os bytes inválidos retirados e o comprimento limitado. Por mais que a resolução do IP do cliente se comporte internamente, a verificação sempre roda logo antes da gravação, então nada escapa. Enviando o cabeçalho malicioso pelas duas rotas de ataque dezenas de vezes cada, as contagens de erro não subiram e as sessões legítimas continuaram funcionando.
A visão deste site: a verificação final pertence à saída (ponto de uso)
A regra básica de segurança é "não confie na entrada; sempre valide/escape no ponto de uso (a saída)." Verificações de entrada ajudam a rejeitar cedo, mas informação que o framework resolve/deriva internamente (como o IP do cliente) pode passar pela verificação de entrada e aparecer de novo. Validar/escapar logo antes de um sink perigoso (BD, HTML, um comando) é o único lugar por onde todo valor passa, então é ali que a verificação final pertence. Faça entrada e saída como duas coisas separadas.
Lição: é mais seguro quando uma entrada ruim é rejeitada com um erro visível
Aqui o ataque falhou e foi tornado visível como uma enxurrada de notificações de erro. Isso não é um resultado ruim: ser rejeitado com um erro visível é mais seguro do que ser deixado passar sem ninguém notar. Não descarte as notificações de erro como ruído; use-as para perceber anomalias. Não pare em "bloqueamos o impacto"; corrija "por que chegou às entranhas". Essa é a postura deste site.
Leia a seguir
- Exemplo: CVE-2026-102895 GPUStack (uma autenticação que confiava na origem da requisição deixou partes não autenticadas agirem como administrador em uma plataforma de IA)
- Glossário: O que é injeção de SQL / O que é XSS / O que é SSRF
- Fundamentos: Fundamentos de segurança: .env e chaves de API
- Onde isso importa: limitação de taxa e controle de abuso (por qual IP você conta as requisições depende de como você trata o XFF)
- O panorama: Next.js e React têm muitas vulnerabilidades? (o que foi encontrado nos recursos de proxy embutidos dos frameworks)