
Reproduce el contrabando de solicitudes CWE-444 de aiohttp mediante actualizaciones WebSocket rechazadas, con cargas útiles en Python/Rust y un laboratorio Docker que demuestra la omisión del control de acceso del proxy.
Contrabando de solicitudes mediante una actualización WebSocket rechazada en aiohttp < 3.14.2.
Cuando un proxy inverso reenvía las cabeceras Connection: Upgrade + Upgrade: websocket,
el analizador vulnerable de aiohttp omite el cuerpo de la solicitud e interpreta
los bytes finales como una solicitud canalizada (pipelining) — omitiendo los controles de acceso perimetrales.
Corregido en aiohttp 3.14.2 (commit 6ae358f).
Autor: João Victor Botelho (JV Botelho) — https://glitchedcat.com
Copia y pega la versión en Python (cero dependencias, 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>
O compila la versión en Rust (cero dependencias, 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>
Los binarios precompilados se publicarán en Releases mediante el flujo de trabajo de releases.
Ambos generan payloads byte-idénticos (garantizado por una prueba de paridad en CI). Si el laboratorio está en ejecución:
python3 poc.py nginx-upgrade 80 backend-vuln
Salida esperada: 1 respuesta HTTP (WebSocket upgrade rejected). Luego verifica:
# El backend procesó 2 solicitudes (/ws + /admin contrabandeado):
docker logs backend-vuln | grep -c '"path".*"/admin"'
# Nginx solo registró 1 solicitud (la /ws):
docker exec nginx-upgrade cat /logs/nginx-upgrade.access.log | grep -c '/admin'
Si el recuento del backend > 0 y el recuento de Nginx = 0, la división CWE-444 está confirmada. Este PoC envía un único segmento TCP — el cuerpo es la solicitud contrabandeada; Nginx lo trata como cuerpo, aiohttp lo trata como una segunda solicitud.
_http_parser.pyx de aiohttp 3.14.1 devuelve 2 (omitir cuerpo)
al detectar la actualización antes de que se consuma el cuerpo (línea ~863). Los bytes del cuerpo
permanecen en _message_tail y se devuelven al analizador en finish_response
de web_protocol.py (línea ~771).location /admin { deny all; },
el /admin contrabandeado aún llega al backend porque las decisiones de enrutamiento
de Nginx se toman solo sobre la solicitud externa.await request.read() devuelve 0 bytes en solicitudes
de actualización en 3.14.1 — el cuerpo se retiene por debajo de la capa del handler. Aplica el
parche, o elimina las cabeceras de actualización en el proxy en rutas que no deban cambiar
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
# Ejecuta el PoC:
docker compose run --rm --entrypoint /app/poc attacker nginx-upgrade 80 backend-vuln
Servicios:
Hallazgos completos de las Fases 1-3 en findings/.
Connection: close al backend o eliminan las cabeceras Connection/Upgrade
no son vulnerables. La configuración que expone la división es el
fragmento canónico map para WebSocket de la propia documentación de proxy de Nginx./ws. La evidencia del
contrabando está en los registros del backend, no en la respuesta del atacante — una primitiva
ciega y unidireccional en esta topología. No se probaron otras topologías de proxy.Transfer-Encoding: chunked NO genera contrabando — la línea cruda del tamaño de chunk
(p. ej. 3e) llega al analizador como un método inválido y la conexión muere.
A través de Nginx funciona, pero mediante normalización: Nginx elimina el chunked del cuerpo
y reenvía un Content-Length sintetizado, por lo que el backend se explota
a través de la misma vía CL. La bandera --chunked demuestra la vía del proxy.poc/ # Proyecto cargo en Rust
├── Cargo.toml
├── src/main.rs # Binario CLI
├── src/lib.rs # Librería + pruebas unitarias
├── tests/parity.rs # Prueba de paridad de payloads entre lenguajes
└── fuzz/ # Objetivos cargo-fuzz
poc.py # PoC en Python (copiar y pegar desde el blog)
attacker/ backend/ frontend/ # Servicios del laboratorio Docker
docker-compose.yml # Laboratorio de 7 servicios
findings/ # Notas de investigación (Fases 1-3)
.github/workflows/
├── ci.yml # Compilar, probar, clippy, paridad, integración, fuzz
└── release.yml # Compilación cruzada + GitHub Release
Consulta findings/fase3-deteccao.md para el análisis completo. Resumen, con las
advertencias que importan en producción:
X-Forwarded-For/Proxy Protocol, o registrar un
ID de conexión ascendente + la secuencia de solicitudes por conexión. La IP del cliente +
la ventana de tiempo por sí solas son débiles (NAT, keep-alive, concurrencia).content_length vs bytes de
request.read()): se activa en 3.14.1, silencioso en 3.14.2. Detecta, no
mitiga. Acótalo (rutas candidatas a upgrade, cuerpos pequeños, estados no-101) —
la versión ingenua almacena cada cuerpo en memoria.reqlen en el edge por encima de la línea base de cabeceras en endpoints WebSocket —
confianza baja por sí sola (cookies/JWT/cabeceras de tracing generan ruido), pero es la única
señal en el edge cuando el cliente no envía Content-Length (ingreso chunked).| Contenedor | Propósito |
|---|
backend-vuln | aiohttp 3.14.1 (vulnerable), READ_BODY=false |
backend-vuln-read | aiohttp 3.14.1, READ_BODY=true (demuestra que el handler no puede ayudar) |
backend-patched | aiohttp 3.14.2 (corregido) |
nginx-upgrade | Reenvía cabeceras de actualización (deny all en /admin) |
nginx-default | Sin reenvío de actualización (neutraliza el bug) |
nginx-strip | Eliminación Connection "" (neutraliza el bug) |
attacker | Binarios Rust: reproduce, fase2, poc |
WebSocketResponse