
Riproduce il request smuggling aiohttp CWE-444 tramite upgrade WebSocket rifiutati, con payload Python/Rust e lab Docker che dimostra il bypass del controllo accessi del proxy.
Request smuggling tramite upgrade WebSocket rifiutato in aiohttp < 3.14.2.
Quando un reverse proxy inoltra le intestazioni Connection: Upgrade + Upgrade: websocket,
il parser aiohttp vulnerabile salta il corpo della richiesta e interpreta
i byte finali come una richiesta in pipeline — bypassando i controlli di accesso perimetrali.
Corretto in aiohttp 3.14.2 (commit 6ae358f).
Autore: João Victor Botelho (JV Botelho) — https://glitchedcat.com
Copia e incolla la versione Python (zero dipendenze, solo 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>
Oppure compila la versione Rust (zero dipendenze, solo 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>
I binari precompilati verranno pubblicati nella sezione Releases tramite il workflow di release.
Entrambi producono payload byte-identici (garantito da un test di parità CI). Se il lab è in esecuzione:
python3 poc.py nginx-upgrade 80 backend-vuln
Output atteso: 1 risposta HTTP (WebSocket upgrade rejected). Poi verifica:
# 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'
Un conteggio del backend > 0 e un conteggio di Nginx = 0 significano che lo split CWE-444 è confermato. Questo PoC invia un singolo segmento TCP — il corpo è la richiesta contrabbandata; Nginx lo tratta come corpo, aiohttp lo tratta come una seconda richiesta.
_http_parser.pyx restituisce 2 (salta il corpo) al rilevamento dell'upgrade prima che il corpo sia consumato (riga ~863). I byte del corpo rimangono in _message_tail e vengono reimmessi nel parser in web_protocol.py finish_response (riga ~771).location /admin { deny all; }, la /admin contrabbandata raggiunge comunque il backend perché le decisioni di routing di Nginx vengono prese solo sulla richiesta esterna.await request.read() restituisce 0 byte sulle richieste di upgrade in 3.14.1 — il corpo è trattenuto al di sotto del livello handler. Applica la patch, oppure rimuovi le intestazioni di upgrade sul proxy per le route che non devono cambiare protocollo.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
Servizi:
| Container | Scopo |
|---|---|
backend-vuln | aiohttp 3.14.1 (vulnerabile), READ_BODY=false |
backend-vuln-read | aiohttp 3.14.1, READ_BODY=true (dimostra che l'handler non può aiutare) |
backend-patched | aiohttp 3.14.2 (corretto) |
nginx-upgrade | Inoltra le intestazioni di upgrade (deny all su /admin) |
nginx-default | Nessun inoltro di upgrade (neutralizza il bug) |
nginx-strip | Rimozione Connection "" (neutralizza il bug) |
attacker | Binari Rust: reproduce, fase2, poc |
Risultati completi delle fasi 1-3 in findings/.
Connection: close al backend o rimuovono le intestazioni Connection/Upgrade non sono vulnerabili. La configurazione che espone lo split è lo snippet map WebSocket canonico dalla documentazione di proxy di Nginx./ws esterna. L'evidenza del contrabbando è nei log del backend, non nella risposta dell'attaccante — una primitiva cieca e unidirezionale in questa topologia. Altre topologie di proxy non sono state testate.Transfer-Encoding: chunked NON provoca contrabbando — la riga grezza della dimensione del chunk (es. 3e) arriva al parser come metodo non valido e la connessione muore. Attraverso Nginx funziona, ma tramite normalizzazione: Nginx de-chunka il corpo e inoltra un Content-Length sintetizzato, quindi il backend viene sfruttato attraverso lo stesso percorso CL. Il flag --chunked dimostra il percorso del proxy.WebSocketResponse. La maggior parte delle app che non usano WebSocket su una route rifiutano per impostazione predefinita (il framework restituisce 404 o passa al prossimo 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
Vedi findings/fase3-deteccao.md per l'analisi completa. Riepilogo, con le avvertenze che contano in produzione:
X-Forwarded-For/Proxy Protocol, oppure il logging di un ID di connessione upstream + sequenza richieste per connessione. Solo IP client + finestra temporale è debole (NAT, keep-alive, concorrenza).content_length vs byte da request.read()): scatta su 3.14.1, silenzioso su 3.14.2. Rileva, non mitiga. Limitane l'ambito (route candidate per upgrade, corpi piccoli, stati non-101) — la versione ingenua bufferizza ogni corpo in memoria.reqlen di edge oltre la baseline delle intestazioni sugli endpoint WebSocket — confidenza bassa da solo (cookie/JWT/intestazioni di tracing sono rumorosi), ma l'unico segnale edge quando il client non invia Content-Length (ingresso chunked).