HTTP Request Smuggling (contrabando de requisições HTTP) explora um desacordo sutil entre duas máquinas que processam a mesma requisição: o front-end (um proxy reverso, CDN ou load balancer) e o back-end (o servidor de aplicação atrás dele). Quando os dois discordam sobre onde uma requisição termina e a próxima começa, o atacante consegue anexar — contrabandear — um prefixo de uma segunda requisição que o front-end considera parte do corpo da primeira, mas que o back-end interpreta como o início de uma requisição independente.
Esse prefixo contrabandeado fica “esperando” na conexão TCP reusada entre front-end e back-end e se cola na próxima requisição que passar por aquela conexão — frequentemente a de outro usuário. O resultado é uma das classes de falha mais impactantes da web moderna: burlar controles de segurança do front-end, envenenar caches, capturar requisições e cookies de vítimas e sequestrar sessões — tudo sem tocar diretamente no navegador delas.
A anatomia do ataque
O HTTP/1.1 oferece duas formas de informar o tamanho do corpo de uma requisição, e essa redundância é a raiz do problema:
Content-Length(CL): diz quantos bytes tem o corpo. Ex.:Content-Length: 13.Transfer-Encoding: chunked(TE): o corpo vem em blocos (chunks), cada um prefixado pelo seu tamanho em hexadecimal, terminando com um bloco de tamanho0seguido de uma linha em branco (0\r\n\r\n).
A especificação é clara: se ambos aparecem na mesma requisição, o Transfer-Encoding tem precedência e o Content-Length deve ser ignorado — e o servidor pode (segundo a RFC 9112, que obsoletou a RFC 7230 em 2022, deveria) tratar a mensagem como inválida e respondê-la com 400, fechando a conexão. O problema é que nem todo servidor segue a regra. Quando o front-end usa um cabeçalho e o back-end usa o outro, eles dessincronizam sobre o limite da mensagem.
CL.TE — front-end usa Content-Length, back-end usa Transfer-Encoding
Aqui o front-end respeita o Content-Length e encaminha a mensagem inteira; o back-end respeita o Transfer-Encoding: chunked e para de ler no chunk 0, deixando o resto “órfão” na conexão:
POST / HTTP/1.1
Host: app.exemplo.com
Content-Length: 6
Transfer-Encoding: chunked
0
G
O front-end lê os 6 bytes anunciados em Content-Length (0, CRLF, CRLF, G = 6 bytes) e repassa tudo. O back-end lê o chunk 0 e considera a requisição encerrada ali — 0\r\n\r\n. O byte restante (G) fica no buffer da conexão e servirá de prefixo da próxima requisição que chegar do front-end. Quando outro usuário fizer um POST / HTTP/1.1, ele será concatenado com o nosso prefixo órfão, virando GPOST / HTTP/1.1 — uma requisição Frankenstein. Em um ataque real, esse prefixo é maior: contrabanda-se o início de uma requisição inteira (uma rota, cabeçalhos), não apenas um byte.
TE.CL — front-end usa Transfer-Encoding, back-end usa Content-Length
O espelho do caso anterior. O front-end processa os chunks; o back-end olha apenas o Content-Length e lê só os primeiros bytes, deixando o restante do chunk como prefixo contrabandeado:
POST / HTTP/1.1
Host: app.exemplo.com
Content-Length: 3
Transfer-Encoding: chunked
8
SMUGGLED
0
O front-end (chunked) lê o chunk de tamanho 8 (SMUGGLED) e o terminador 0, consumindo todo o corpo. O back-end (CL) lê apenas 3 bytes (8\r\n) e considera a requisição terminada — o SMUGGLED\r\n0\r\n\r\n vira o prefixo da requisição seguinte.
Nota didática: nos exemplos acima, o valor declarado em
Content-Lengthprecisa casar exatamente com a contagem de bytes do corpo — incluindo os\r\n(CRLF). No exemplo CL.TE,0\r\n\r\nGsão 6 bytes; no TE.CL,8\r\nsão 3 bytes. Por isso o ajuste fino do CL é a parte mais delicada de reproduzir um smuggling na prática.
O ponto central, em qualquer variante: uma única conexão TCP entre front-end e back-end é reusada para servir vários usuários, e o prefixo órfão que deixamos para trás envenena a requisição de quem vier depois.
Variações e bypasses
TE.TE — ambos suportam chunked, mas um é enganado por ofuscação
Quando os dois servidores entendem Transfer-Encoding, ainda dá para dessincronizá-los fazendo um deles deixar de reconhecer o cabeçalho. A técnica é ofuscar o Transfer-Encoding de um jeito que um servidor aceita e o outro descarta. Alguns exemplos de ofuscação (o \r/\t representam bytes reais — CR e TAB):
Transfer-Encoding: chunked
Transfer-Encoding: identity
Um segundo cabeçalho com valor diferente faz alguns parsers honrarem o último (identity, sem chunked) e outros o primeiro. Outras variantes comuns, byte a byte:
Transfer-Encoding:[tab]chunked # tab no lugar do espaço após os dois-pontos
Transfer-Encoding[espaço]: chunked # espaço antes dos dois-pontos
Transfer-Encoding: chunked[CR] # CR isolado antes do CRLF de fim de linha
Se o front-end ignora o cabeçalho ofuscado (caindo de volta no Content-Length) e o back-end o aceita — ou vice-versa — temos uma TE.TE explorável. O catálogo de ofuscações é grande porque cada parser HTTP tem suas próprias frouxidões; a extensão HTTP Request Smuggler já carrega dezenas delas.
CL.0 — quando o Content-Length é tratado como zero
Em endpoints que não esperam corpo (ex.: um GET ou um recurso estático servido diretamente pelo back-end), alguns back-ends ignoram o Content-Length e tratam o corpo como inexistente, enquanto o front-end o respeita e encaminha. O corpo inteiro “vaza” para a próxima requisição:
POST /recurso-estatico HTTP/1.1
Host: app.exemplo.com
Content-Length: 44
GET /admin HTTP/1.1
Host: app.exemplo.com
O corpo declarado (GET /admin HTTP/1.1\r\nHost: app.exemplo.com\r\n) tem exatamente 44 bytes. O back-end serve /recurso-estatico ignorando o corpo, e o GET /admin... se torna o prefixo da próxima requisição na conexão.
Desync por downgrade de HTTP/2 (H2.CL / H2.TE)
Esse é o vetor mais relevante hoje. O HTTP/2 não usa cabeçalhos textuais para delimitar mensagens — o comprimento do corpo está embutido nos frames (DATA frames com tamanho explícito), o que torna o smuggling clássico impossível dentro do próprio HTTP/2. O perigo aparece quando o front-end fala HTTP/2 com o navegador mas faz downgrade para HTTP/1.1 ao falar com o back-end. Nesse momento, ele reconstrói a mensagem HTTP/1.1 — incluindo Content-Length/Transfer-Encoding — a partir do que recebeu via HTTP/2, e se não sanitizar, reintroduz a ambiguidade:
- H2.CL: injetamos um
content-lengthmentiroso como cabeçalho HTTP/2; se o front-end o copia para a requisição HTTP/1.1 traduzida em vez de recalcular o tamanho real, o back-end dessincroniza. - H2.TE: injetamos
transfer-encoding: chunkedvia HTTP/2; se o front-end não o rejeita (a RFC 9113 proíbeTransfer-Encodingem HTTP/2, mas nem todo front-end valida), ele aparece na mensagem HTTP/1.1 traduzida e o back-end passa a usar chunked.
Vetores correlatos do downgrade incluem CRLF injection dentro de valores de cabeçalho/pseudo-cabeçalho HTTP/2 (que viram quebras de linha reais na tradução para HTTP/1.1) e request splitting, permitindo desync mesmo contra back-ends que pareciam imunes. A lição: o downgrade é onde as garantias do HTTP/2 morrem.
Como exploramos no pentest
Alerta de escopo. Detecção de request smuggling é intrusiva por natureza: um prefixo contrabandeado pode se colar à requisição de um usuário real e quebrar a sessão dele, corromper respostas ou poluir o cache de produção. Só execute com autorização explícita por escrito, preferencialmente em ambiente de homologação, fora de horário de pico, e nunca deixe prefixos “armados” pendurados na conexão.
O fluxo que usamos em campo:
1. Detecção por timing (atraso induzido). A técnica mais segura para confirmar a dessincronização sem afetar terceiros. Enviamos uma requisição malformada que, se houver desync, faz o servidor ficar esperando bytes que nunca chegam — gerando um atraso mensurável. Exemplo de probe CL.TE: declaramos Content-Length maior do que o corpo enviado; o front-end, lendo chunked, encerra a requisição no chunk 0, mas o back-end, lendo CL, fica bloqueado aguardando os bytes faltantes até o timeout. Sem desync, a resposta volta normal; com desync, ela trava. Diferença de tempo = sinal. (A direção exata da probe depende de qual lado lê CL e qual lê TE; a HTTP Request Smuggler testa ambas.)
2. Confirmação por resposta diferencial. Enviamos duas requisições em sequência: a primeira contrabandeia um prefixo que deveria alterar a resposta da segunda (ex.: forçar um 404 ou um redirecionamento). Se a segunda requisição volta diferente do esperado, confirmamos que o prefixo foi injetado. Bem conduzida, essa abordagem afeta apenas as nossas próprias requisições subsequentes (enviadas na mesma conexão, em rápida sucessão), o que a torna mais controlada que esperar a vítima — embora ainda haja risco residual de a janela “vazar” para outro usuário.
3. Ferramentas. Na prática, automatizamos com:
- HTTP Request Smuggler (extensão do Burp Suite, de James Kettle/PortSwigger): faz a varredura de timing, testa a matriz CL.TE / TE.CL / TE.TE e as ofuscações automaticamente, e ajuda a acertar o Content-Length exato.
- Turbo Intruder: para disparar as requisições com o controle fino de conexão e concorrência que o smuggling exige (manter a conexão aberta, sincronizar o envio, agrupar requisições no mesmo pacote).
- Param Miner: útil para descobrir cabeçalhos e comportamentos de cache que potencializam o web cache poisoning derivado do desync.
4. Escalada. Confirmado o desync, medimos impacto real: contrabandear uma requisição para uma rota administrativa burlando o controle de acesso do front-end (o front-end nunca “vê” essa rota, então não aplica suas regras); capturar a requisição de outro usuário fazendo o back-end refletir o corpo da vítima de volta para nós (vazando cookies e tokens em um campo controlado, como um parâmetro de busca ou um comentário armazenado); ou envenenar o cache para servir conteúdo malicioso a todos que pedirem aquele recurso.
Resumo para o relatório
- Impacto: burla dos controles de segurança do front-end (acesso a rotas restritas), captura de requisições/cookies de outros usuários, sequestro de sessão e envenenamento de cache afetando toda a base de usuários.
- Severidade: Alta a Crítica. Exemplo de vetor CVSS v3.1
AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:L= 8.9 (escopo alterado por afetar requisições de terceiros;AC:Hreflete o ajuste fino de timing/Content-Length necessário). A pontuação exata varia conforme o impacto comprovado.- Pré-condições: arquitetura com front-end e back-end que reusa conexões TCP e discorda sobre
Content-LengthvsTransfer-Encoding(ou faz downgrade de HTTP/2 para HTTP/1.1 sem sanitizar).- Evidência sugerida: a requisição crua contrabandeada (com
CL/TEdestacados), o atraso medido na prova por timing, o par requisição→resposta diferencial mostrando a injeção, e — quando aplicável — a resposta de uma vítima/cache envenenado capturada (com dados sensíveis redigidos).
Como mitigar
A correção não é um patch de aplicação — é arquitetural. O objetivo é eliminar a ambiguidade na fronteira da mensagem e a reutilização de conexão envenenável.
1. HTTP/2 fim a fim (a defesa que resolve a raiz)
Se o front-end fala HTTP/2 com o cliente e HTTP/2 com o back-end, não há reconstrução de cabeçalhos CL/TE e o comprimento vem nos frames — o smuggling clássico e o de downgrade desaparecem. Evite o downgrade para HTTP/1.1 no salto interno sempre que possível. Quando o downgrade for inevitável, o front-end deve sanitizar rigorosamente os cabeçalhos traduzidos: recalcular o Content-Length a partir do corpo real, rejeitar valores que contenham CR/LF e descartar qualquer transfer-encoding/content-length injetado pelo cliente via HTTP/2.
2. Rejeitar mensagens ambíguas no front-end
O front-end é a linha de frente: ele deve normalizar ou recusar qualquer requisição que apresente Content-Length e Transfer-Encoding juntos, ou um Transfer-Encoding ofuscado. Configuração ilustrativa (NGINX como exemplo de endurecimento):
# Errado: encaminhar a requisição como veio, com CL e TE conflitantes
# location / { proxy_pass http://backend; }
# Correto: rejeitar requisições ambíguas antes de repassar ao back-end
server {
# Versões modernas do NGINX já normalizam a maioria desses casos;
# reforce a política explicitamente:
underscores_in_headers off; # não aceitar headers com '_' ambíguos
location / {
# Política estrita: este caminho não aceita corpo chunked.
# Bloqueia qualquer requisição que traga Transfer-Encoding.
if ($http_transfer_encoding) {
return 400;
}
proxy_http_version 1.1;
proxy_pass http://backend;
}
}
O exemplo é didático e estrito (recusar todo
Transfer-Encodingquebra clientes legítimos que dependem de chunked, então valide a regra para o seu tráfego). A política exata depende do produto (NGINX, HAProxy, Envoy, CDN). O princípio é universal — front-end e back-end precisam concordar sobre como delimitar o corpo, e o front-end deve recusar o que for ambíguo em vez de “tentar adivinhar”.
3. Garantir consistência de parsing entre as camadas
Idealmente, front-end e back-end usam a mesma implementação de servidor (ou versões com comportamento de parsing idêntico e estrito). Um back-end que segue a RFC à risca — Transfer-Encoding tem precedência e requisição com CL+TE é rejeitada com 400 e a conexão fechada — fecha as variantes CL.TE/TE.CL. Em aplicação, prefira frameworks/servidores que rejeitam corpos ambíguos a frameworks tolerantes.
# Exemplo conceitual de validação estrita no back-end (defensivo).
# Premissa: 'headers' já normaliza nomes para minúsculas e detecta duplicatas.
def validar_delimitadores(headers):
tem_cl = "content-length" in headers
tem_te = "transfer-encoding" in headers
if tem_cl and tem_te:
raise BadRequest(400) # ambíguo: rejeitar, não adivinhar
if tem_te and headers["transfer-encoding"].strip().lower() != "chunked":
raise BadRequest(400) # TE com valor ofuscado/desconhecido
# Atenção: cabeçalhos DUPLICADOS (dois Content-Length ou dois
# Transfer-Encoding) também devem ser rejeitados antes deste ponto.
4. Não reusar conexões TCP envenenáveis com o back-end
Se o front-end não reutiliza a conexão com o back-end entre requisições de usuários diferentes (ou abre uma conexão por requisição), o prefixo órfão não tem onde “esperar” pela vítima. É um custo de desempenho real, mas é uma camada de contenção poderosa em arquiteturas que não conseguem migrar para HTTP/2 fim a fim.
5. Manter tudo atualizado
A maioria das variantes de ofuscação foi corrigida em versões recentes de proxies, CDNs e servidores de aplicação. Atualize front-ends (NGINX, HAProxy, Envoy, CDN) e back-ends regularmente — boa parte do smuggling explorado em campo vive de componentes desatualizados.
Checklist de mitigação
- HTTP/2 fim a fim; evitar downgrade para HTTP/1.1 no salto interno.
- Se houver downgrade, sanitizar os cabeçalhos traduzidos (recalcular
CL, rejeitar CR/LF eTE/CLinjetados via HTTP/2). - Front-end rejeita requisições com
Content-LengtheTransfer-Encodingsimultâneos. - Front-end rejeita
Transfer-Encodingofuscado (espaços/tabs, valor inesperado, cabeçalho duplicado). - Back-end segue a RFC:
TEtem precedência e requisição ambígua retorna400e fecha a conexão. - Parsing consistente entre front-end e back-end (mesma implementação/versão quando possível).
- Não reusar conexões TCP envenenáveis com o back-end (ou isolar por usuário).
- Proxies, CDNs e servidores de aplicação atualizados.
- Monitorar respostas anômalas e quebras de sessão que indiquem desync em produção.
Request smuggling é o que acontece quando duas máquinas leem a mesma frase e param em pontos diferentes. Toda a sofisticação do ataque se resume a explorar essa discordância de pontuação entre o front-end e o back-end — e toda a defesa madura se resume a garantir que ambos leiam a requisição exatamente do mesmo jeito, ou recusem o que for ambíguo.