Guias de Segurança

O que é a falsificação de X-Forwarded-For (XFF): configuração incorreta de proxy confiável e como se defender

X-Forwarded-For (XFF) é um cabeçalho que o cliente pode forjar. Como confiar em todos os proxies deixa o valor falso chegar ao app e como corrigir.

Publicado 2026-06-08 12 min de leitura
Nesta página

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.

Arquivo do caso
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)
0
Impacto real (vazamento/adulteração)
2
Proteções que funcionaram
todos
Proxies confiados (curinga)
fronteira
A correção principal

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.

1

Suspeite da sua própria mudança

Hipótese: uma mudança recente de configuração fez os usuários relogarem repetidamente. Mas isso não explica as sequências de bytes malformados no valor armazenado → rejeitada.
2

Deixe o valor ser a evidência

O que apareceu foram aspas codificadas em URL múltiplas vezes e sequências de bytes longas demais (redundantes) — não valores que usuários reais ou proxies produzem → carga maliciosa confirmada.
3

Direcionado ou um scan?

Menos um ataque direcionado, mais provavelmente um scanner automatizado varrendo a internet inteira indiscriminadamente.

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

Com os proxies confiáveis definidos como curinga, o XFF falsificado de qualquer um passa como o 'IP real'. Sem proxy na frente, 'confiar em nada' é o correto.

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

1

Higienize os cabeçalhos de IP na fronteira (curto prazo, imediato)

Antes de a lógica de proxy confiável rodar, divida o XFF, valide cada parte como "isto é um IP válido?", mantenha só IPs válidos; se nenhum, descarte o cabeçalho por completo. Os IPs reais dos usuários reais passam, então nenhum impacto; as strings de sondagem são descartadas.
2

Defina os proxies confiáveis corretamente (causa)

Largue o curinga. Sem proxy na frente → confie em nada. A confiança no proxy também afeta a detecção de http/https, então mudanças podem causar laços de redirecionamento — verifique em um ambiente primeiro.
3

Não confunda 'IP válido' com 'IP real'

Higienizar só corrige o formato. Um IP bem-formado ainda pode ser um valor forjado. A confiança no IP real vem apenas da config de proxy do passo 2.

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.

Entradas: XFF · User-Agent · Referer · … (infinitas)
↓ mas as que de fato são armazenadas são
Sink: só ip_address e user_agent na tabela de sessões
↓ então as entradas a higienizar são
só os cabeçalhos que alimentam essas duas (cabeçalhos de IP + User-Agent)
Bloquear cabeçalhos perigosos um a um só faz o ataque passar para outro cabeçalho. Trabalhe de trás para frente a partir de onde a entrada não confiável é armazenada no fim (o sink), e as entradas que você precisa tratar viram uma lista curta e fixa.

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

Um filtro de entrada (verificando valores recebidos) pode ser burlado por caminhos internos de resolução. A saída (logo antes da gravação no BD) é o único lugar por onde todo valor passa, então nada escapa.

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

FAQ

QPosso confiar no X-Forwarded-For?
A

Não como está. XFF é um cabeçalho que o cliente pode definir livremente — um atacante pode forjar um IP de aparência real ou uma string de sondagem de injeção. O único XFF confiável é um adicionado por um proxy que VOCÊ colocou na frente e confia explicitamente.

QE se eu não uso um proxy reverso ou CDN?
A

Não confie em XFF nenhum (deixe o padrão do framework). Se não há proxy na frente mas você 'confia em todos os proxies', qualquer um pode passar ao seu app um IP falso. Sem proxy no caminho → confiar em nada é a configuração correta.

QHigienizar o cabeçalho de IP basta?
A

Isso detém as sondagens de injeção e a enxurrada de erros, mas é só uma correção de curto prazo. Um 'IP de aparência válida' não é um 'IP real'. A confiança no IP real do cliente deve vir da configuração de proxy confiável, não da higienização.

QCorrigi o cabeçalho de IP mas o mesmo erro voltou. Por quê?
A

A mesma carga foi movida para outro cabeçalho (ex.: o cabeçalho User-Agent) e chegou à mesma coluna armazenada. Se você corrige cabeçalhos um a um, o atacante simplesmente passa para outro. Raciocine de trás para frente a partir de 'a qual coluna de BD / saída a entrada não confiável chega no fim?' e higienize juntas todas as entradas que a alimentam (cabeçalhos de IP + User-Agent etc.).