
Reproduz contrabando de requisições (CWE-444) em aiohttp por meio de upgrades WebSocket rejeitados, com payloads em Python/Rust e laboratório Docker demonstrando bypass do controle de acesso de proxy.
Smuggling de requisições por meio de upgrade WebSocket rejeitado no aiohttp < 3.14.2.
Quando um proxy reverso encaminha os cabeçalhos Connection: Upgrade + Upgrade: websocket,
o parser vulnerável do aiohttp ignora o corpo da requisição e interpreta
os bytes restantes como uma requisição em pipeline — contornando os controles de acesso de borda.
Corrigido no aiohttp 3.14.2 (commit 6ae358f).
Autor: João Victor Botelho (JV Botelho) — https://glitchedcat.com
Copie e cole a versão Python (zero dependências, apenas stdlib):
curl -O https://raw.githubusercontent.com/JVBotelho/cve-2026-69243-poc-aiohttp-smuggling/main/poc.py
python3 poc.py <proxy-host> <proxy-port> <backend-host>
Ou compile a versão Rust (zero dependências, apenas std):
git clone https://github.com/JVBotelho/cve-2026-69243-poc-aiohttp-smuggling
cd cve-2026-69243-poc-aiohttp-smuggling/poc
cargo build --release
./target/release/cve-2026-69243-poc <proxy-host> <proxy-port> <backend-host>
Binários pré-compilados serão publicados em Releases por meio do fluxo de trabalho de release.
Ambos produzem payloads byte-idênticos (garantido por um teste de paridade na CI). Se o laboratório estiver em execução:
python3 poc.py nginx-upgrade 80 backend-vuln
Saída esperada: 1 resposta HTTP (WebSocket upgrade rejected). Em seguida, verifique:
# Backend processed 2 requests (/ws + smuggled /admin):
docker logs backend-vuln | grep -c '"path".*"/admin"'
# Nginx only logged 1 request (the /ws):
docker exec nginx-upgrade cat /logs/nginx-upgrade.access.log | grep -c '/admin'
A contagem no backend > 0 e a contagem no Nginx = 0 significa que a divisão CWE-444 foi confirmada. Este PoC envia um único segmento TCP — o corpo é a requisição contrabandeada; o Nginx o trata como corpo, o aiohttp o trata como uma segunda requisição.
_http_parser.pyx retorna 2 (pular corpo)
na detecção de upgrade antes que o corpo seja consumido (linha ~863). Os bytes do corpo
permanecem em _message_tail e são realimentados ao parser em web_protocol.py
finish_response (linha ~771).location /admin { deny all; },
o /admin contrabandeado ainda chega ao backend porque as decisões de roteamento
do Nginx são tomadas apenas com base na requisição externa.await request.read() retorna 0 bytes
em requisições de upgrade no 3.14.1 — o corpo é retido abaixo da camada do handler.
Aplique o patch ou remova os cabeçalhos de upgrade no proxy em rotas que não devem
trocar de protocolo.git clone https://github.com/JVBotelho/cve-2026-69243-poc-aiohttp-smuggling
cd cve-2026-69243-poc-aiohttp-smuggling
docker compose up -d
# Run the PoC:
docker compose run --rm --entrypoint /app/poc attacker nginx-upgrade 80 backend-vuln
Serviços:
| Container | Finalidade |
|---|---|
backend-vuln | aiohttp 3.14.1 (vulnerável), READ_BODY=false |
backend-vuln-read | aiohttp 3.14.1, READ_BODY=true (prova que o handler não resolve) |
backend-patched | aiohttp 3.14.2 (corrigido) |
nginx-upgrade | Encaminha cabeçalhos de upgrade (deny all em /admin) |
nginx-default | Sem encaminhamento de upgrade (neutraliza o bug) |
nginx-strip | Remove Connection "" (neutraliza o bug) |
attacker | Binários Rust: reproduce, fase2, poc |
Resultados completos das Fases 1 a 3 em findings/.
Connection: close ao backend ou removem os cabeçalhos Connection/Upgrade
não são vulneráveis. A configuração que expõe a divisão é o trecho canônico
map para WebSocket da própria documentação de proxy do Nginx./ws. A evidência do
contrabando está nos logs do backend, não na resposta ao atacante — uma primitiva
cega e unidirecional nessa topologia. Outras topologias de proxy não foram testadas.Transfer-Encoding: chunked NÃO contrabandeia — a linha bruta de tamanho de chunk
(ex.: 3e) chega ao parser como um método inválido e a conexão morre.
Através do Nginx funciona, mas por normalização: o Nginx remove o chunking do corpo
e encaminha um Content-Length sintetizado, então o backend é explorado
pelo mesmo caminho CL. A flag --chunked demonstra o caminho via proxy.WebSocketResponse. A maioria dos apps que não usa
WebSockets em uma rota rejeita por padrão (o framework retorna 404
ou passa para o próximo handler).poc/ # Rust cargo project
├── Cargo.toml
├── src/main.rs # CLI binary
├── src/lib.rs # Library + unit tests
├── tests/parity.rs # Cross-language payload parity test
└── fuzz/ # cargo-fuzz targets
poc.py # Python PoC (copy-paste from blog)
attacker/ backend/ frontend/ # Docker lab services
docker-compose.yml # 7-service lab
findings/ # Research notes (Phase 1-3)
.github/workflows/
├── ci.yml # Build, test, clippy, parity, integration, fuzz
└── release.yml # Cross-compile + GitHub Release
Consulte findings/fase3-deteccao.md para a análise completa. Resumo, com as
ressalvas que importam em produção:
X-Forwarded-For/Proxy Protocol, ou registrar um
ID de conexão upstream + a sequência de requisições por conexão. Apenas IP do cliente +
janela de tempo é fraco (NAT, keep-alive, concorrência).content_length vs bytes de
request.read()): dispara no 3.14.1, fica silencioso no 3.14.2. Detecta, não
mitiga. Defina o escopo (rotas candidatas a upgrade, corpos pequenos, status não-101) —
a versão ingênua armazena todo corpo em memória.reqlen de borda acima da linha de base de cabeçalhos em endpoints WebSocket —
baixa confiança isoladamente (cookies/JWT/cabeçalhos de rastreamento geram ruído), mas o único
sinal de borda quando o cliente não envia Content-Length (entrada chunked).