Cómo crear un servidor WebSocket seguro en Python
Kingsley Ubah
5 de julio de 2022
0 minutos de lecturaPor lo general, cuando una aplicación web necesita algo de un servidor externo, el cliente envía una solicitud a ese servidor, el servidor responde y luego se cierra la conexión.
Pensemos en una aplicación web que muestra precios de acciones. Para ofrecer los precios más recientes, el cliente debe solicitar actualizaciones al servidor una y otra vez. Este método es ineficiente, ya que interrumpe constantemente la conexión HTTP, y tampoco es ideal porque el precio de una acción en tiempo real almacenado en la base de datos podría cambiar mientras la aplicación sigue mostrando el precio anterior.
Sin embargo, con la comunicación bidireccional, el servidor puede enviar nuevos precios de acciones al cliente por iniciativa propia cada vez que se actualiza la información. Los ingenieros crearon el protocolo WebSocket para permitir este tipo de conexión persistente y evitar los problemas asociados con los métodos anteriores de long polling.
Una opción popular para implementar WebSockets es la biblioteca Socket.IO. Una de sus principales ventajas es que se puede usar en distintos entornos de lenguajes de programación, incluido Python. También ofrece una API más sencilla y coherente en ambos entornos, sin necesidad de usar directamente las API de WebSocket sin procesar. Además, facilita la implementación de medidas de seguridad como el uso compartido de recursos entre orígenes (CORS), la autenticación de usuarios y la restricción del tamaño de las cargas útiles.
En este tutorial veremos cómo crear un servidor WebSocket seguro en Python con python-socket.io y el cliente Socket.IO de JavaScript para conectar nuestro cliente al servidor.
Cómo crear un servidor WebSocket con Python
Instalación y configuración
Para empezar, debes tener Python 3.x y su administrador de paquetes, pip, instalados en tu equipo. Por suerte, todas las versiones recientes de Python incluyen pip. Si no tienes Python, descarga la versión más reciente para tu sistema.
Ahora, creemos algunos archivos de código fuente. Primero, crea una carpeta para guardar el código de ejemplo. Luego, instala el paquete python-socketio con pip:
Después, crea un archivo app.py dentro de quick-socket. Aquí escribirás el código del servidor de sockets.
Cómo crear un servidor WebSocket en Python
Empecemos por importar la biblioteca Socket.IO de Python y crear una instancia de servidor de sockets asíncrono en app.py. Lo convertiremos en un servidor ASGI configurando async_mode como asgi. Luego, configura los detectores de eventos en la instancia:
El primer evento se activará cuando un socket se conecte a nuestro servidor. La función connect correspondiente recibe dos argumentos:
sidosession ides un ID único que representa a un cliente conectado.socketes un diccionario que contiene toda la información relacionada con el cliente. Pásalo aconnect()para comprobar si hay información de identificación (por ejemplo, un nombre de usuario) al autenticar al cliente.
El último evento se activa cuando se hace clic en un botón de la aplicación cliente. El mensaje de carga útil (hero) se usa para obtener el nombre correspondiente del diccionario definido al principio del archivo.
Debes ejecutar un servidor web que exponga la aplicación de sockets del servidor al cliente. Para esto, usaremos ASGI. Por suerte, la biblioteca python-socket.io incluye ASGIApp, que nos ayuda a convertir app.py en una aplicación ASGI que puedes conectar a un servidor WSGI (como Gunicorn).
Debes crear una instancia de esta clase en la línea que está debajo de server_io = socketio.AsyncServer() y pasarle la instancia del servidor de sockets y la ruta a los archivos estáticos que creaste antes:
Por último, debes implementar la aplicación. Hay varias estrategias para implementar servidores Socket.IO, como Gunicorn y Eventlet. Consulta esta documentación y elige la estrategia que mejor se adapte a tus necesidades.
Cómo crear un cliente WebSocket con Socket.IO
Ahora que ya creaste un servidor WebSocket, es momento de crear un cliente WebSocket para comunicarse con él.
Primero, crea una carpeta client dentro de la carpeta raíz. Luego, crea dos archivos: index.html y index.js.
Dentro de index.html, define una estructura HTML sencilla e incluye enlaces al cliente Socket.IO, Bootstrap CSS y el archivo de script local, index.js:

