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 tamanho 0 seguido 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 ali0\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-Length precisa casar exatamente com a contagem de bytes do corpo — incluindo os \r\n (CRLF). No exemplo CL.TE, 0\r\n\r\nG são 6 bytes; no TE.CL, 8\r\n sã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-length mentiroso 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: chunked via HTTP/2; se o front-end não o rejeita (a RFC 9113 proíbe Transfer-Encoding em 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:H reflete 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-Length vs Transfer-Encoding (ou faz downgrade de HTTP/2 para HTTP/1.1 sem sanitizar).
  • Evidência sugerida: a requisição crua contrabandeada (com CL/TE destacados), 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-Encoding quebra 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 e TE/CL injetados via HTTP/2).
  • Front-end rejeita requisições com Content-Length e Transfer-Encoding simultâneos.
  • Front-end rejeita Transfer-Encoding ofuscado (espaços/tabs, valor inesperado, cabeçalho duplicado).
  • Back-end segue a RFC: TE tem precedência e requisição ambígua retorna 400 e 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.