Guias de Segurança
O que é a falsificação de X-Forwarded-For (XFF) — a armadilha da configuração de proxy confiável
X-Forwarded-For (XFF) é um cabeçalho que os clientes podem forjar — scanners escondem sondagens de SQLi/XSS nele, e uma configuração de 'confiar em todos os proxies' deixa o valor falso chegar ao seu app. Um estudo de caso defensivo: por que a defesa em profundidade resistiu e a correção real de proxy confiável.
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 (deter pela defesa em profundidade)
- Como surgiu
- Requisições de ataque deram 500; uma enxurrada de e-mails de exceção
- Escudos que resistiram
- ① placeholders (valores vinculados) ② validação de charset do BD
- A lacuna deixada
- "confiar em todos os proxies" — sem proxy de fato na frente
- Resposta
- Higienizar cabeçalhos de IP na fronteira (patch) → corrigir a config de proxy confiável (raiz)
Primeiro, separe: "ataque" ou "erro meu"?
A regra é: 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 raciocinar de trás para frente a partir dos sintomas, mate hipóteses com 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 derrube-as uma a uma com dados, não com palpites — o erro de diagnóstico cai acentuadamente.
O que era o ataque: sondagens de injeção pegando carona em um XFF falsificado
O bot empacota uma string de sondagem no XFF "em vez de um IP", com só um IP de aparência normal no fim. O objetivo é 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 chega via XFF. Ele abusa do "lugar que carrega um IP" como um ponto de injeção de strings arbitrárias.
Por que o impacto foi zero: dois escudos
Escudo ① valores vinculados (placeholders)
O valor do IP foi passado via um placeholder como dado, não concatenado em string. A entrada não consegue cruzar para "comando", então o SQLi não pode se formar por design — um dos maiores benefícios de usar um framework.
Escudo ② 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 seria uma história bonitinha — 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, verificações de IP envenenados
✓ 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" é o atalho clássico de tempo de desenvolvimento que sobrevive até a produção.
Ponto cego: a falsificação de XFF não é só sobre injeção
Qualquer coisa que confie no IP do cliente é envenenada: 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 sólidas como rocha, 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 (patch, instantâneo)
Defina os proxies confiáveis corretamente (raiz)
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: bloqueie o cabeçalho de IP, seja atingido no User-Agent (a armadilha do bate-toupeira)
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 força uma reformulação.
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) deter os erros derivados de sessão por design — outros cabeçalhos não armazenados não podem ser usados como pivô. 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: raciocine de trás para frente a partir do sink (não sele entradas uma a uma)
Selar cabeçalhos um de cada vez é bate-toupeira — as entradas que um atacante pode usar são infinitas. 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 "bloqueie o IP, seja atingido no User-Agent" nos ensinou: quando você corrige um cabeçalho, sele 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: o filtro de entrada foi burlado — defenda na saída
Logo após selar 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 a produção continuava recorrendo. Este é o cerne.
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 contaminado ressurgiu como o "IP do cliente" por uma rota que não havíamos higienizado. O ponto de entrada não consegue pegar tudo.
✗ Defender na entrada
Higienizar cabeçalhos da requisição → o IP do cliente ressurge por outro caminho interno → burlado
✓ Defender na saída
Verificar logo antes da gravação no BD → o único gargalo por onde tudo passa → sem burla
Então invertemos 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. Martelando ambas as 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 última linha de defesa é a saída (ponto de uso)
A máxima 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 entrada e ressurgir. Validar/escapar logo antes de um sink perigoso — BD, HTML, um comando — é o gargalo por onde tudo passa, e essa é a última linha de defesa. Faça entrada e saída como duas coisas separadas.
A maior lição: ser ruidosamente rejeitado é mais seguro
Aqui o ataque falhou e foi tornado visível como uma enxurrada de notificações de erro. Ironicamente, isso não é ruim: ser ruidosamente rejeitado vence ser silenciosamente deixado passar. Não descarte as notificações de erro como "ruído" — use-as como um sensor de anomalia. Não pare em "bloqueamos o impacto"; corrija "por que chegou às entranhas". Essa é a postura deste site.
Leia a seguir
- Glossário: O que é injeção de SQL / O que é XSS / O que é SSRF
- Fundamentos: Fundamentos de segurança: .env e chaves de API
FAQ
QPosso confiar no X-Forwarded-For?
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?
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 alimentar seu app com um IP falso. Sem proxy no caminho → confiar em nada é a configuração correta.
QHigienizar o cabeçalho de IP basta?
Isso deter as sondagens de injeção e a enxurrada de erros, mas é higiene (estancar o sangramento). 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 mesma carga foi movida para outro ponto de entrada (ex.: o cabeçalho User-Agent) e chegou à mesma coluna armazenada. Corrigir cabeçalhos um a um é um jogo de bate-toupeira. 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.).