Controlarás todos los eventos que emite el servidor dentro de index.js y emitirás un evento cuando se haga clic en cualquier botón de la página:
El código anterior obtiene el atributo id del botón en el que se hizo clic y emite ese valor al servidor. En respuesta, el servidor activará el evento name. En la función de devolución de llamada final, que se ejecuta cuando se activa el evento name, mostramos el nombre mediante la función alert de JavaScript.
En tu navegador, ve a 127.0.0.1:8000/index.html. La página debería mostrar una lista de botones, uno por cada héroe.
Haz clic en cualquier botón para emitir el evento get_name. En respuesta, verás una alerta con el nombre del héroe.

Pasemos a la seguridad de WebSocket.
Protege tus WebSockets
Debes implementar ciertas funciones en el servidor para proteger tus WebSockets.
Algunas de estas funciones están habilitadas de forma predeterminada en python-socketio, como ocurre con muchas otras bibliotecas de servidores WebSocket. Por ejemplo, la biblioteca comprime automáticamente el tráfico HTTP en las conexiones WebSocket, lo que ayuda a mejorar la velocidad de transferencia y el uso del ancho de banda.
Debes implementar o habilitar otras funciones al inicializar el servidor WebSocket.
Usa CORS para permitir o bloquear dominios
El uso compartido de recursos entre orígenes (CORS) es un mecanismo que permite a los clientes web realizar solicitudes entre orígenes. Las restricciones de CORS ayudan a proteger el servidor contra ataques de falsificación de solicitudes entre sitios (CSRF), en los que el atacante suele hacer que la víctima realice acciones no deseadas y, a menudo, perjudiciales, como transferir dinero propio.
Sin embargo, la protección que ofrece CORS se limita a las conexiones HTTP. Las políticas de CORS no se aplican a WebSockets porque las conexiones WebSocket usan el protocolo WebSocket (WS) o WebSocket seguro (WSS). En estos protocolos, el protocolo de enlace inicial se realiza mediante una solicitud HTTP Upgrade, se ignora el cuerpo de la respuesta y el protocolo HTTP/HTTPS cambia al protocolo WS/WSS.
Como CORS no restringe el acceso a los protocolos WebSocket, un usuario malicioso puede establecer fácilmente una conexión WebSocket entre orígenes para enviar y recibir datos maliciosos. La solución es actualizar el protocolo a WS agregando el campo de encabezado upgrade a la solicitud del cliente.
En respuesta, el servidor enviará un mensaje 101 switching protocols para confirmar que la comunicación posterior puede realizarse mediante WebSocket. Por suerte, la biblioteca cliente Socket.IO realiza este proceso automáticamente. Puedes consultar más opciones de configuración aquí.
El servidor verifica el encabezado Origin en la solicitud Upgrade para evitar conexiones WS entre orígenes no deseadas. La biblioteca python-socket.io permite hacerlo con cors_allowed_origins. Este parámetro puede recibir un solo origen o una lista de orígenes (un arreglo de URL) al inicializar el WebSocket en el servidor.
También puedes configurar este argumento como '*' para permitir todos los orígenes o como [] para bloquearlos todos:
También podrías querer evitar conexiones WebSocket desde orígenes distintos al tuyo para prevenir ataques de secuestro de WebSocket entre sitios (CSWSH). Este tipo de ataque es una variante de los ataques CSRF y permite la comunicación de lectura y escritura mediante WebSockets.
En este caso, los atacantes pueden crear una página web maliciosa en su dominio y establecer una conexión con el socket del servidor haciéndose pasar por el usuario. Luego, la aplicación maliciosa puede leer los mensajes que envía el servidor y escribir directamente en él.
Ten en cuenta que algunos clientes que no son navegadores pueden configurar fácilmente el encabezado Origin. Por lo tanto, asegúrate de complementar este método con otras formas de autenticar a los clientes.
Autentica a los clientes WebSocket antes de conectarlos
A medida que la web crece en tamaño y complejidad, también evolucionan los ciberataques. Un sistema de autenticación es una de las formas más eficaces de evitar el robo de datos y garantizar la privacidad en la comunicación entre cliente y servidor.
Un sistema de autenticación de usuarios funciona como una barrera entre el cliente y el servidor. Para acceder a los recursos del servidor, todo usuario debe proporcionar primero información de identificación única en la solicitud de inicio de sesión, normalmente un nombre de usuario y una contraseña.
El servidor verificará al usuario y aprobará o rechazará la solicitud de inicio de sesión con esta información. Si el servidor rechaza la solicitud, le indicará al usuario qué salió mal; por ejemplo: «La información de inicio de sesión es incorrecta». Esto ayuda a garantizar que los recursos no caigan en manos equivocadas.
Los usuarios pueden crear una contraseña segura y usar un administrador de contraseñas para reforzar la seguridad de la autenticación. Sin embargo, todos estos pasos dependen del usuario. Esta es otra razón clave para implementar sistemas de autenticación sólidos en el servidor de la aplicación.
Hay muchas bibliotecas de autenticación disponibles para el servidor. La que elijas dependerá del framework del servidor que uses para crear la aplicación.
Para nuestro ejemplo de WebSocket, puedes crear un archivo login.html en la carpeta public que muestre un formulario HTML en el navegador. Así, el usuario podrá ingresar su nombre de usuario y contraseña.
Luego, esperarás un evento submit en el script del cliente, public/index.js. Cuando el usuario envíe el formulario de inicio de sesión, inicializarás la conexión WebSocket entre el cliente y el servidor y enviarás el nombre de usuario y la contraseña proporcionados como extraHeaders:
La configuración anterior obtiene los valores del nombre de usuario y la contraseña de los campos de entrada según sus atributos id, los codifica como encabezados HTTP y los envía al servidor WebSocket cuando se conecta el cliente.
Para autenticar al usuario en el código del servidor, app.py, obtendremos el nombre de usuario y la contraseña de request:
Aquí, tomamos el nombre de usuario y la contraseña y comprobamos si hay datos de autenticación. Si las credenciales no existen, significa que el usuario no las envió, así que devolvemos False. En este caso, no podemos autenticar al usuario.
De lo contrario, procedemos a autenticar al usuario. Normalmente, comprobarías si el nombre de usuario existe en una base de datos y luego lo usarías para obtener contenido específico del usuario. Después, crearías una sesión de usuario y emitirías el mensaje user_authorized al cliente, que usaría JavaScript en su código para mostrarle este contenido al usuario.
Si no se autentican las conexiones WebSocket, cualquiera podría conectarse al servidor y robar datos confidenciales. Ten en cuenta que, una vez que el usuario se haya autenticado e iniciado sesión, las autenticaciones posteriores deben realizarse con tokens, en lugar de pedirle que vuelva a ingresar su nombre de usuario y contraseña.
Además, debes tener cuidado al procesar los datos enviados por el cliente. Asegúrate de validar cualquier entrada del cliente antes de procesarla. Los ataques como la inyección SQL también pueden realizarse mediante WebSockets, al igual que en las conexiones HTTP tradicionales.
Usa límites de frecuencia para proteger tu servidor WebSocket contra ataques
La limitación de frecuencia es una técnica que protege contra ataques de denegación de servicio (DoS) y denegación de servicio distribuido (DDoS). En estos ataques, un atacante intenta saturar —o incluso hacer caer— un servicio realizando tantas llamadas repetidas que deja de estar disponible para los usuarios legítimos.
La limitación de solicitudes evita estos ataques al limitar la frecuencia con la que cada usuario puede hacer solicitudes a la API. Podemos aplicar esta limitación según claves como una dirección IP, una clave de API u otro identificador único, como UniqueId.
Los límites de frecuencia y asignación, o cuotas, especifican la cantidad de solicitudes que un cliente puede hacer a una API en un período determinado. El proveedor del servicio suele aplicar este método para garantizar un consumo equitativo de los servicios y recursos basados en API.
Para aplicar la limitación de solicitudes en tu servidor, primero debes saber por qué la necesitas: para proteger un servicio, establecer cuotas para distintos planes, etc. Luego, debes identificar y elegir la clave de limitación más adecuada para tu caso. Después, implementas un mecanismo de limitación para hacer un seguimiento del uso de la API según la clave elegida.
Aunque actualmente no existe un paquete de limitación de solicitudes específico para python-socketio, hay una solución que puedes implementar con algo de trabajo.
Limita el tamaño de la carga útil para proteger tu servidor WebSocket
Enviar una carga útil enorme mediante WebSockets probablemente afectará el rendimiento y, en última instancia, hará que el servidor de sockets deje de funcionar.
Para evitar que WebSocket se vuelva lento o deje de funcionar, probablemente querrás limitar el tamaño máximo de las cargas útiles de los mensajes enviados a través de la conexión WebSocket. Esto te ayudará a evitar que el servidor deje de funcionar por el envío de un mensaje demasiado grande (como en un ataque de descompresión de archivos ZIP).
Además, regular el tamaño de los mensajes reduce considerablemente la latencia de la conexión WebSocket y puede mejorar significativamente la velocidad de transferencia. El protocolo WebSocket limita el tamaño de un frame y ayuda a determinar cuánta compresión necesita el mensaje. El mensaje se comprime. Comprimir el flujo de mensajes puede consumir recursos de memoria y CPU, pero suele valer la pena porque puede reducir enormemente el tráfico de red.
Usa TLS para crear una comunicación segura mediante sockets
Usar TLS/SSL es esencial para publicar un sitio web en Internet. Sin esta protección, la información confidencial que se transmite entre el cliente y el servidor puede robarse fácilmente mediante un ataque de intermediario. Para proteger la transmisión, debes usar un protocolo seguro como https:// en lugar del protocolo no seguro http://.
Para WebSockets, debes usar el protocolo wss:// (WebSockets cifrados con TLS) en lugar del protocolo no seguro ws://. Usar este último deja la conexión expuesta a la interferencia de terceros. En cambio, el protocolo wss:// cifra todos los datos enviados a través de WebSocket.
Gracias a este cifrado, ningún tercero puede leer ni modificar los mensajes enviados a través de WebSocket, lo que protege la información confidencial. Si la conexión es segura, también se vuelven imposibles otros tipos de ataques.
Además, antes de establecer una conexión WebSocket con el servidor, asegúrate de que el sitio web que la solicita también use https://. De lo contrario, actores maliciosos pueden manipular fácilmente las solicitudes.
TLS es necesario para cualquier sitio web, use o no WebSockets. Por suerte, muchos servicios de hosting ofrecen TLS gratis al implementar aplicaciones. Algunas de las plataformas de hosting más populares para aplicaciones de Python son Google Cloud, AWS, Azure, Heroku y Fly.io.
Conclusiones sobre WebSocket
En este tutorial, creamos un servidor WebSocket básico con python-socketio y lo conectamos a un cliente de JavaScript. También aprendimos cómo funcionan los WebSockets y analizamos algunas consideraciones de seguridad para asegurarnos de que los uses de forma segura.
Al igual que muchos otros protocolos web, los WebSockets dependen de medidas similares (TLS, CORS, limitación de solicitudes, etc.) para proteger adecuadamente las comunicaciones. Sin embargo, ninguna de estas medidas es exclusiva de WebSockets; aun así, es esencial entenderlas para evitar problemas de seguridad innecesarios.
Herramienta gratuita en línea para revisar código
Protege tu código antes de tu próximo commit.
