Cómo proteger sus conexiones WebSocket

La Web está creciendo a un ritmo enorme. Cada vez más aplicaciones web son dinámicas, inmersivas y no requieren que el usuario final se actualice. Existe un soporte emergente para tecnologías de comunicación de baja latencia como websockets. Los Websockets nos permiten lograr una comunicación en tiempo real entre diferentes clientes conectados a un servidor.

Mucha gente no sabe cómo proteger sus websockets contra algunos ataques muy comunes. Veamos cuáles son y qué debe hacer para proteger sus websockets.

# 0: habilitar CORS

WebSocket no viene con CORS incorporado. Dicho esto, significa que cualquier sitio web puede conectarse a la conexión websocket de cualquier otro sitio web y comunicarse sin ninguna restricción. No voy a explicar las razones por las que es así, pero una solución rápida es verificar el Originencabezado en el protocolo de enlace de websocket.

Claro, un atacante puede falsificar el encabezado Origin, pero no importa, porque para explotarlo, el atacante necesita falsificar el encabezado Origin en el navegador de la víctima, y ​​los navegadores modernos no permiten que el javascript normal que se encuentra en los navegadores web cambie el encabezado Origin .

Además, si realmente está autenticando a los usuarios utilizando, preferiblemente, cookies, entonces esto no es realmente un problema para usted (más sobre esto en el punto # 4)

# 1: Implementar limitación de velocidad

La limitación de la frecuencia es importante. Sin él, los clientes pueden, a sabiendas o sin saberlo, realizar un ataque DoS en su servidor. DoS significa denegación de servicio. DoS significa que un solo cliente mantiene al servidor tan ocupado que el servidor no puede manejar a otros clientes.

En la mayoría de los casos, es un intento deliberado de un atacante de derribar un servidor. A veces, las implementaciones deficientes de frontend también pueden provocar DoS por parte de clientes normales.

Vamos a hacer uso del algoritmo de depósito con fugas (que aparentemente es un algoritmo muy común que las redes implementan) para implementar la limitación de velocidad en nuestros websockets.

La idea es que tenga un balde que tenga un orificio de tamaño fijo en el piso. Empiezas a ponerle agua y el agua sale por el agujero en la parte inferior. Ahora, si su tasa de poner agua en el balde es mayor que la tasa de fluir fuera del agujero durante mucho tiempo, en algún momento, el balde se llenará y comenzará a gotear. Eso es todo.

Ahora entendamos cómo se relaciona con nuestro websocket:

  1. El agua es el tráfico websocket enviado por el usuario.
  2. El agua pasa por el agujero. Esto significa que el servidor procesó con éxito esa solicitud de websocket en particular.
  3. El agua que todavía está en el balde y no se ha desbordado es básicamente tráfico pendiente. El servidor procesará este tráfico más adelante. Esto también podría ser un flujo de tráfico en ráfagas (es decir, demasiado tráfico durante un tiempo muy corto está bien siempre que el cubo no gotee)
  4. El agua que se desborda es el tráfico descartado por el servidor (demasiado tráfico procedente de un solo usuario)

El punto aquí es que debe verificar la actividad de su websocket y determinar estos números. Vas a asignar un depósito a cada usuario. Decidimos qué tan grande debe ser el depósito (tráfico que un solo usuario podría enviar durante un período fijo) dependiendo de qué tan grande sea su agujero (cuánto tiempo en promedio necesita su servidor para procesar una sola solicitud de websocket, digamos guardar un mensaje enviado por un usuario en una base de datos).

Esta es una implementación reducida que estoy usando en codedamn para implementar el algoritmo de depósito con fugas para los websockets. Está en NodeJS pero el concepto sigue siendo el mismo.

if(this.limitCounter >= Socket.limit) { if(this.burstCounter >= Socket.burst) { return 'Bucket is leaking' } ++this.burstCounter return setTimeout(() => { this.verify(callingMethod, ...args) setTimeout(_ => --this.burstCounter, Socket.burstTime) }, Socket.burstDelay) } ++this.limitCounter

Entonces, ¿qué está pasando aquí? Básicamente, si se cruza el límite y el límite de ráfaga (que son constantes establecidas), la conexión de websocket se cae. De lo contrario, después de un retraso particular, reiniciaremos el contador de ráfagas. Esto deja espacio nuevamente para otra ráfaga.

# 2: restringir el tamaño de la carga útil

Esto debe implementarse como una característica dentro de su biblioteca de websocket del lado del servidor. Si no es así, ¡es hora de cambiarlo por uno mejor! Debe limitar la longitud máxima del mensaje que se puede enviar a través de su websocket. Teóricamente no hay límite. Por supuesto, es muy probable que obtener una gran carga útil bloquee esa instancia de socket en particular y consuma más recursos del sistema de los necesarios.

Por ejemplo, si está usando la biblioteca WS para Node para crear websockets en el servidor, puede usar la opción maxPayload para especificar el tamaño máximo de carga útil en bytes. Si el tamaño de la carga útil es mayor que eso, la biblioteca descartará la conexión de forma nativa.

No intente implementar esto por su cuenta determinando la longitud del mensaje. No queremos leer primero todo el mensaje en la RAM del sistema. Si es incluso 1 byte mayor que nuestro límite establecido, elimínelo. Eso solo podría ser implementado por la biblioteca (que maneja los mensajes como un flujo de bytes en lugar de cadenas fijas).

# 3: cree un protocolo de comunicación sólido

Debido a que ahora está en una conexión dúplex, podría enviar cualquier cosa al servidor. El servidor puede enviar cualquier mensaje de texto al cliente. Necesitaría tener una forma de comunicación efectiva entre ambos.

No puede enviar mensajes sin procesar si desea escalar el aspecto de mensajería de su sitio web. Prefiero usar JSON, pero hay otras formas optimizadas de configurar una comunicación. Sin embargo, considerando JSON, así es como se vería un esquema de mensajería básico para un sitio genérico:

Client to Server (or vice versa):  status: "ok"

Ahora es más fácil para usted en el servidor verificar eventos y formatos válidos. Desconecte la conexión inmediatamente y registre la dirección IP del usuario si el formato del mensaje difiere. No hay forma de que el formato cambie a menos que alguien esté hormigueando manualmente con su conexión websocket. Si está en el nodo, le recomiendo usar la biblioteca Joi para una mayor validación de los datos entrantes del usuario.

# 4: autentique a los usuarios antes de que se establezca la conexión WS

Si está utilizando websockets para usuarios autenticados, es una buena idea permitir que solo los usuarios autenticados establezcan una conexión websocket exitosa. No permita que nadie establezca una conexión y luego espere a que se autentique a través del propio websocket. En primer lugar, establecer una conexión websocket es un poco caro de todos modos. Por lo tanto, no desea que personas no autorizadas accedan a sus websockets y acaparen conexiones que podrían ser utilizadas por otras personas.

Para hacer esto, cuando esté estableciendo una conexión en la interfaz, pase algunos datos de autenticación a websocket. Podría ser un encabezado como X-Auth-Token:. De forma predeterminada, las cookies se pasarían de todos modos.

Una vez más, todo se reduce a la biblioteca que está utilizando en el servidor para implementar websockets. Pero si está en Node y usa WS, existe esta función verifyClient que le da acceso al objeto de información que se pasa a una conexión websocket. (Al igual que tiene acceso al objeto req para solicitudes HTTP).

# 5: use SSL sobre websockets

Esto es una obviedad, pero aún hay que decirlo. Utilice wss: // en lugar de ws: //. Esto agrega una capa de seguridad sobre su comunicación. Utilice un servidor como Nginx para el proxy inverso de websockets y habilite SSL sobre ellos. Configurar Nginx sería otro tutorial. Dejaré la directiva que necesita usar para Nginx para aquellas personas que estén familiarizadas con él. Más info aquí.

location /your-websocket-location/ { proxy_pass ​//127.0.0.1:1337; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; }

Aquí se asume que su servidor websocket está escuchando en el puerto 1337 y sus usuarios se conectan a su websocket de esta manera:

const ws = new WebSocket('wss://yoursite.com/your-websocket-location')

Preguntas?

Have any questions or suggestions? Ask away!