
Reproduit le smuggling de requêtes CWE-444 dans aiohttp via des mises à niveau WebSocket rejetées, avec des payloads Python/Rust et un laboratoire Docker démontrant le contournement du contrôle d'accès du proxy.
Contrebande de requêtes via une mise à niveau WebSocket rejetée dans aiohttp < 3.14.2.
Lorsqu'un proxy inverse transmet les en-têtes Connection: Upgrade + Upgrade: websocket,
l'analyseur aiohttp vulnérable ignore le corps de la requête et interprète
les octets suivants comme une requête pipelinée — contournant les contrôles d'accès en périphérie.
Corrigé dans aiohttp 3.14.2 (commit 6ae358f).
Auteur : João Victor Botelho (JV Botelho) — https://glitchedcat.com
Copiez-collez la version Python (zéro dépendance, uniquement la bibliothèque standard) :
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 compilez la version Rust (zéro dépendance, uniquement la bibliothèque standard) :
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>
Des binaires précompilés seront publiés dans Releases via le workflow de publication.
Les deux produisent des payloads octet pour octet identiques (garanti par un test de parité CI). Si le lab tourne :
python3 poc.py nginx-upgrade 80 backend-vuln
Sortie attendue : 1 réponse HTTP (WebSocket upgrade rejected). Ensuite, vérifiez :
# 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 compteur backend > 0 et un compteur Nginx = 0 signifient que la scission CWE-444 est confirmée. Ce PoC envoie un seul segment TCP — le corps est la requête introduite en contrebande ; Nginx le traite comme un corps, aiohttp le traite comme une seconde requête.
_http_parser.pyx retourne 2 (ignorer le corps)
lors de la détection de la mise à niveau avant que le corps ne soit consommé (ligne ~863). Les octets du corps
restent dans _message_tail et sont renvoyés à l'analyseur dans web_protocol.py
finish_response (ligne ~771).location /admin { deny all; },
le /admin introduit en contrebande atteint toujours le backend car les décisions
de routage de Nginx sont prises uniquement sur la requête externe.await request.read() retourne 0 octet sur les requêtes
de mise à niveau dans 3.14.1 — le corps est retenu sous la couche du gestionnaire. Corrigez,
ou supprimez les en-têtes de mise à niveau au niveau du proxy sur les routes qui ne doivent pas changer
de protocole.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
Services :
Résultats complets des phases 1 à 3 dans findings/.
Connection: close au backend ou suppriment les en-têtes Connection/Upgrade
ne sont pas vulnérables. La configuration qui expose la scission est
l'extrait map WebSocket canonique de la documentation de proxy de Nginx./ws. La preuve de la
contrebande se trouve dans les journaux du backend, pas dans la réponse à l'attaquant — une primitive
aveugle à sens unique dans cette topologie. D'autres topologies de proxy n'ont pas été testées.Transfer-Encoding: chunked ne fait PAS de contrebande — la ligne brute de taille de morceau
(par ex. 3e) atteint l'analyseur comme une méthode invalide et la connexion meurt.
Via Nginx, cela fonctionne, mais par normalisation : Nginx dé-chunke le corps
et transmet un Content-Length synthétisé, donc le backend est exploité
par le même chemin CL. L'option --chunked démontre le chemin via le proxy.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
Voir findings/fase3-deteccao.md pour l'analyse complète. Résumé, avec les
mises en garde qui comptent en production :
X-Forwarded-For/Proxy Protocol, ou de journaliser un
ID de connexion en amont + la séquence de requêtes par connexion. L'IP du client +
la fenêtre temporelle, à elles seules, constituent une preuve faible (NAT, keep-alive, concurrence).content_length vs octets de
request.read()) : se déclenche sur 3.14.1, silencieux sur 3.14.2. Détecte, ne
mitige pas. Limitez son périmètre (routes candidates à la mise à niveau, petits corps, statuts non-101) —
la version naïve met en mémoire tampon chaque corps.reqlen de périphérie au-dessus de la ligne de base des en-têtes sur les points de terminaison WebSocket —
faible confiance à elle seule (cookies/JWT/en-têtes de traçage sont bruyants), mais le seul
signal périphérique lorsque le client n'envoie pas de Content-Length (entrée chunked).| Container | Rôle |
|---|
backend-vuln | aiohttp 3.14.1 (vulnérable), READ_BODY=false |
backend-vuln-read | aiohttp 3.14.1, READ_BODY=true (prouve que le gestionnaire ne peut pas aider) |
backend-patched | aiohttp 3.14.2 (corrigé) |
nginx-upgrade | Transmet les en-têtes de mise à niveau (deny all sur /admin) |
nginx-default | Aucune transmission de mise à niveau (neutralise le bug) |
nginx-strip | Suppression de Connection "" (neutralise le bug) |
attacker | Binaires Rust : reproduce, fase2, poc |
WebSocketResponse