
Reproduziert aiohttp CWE-444 Request-Smuggling über abgelehnte WebSocket-Upgrades, mit Python/Rust-Payloads und Docker-Lab zur Demonstration der Umgehung der Proxy-Zugriffskontrolle.
Request-Smuggling über abgelehntes WebSocket-Upgrade in aiohttp < 3.14.2.
Wenn ein Reverse-Proxy Connection: Upgrade + Upgrade: websocket-Header weiterleitet, überspringt der verwundbare aiohttp-Parser den Request-Body und interpretiert nachfolgende Bytes als gepipelinte Anforderung – und umgeht so Edge-Zugriffskontrollen.
Behoben in aiohttp 3.14.2 (Commit 6ae358f).
Autor: João Victor Botelho (JV Botelho) — https://glitchedcat.com
Python-Version kopieren und einfügen (keine Abhängigkeiten, nur Standardbibliothek):
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>
Oder die Rust-Version bauen (keine Abhängigkeiten, nur Standardbibliothek):
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>
Vorgefertigte Binärdateien werden über den Release-Workflow auf Releases veröffentlicht.
Beide erzeugen byte-identische Payloads (durch einen CI-Paritätstest erzwungen). Wenn das Labor läuft:
python3 poc.py nginx-upgrade 80 backend-vuln
Erwartete Ausgabe: 1 HTTP-Antwort (WebSocket upgrade rejected). Danach prüfen:
# 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'
Ein Backend-Zähler > 0 und ein Nginx-Zähler = 0 bedeutet, dass die CWE-444-Aufspaltung bestätigt ist. Dieser PoC sendet ein einzelnes TCP-Segment – der Body ist die geschmuggelte Anforderung; Nginx behandelt ihn als Body, aiohttp als zweite Anforderung.
_http_parser.pyx liefert bei Erkennung des Upgrades den Wert 2 (Body überspringen), bevor der Body verarbeitet wurde (Zeile ~863). Die Body-Bytes verbleiben in _message_tail und werden in web_protocol.py finish_response (Zeile ~771) dem Parser erneut zugeführt.location /admin { deny all; } konfiguriert hat, erreicht das geschmuggelte /admin trotzdem das Backend, weil Nginx seine Routing-Entscheidungen nur anhand der äußeren Anforderung trifft.await request.read() liefert bei Upgrade-Anforderungen in 3.14.1 0 Bytes zurück – der Body wird unterhalb der Handler-Ebene zurückgehalten. Patchen Sie, oder entfernen Sie die Upgrade-Header am Proxy auf Routen, die keine Protokollwechsel erlauben dürfen.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
Dienste:
| Container | Zweck |
|---|---|
backend-vuln | aiohttp 3.14.1 (verwundbar), READ_BODY=false |
backend-vuln-read | aiohttp 3.14.1, READ_BODY=true (beweist, dass der Handler nicht helfen kann) |
backend-patched | aiohttp 3.14.2 (behoben) |
nginx-upgrade | Leitet Upgrade-Header weiter (deny all auf /admin) |
nginx-default | Keine Upgrade-Weiterleitung (neutralisiert den Fehler) |
nginx-strip | Connection ""-Entfernung (neutralisiert den Fehler) |
attacker | Rust-Binärdateien: reproduce, fase2, poc |
Vollständige Erkenntnisse der Phasen 1-3 in findings/.
Connection: close an das Backend senden oder die Connection-/Upgrade-Header entfernen, sind nicht verwundbar. Die Konfiguration, die die Aufspaltung ermöglicht, ist das kanonische WebSocket-map-Snippet aus Nginxs eigener Proxying-Dokumentation./ws-Antwort. Belege für das Schmuggeln finden sich in den Backend-Logs, nicht in der Antwort an den Angreifer – eine blinde, einseitige Primitive in dieser Topologie. Andere Proxy-Topologien wurden nicht getestet.Transfer-Encoding: chunked NICHT – die rohe Chunk-Größenzeile (z. B. 3e) trifft als ungültige Methode auf den Parser und die Verbindung stirbt. Über Nginx funktioniert es, jedoch über Normalisierung: Nginx entfernt die Chunk-Kodierung des Bodys und leitet eine synthetisierte Content-Length weiter, sodass das Backend über denselben CL-Pfad ausgenutzt wird. Das --chunked-Flag demonstriert den Proxy-Pfad.WebSocketResponse zurückgeben. Die meisten Apps, die auf einer Route keine WebSockets verwenden, lehnen standardmäßig ab (das Framework gibt 404 zurück oder fällt auf den nächsten Handler zurück).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
Siehe findings/fase3-deteccao.md für die vollständige Analyse. Zusammenfassung, mit den Einschränkungen, die in der Produktion relevant sind:
X-Forwarded-For/Proxy Protocol oder das Protokollieren einer Upstream-Verbindungs-ID + der Anforderungssequenz pro Verbindung. Allein Client-IP + Zeitfenster ist schwach (NAT, Keep-alive, Nebenläufigkeit).content_length vs. Bytes aus request.read()): schlägt bei 3.14.1 an, bleibt bei 3.14.2 ruhig. Erkennt, entschärft aber nicht. Eingrenzen (Routen mit Upgrade-Kandidaten, kleine Bodies, Nicht-101-Status) – die naive Version puffert jeden Body im Speicher.reqlen über der Header-Basislinie an WebSocket-Endpunkten – allein betrachtet geringe Konfidenz (Cookies/JWT/Tracing-Header sind verrauscht), aber das einzige Edge-Signal, wenn der Client kein Content-Length sendet (Chunked-Eingang).