Como criar um servidor WebSocket seguro em Python
Kingsley Ubah
5 de julho de 2022
0 minutos de leituraEm geral, quando um aplicativo web precisa de algo de um servidor externo, o cliente envia uma solicitação ao servidor, que responde, e a conexão é encerrada em seguida.
Imagine um aplicativo web que mostra cotações de ações. Para exibir os preços mais recentes, o cliente precisa solicitar atualizações repetidamente ao servidor. Essa abordagem é ineficiente, pois encerra e reabre a conexão HTTP o tempo todo. Além disso, não é ideal: o preço de uma ação no banco de dados pode mudar em tempo real enquanto o aplicativo ainda mostra o valor antigo.
Com a comunicação bidirecional, porém, o servidor pode enviar novos preços ao cliente por conta própria sempre que as informações forem atualizadas. Engenheiros criaram o protocolo WebSocket para permitir esse tipo de conexão persistente e evitar os problemas dos métodos mais antigos de long polling.
Uma opção popular para implementar WebSockets é a biblioteca Socket.IO. Uma de suas principais vantagens é funcionar em diferentes ambientes de linguagens de programação, inclusive Python. Ela também oferece uma API mais simples e consistente nos dois ambientes, sem exigir o uso direto das APIs WebSocket nativas. Além disso, facilita a implementação de medidas de segurança, como compartilhamento de recursos entre origens (CORS), autenticação de usuários e limite de tamanho do payload.
Neste tutorial, vamos explorar como criar um servidor WebSocket seguro em Python usando python-socket.io e o cliente Socket.IO em JavaScript para conectar o cliente ao servidor.
Criar um servidor WebSocket com Python
Instalação e configuração
Para começar, você precisa ter o Python 3.x e o gerenciador de pacotes dele, o pip, instalados na sua máquina. Felizmente, todas as versões recentes do Python incluem o pip. Se você ainda não tem o Python, baixe a versão mais recente para o seu sistema.
Agora, vamos criar alguns arquivos de código-fonte. Primeiro, crie uma pasta para guardar o código de exemplo. Em seguida, instale o pacote python-socketio com o pip:
Depois, crie um arquivo app.py dentro da pasta quick-socket. É nele que você vai colocar o código do servidor socket.
Criar um servidor WebSocket em Python
Vamos começar importando a biblioteca Socket.IO do Python e criando uma instância assíncrona do servidor socket dentro de app.py. Vamos configurá-lo como um servidor ASGI definindo async_mode como asgi. Em seguida, vamos configurar os listeners de eventos na instância:
O primeiro evento será disparado quando um socket se conectar ao servidor. A função connect correspondente recebe dois argumentos:
sid, ousession id, é um identificador exclusivo que representa um cliente conectado.socketé um dicionário com todas as informações relacionadas ao cliente. Passe esse dicionário paraconnect()para verificar dados de identificação (por exemplo, um nome de usuário) durante a autenticação do cliente.
O último evento é disparado quando um botão é clicado no aplicativo cliente. A mensagem do payload (hero) é usada para acessar o nome correspondente no dicionário definido no início do arquivo.
Você precisa executar um servidor web que disponibilize o aplicativo socket do lado do servidor para o cliente. Para isso, vamos usar o ASGI. Felizmente, a biblioteca python-socket.io inclui ASGIApp, que ajuda a transformar app.py em um aplicativo ASGI que pode se conectar a um servidor WSGI (como o Gunicorn).
Você vai precisar criar uma instância dessa classe na linha abaixo de server_io = socketio.AsyncServer(), passando a instância do servidor socket e o caminho para os arquivos estáticos que você criou antes:
Por fim, você precisa implantar o aplicativo. Há várias estratégias para implantar servidores Socket.IO, como Gunicorn e Eventlet. Consulte esta documentação e escolha a estratégia que melhor atende às suas necessidades.
Criar um cliente WebSocket usando Socket.IO
Agora que você criou um servidor WebSocket, é hora de criar um cliente WebSocket para se comunicar com ele.
Primeiro, crie uma pasta client dentro da pasta raiz. Depois, crie dois arquivos: index.html e index.js.
Dentro de index.html, defina uma estrutura HTML simples e inclua links para o cliente Socket.IO, o Bootstrap CSS e seu arquivo de script local, index.js:

Você vai tratar em index.js todos os eventos emitidos pelo servidor e emitir um evento quando qualquer botão da página for clicado:
O código acima obtém o atributo id do botão clicado e emite esse valor para o servidor. Em resposta, o servidor dispara o evento name. No callback final, executado quando o evento name é acionado, mostramos o nome usando a função alert do JavaScript.
No navegador, acesse 127.0.0.1:8000/index.html. A página deve mostrar uma lista de botões, um para cada herói.
Clique em qualquer botão para emitir o evento get_name. Em resposta, você verá um alerta com o nome do herói.

Agora, vamos falar sobre a segurança do WebSocket.
Proteja seus WebSockets
Para proteger seus WebSockets, você precisa implementar alguns recursos no servidor.
Alguns desses recursos já vêm ativados por padrão no python-socketio, assim como em muitas outras bibliotecas de servidor WebSocket. Por exemplo, a biblioteca comprime automaticamente os dados HTTP nas conexões WebSocket, o que ajuda a melhorar a velocidade de transferência e o uso da largura de banda.
Outros recursos precisam ser implementados ou ativados ao inicializar o servidor WebSocket.
Use CORS para permitir ou bloquear domínios
O compartilhamento de recursos entre origens (CORS) é um mecanismo que permite que clientes web façam solicitações entre origens. As restrições de CORS ajudam a proteger o servidor contra ataques de falsificação de solicitação entre sites (CSRF), nos quais o invasor geralmente induz a vítima a realizar ações não intencionais e muitas vezes prejudiciais, como transferir dinheiro pessoal.
No entanto, a proteção oferecida pelo CORS se limita a conexões HTTP. As políticas de CORS não se aplicam a WebSockets, pois as conexões WebSocket usam os protocolos WebSocket (WS) ou WebSocketSecure (WSS). Nesses protocolos, o handshake inicial acontece por meio de uma solicitação HTTP Upgrade; o corpo da resposta é ignorado, e o protocolo HTTP/HTTPS é atualizado para o protocolo WS/WSS.
Como o CORS não restringe o acesso aos protocolos WebSocket, um usuário mal-intencionado pode criar facilmente uma conexão WebSocket entre origens para enviar e receber dados maliciosos. Uma solução é atualizar o protocolo para WS adicionando o campo de cabeçalho upgrade à solicitação do cliente.
Em resposta, o servidor enviará uma mensagem 101 switching protocols, confirmando que a comunicação seguinte pode ocorrer pelo WebSocket. Felizmente, a biblioteca cliente Socket.IO realiza esse processo automaticamente. Você pode ver mais opções de configuração aqui.
O servidor verifica o cabeçalho Origin na solicitação Upgrade para impedir conexões WS indesejadas entre origens. A biblioteca python-socket.io permite fazer isso com cors_allowed_origins. Ao inicializar o WebSocket no servidor, esse parâmetro pode receber uma única origem ou uma lista de origens (um array de URLs).
Você também pode definir esse argumento como '*' para permitir todas as origens ou como [] para bloquear todas:
Também é recomendável impedir conexões WebSocket de origens diferentes da sua para evitar ataques de sequestro de WebSocket entre sites (CSWSH). Esse tipo de ataque é uma variação dos ataques CSRF e permite comunicação de leitura e gravação via WebSockets.
Nesse caso, os invasores podem criar uma página web maliciosa no próprio domínio e estabelecer uma conexão com o socket do servidor fingindo ser o usuário. Em seguida, o aplicativo malicioso pode ler as mensagens enviadas pelo servidor e gravar diretamente nele.
Lembre-se de que alguns clientes que não são navegadores conseguem definir facilmente o cabeçalho Origin. Por isso, combine essa abordagem com outras formas de autenticação de clientes.
Autentique os clientes WebSocket antes da conexão
À medida que a web cresce em tamanho e complexidade, os ataques cibernéticos também evoluem. Um sistema de autenticação é uma das formas mais eficazes de evitar o roubo de dados e garantir a privacidade na comunicação entre cliente e servidor.
Um sistema de autenticação de usuários funciona como uma barreira entre o cliente e o servidor. Qualquer pessoa que queira acessar recursos no servidor precisa primeiro fornecer informações de identificação exclusivas na solicitação de login — normalmente, um nome de usuário e uma senha.
Com essas informações, o servidor verifica a identidade do usuário e aprova ou rejeita a solicitação de login. Quando o servidor rejeita a solicitação, ele informa ao usuário o que deu errado, por exemplo: “As informações de login estão incorretas”. Isso ajuda a impedir que os recursos caiam nas mãos erradas.
Os usuários podem criar uma senha forte e usar um gerenciador de senhas para aumentar a segurança da autenticação. No entanto, essas medidas dependem do usuário. Esse é outro motivo importante para implementar sistemas robustos de autenticação no lado do servidor do aplicativo.
Há muitas bibliotecas de autenticação disponíveis para o lado do servidor. A escolha depende do framework de servidor usado para criar o aplicativo.
No nosso exemplo de WebSocket, você pode criar um arquivo login.html na pasta public para exibir um formulário HTML no navegador. Assim, o usuário poderá informar o nome de usuário e a senha.
Em seguida, você vai aguardar um evento submit no script do cliente, public/index.js. Quando o usuário enviar o formulário de login, você inicializará a conexão WebSocket entre o cliente e o servidor, enviando o nome de usuário e a senha informados como extraHeaders:
A configuração acima obtém os valores do nome de usuário e da senha dos campos de entrada com base nos atributos id, codifica-os como cabeçalhos HTTP e os envia ao servidor WebSocket quando o cliente se conecta.
Para autenticar o usuário no código do servidor, app.py, vamos obter o nome de usuário e a senha de request:
Aqui, usamos o nome de usuário e a senha e verificamos se há dados de autenticação. Se as credenciais não existirem, significa que o usuário não as enviou, então retornamos False. Nesse caso, não podemos autenticar o usuário.
Caso contrário, prosseguimos com a autenticação do usuário. Normalmente, você verificaria se o nome de usuário existe em um banco de dados e o usaria para recuperar conteúdo específico do usuário. Em seguida, criaria uma sessão de usuário e emitiria a mensagem user_authorized para o cliente. No código do cliente, você usaria JavaScript para exibir esse conteúdo ao usuário.
Sem autenticar as conexões WebSocket, qualquer pessoa pode conseguir se conectar ao servidor e roubar dados confidenciais. Depois que o usuário for autenticado e fizer login, as autenticações seguintes devem ser feitas com tokens, em vez de pedir que ele informe novamente o nome de usuário e a senha.
Além disso, tenha cuidado ao lidar com dados enviados pelo cliente. Valide todas as entradas do cliente antes de processá-las. Ataques como a injeção de SQL também podem ocorrer por WebSockets, assim como em conexões HTTP tradicionais.
Use limitação de taxa para proteger seu servidor WebSocket contra ataques
A limitação de taxa é uma técnica que protege contra ataques de negação de serviço (DoS) e de negação de serviço distribuída (DDoS). Nesses ataques, o invasor tenta sobrecarregar — ou até derrubar — um serviço fazendo tantas chamadas repetidas que ele fica indisponível para usuários legítimos.
A limitação de taxa impede esses ataques ao limitar a frequência de solicitações à API que cada usuário pode fazer. Podemos aplicar esse limite com base em chaves, como um endereço IP, uma chave de API ou outro identificador exclusivo, como UniqueId.
Os limites de taxa e de alocação, ou cotas, especificam o número de solicitações que um cliente pode fazer a uma API em determinado período. Em geral, o provedor de serviços aplica esse método para garantir o uso justo de serviços e recursos baseados em API.
Para aplicar a limitação de taxa no servidor, primeiro você precisa entender por que ela é necessária — para proteger um serviço, definir cotas para diferentes planos etc. Em seguida, identifique e escolha a chave de limitação mais adequada ao seu caso. Depois, use uma implementação de limitação para monitorar o uso da API com base na chave escolhida.
Embora ainda não exista um pacote de limitação de taxa específico para python-socketio, há uma solução que você pode implementar com um pouco de trabalho.
Limite o tamanho do payload para proteger seu servidor WebSocket
O envio de um payload enorme por WebSockets provavelmente prejudicará o desempenho — e, no fim das contas, poderá derrubar o servidor de sockets.
Para evitar lentidão ou falhas no WebSocket, você provavelmente vai querer limitar o tamanho máximo dos payloads das mensagens enviadas pela conexão WebSocket. Isso ajuda a evitar que o servidor caia devido ao envio de uma mensagem grande demais (como em um ataque de bomba zip).
Além disso, controlar o tamanho das mensagens reduz bastante a latência na conexão WebSocket e pode melhorar significativamente a velocidade de transferência. O protocolo WebSocket limita o tamanho de um quadro e ajuda a determinar o nível de compactação necessário para a mensagem. Compactar o fluxo de mensagens pode consumir memória e recursos de CPU, mas geralmente vale a pena, pois reduz bastante o tráfego de rede.
Use TLS para criar uma comunicação segura por sockets
O uso de TLS/SSL é essencial para publicar um site na Internet. Sem isso, informações confidenciais transmitidas entre o cliente e o servidor podem ser facilmente roubadas em um ataque man-in-the-middle. Para proteger a transmissão, use um protocolo seguro, como https://, em vez do protocolo não seguro http://.
Para WebSockets, use o protocolo wss:// (WebSockets criptografados com TLS) em vez do protocolo não seguro ws://. Usar este último deixa a conexão vulnerável à interferência de terceiros. Já o protocolo wss:// criptografa todos os dados enviados pelo WebSocket.
Com essa criptografia, terceiros não conseguem ler nem modificar as mensagens enviadas pelo WebSocket, protegendo assim as informações confidenciais. Outros tipos de ataque também se tornam impossíveis quando a conexão é segura.
Além disso, antes de estabelecer uma conexão WebSocket com o servidor, verifique se o site que está fazendo a solicitação também usa https:// — sem isso, agentes mal-intencionados podem adulterar as solicitações com facilidade.
O TLS é necessário para qualquer site, com ou sem WebSockets. Felizmente, muitos serviços de hospedagem oferecem TLS gratuito na implantação de aplicações. Algumas das plataformas de hospedagem mais populares para aplicações Python são Google Cloud, AWS, Azure, Heroku e Fly.io.
Conclusão sobre WebSockets
Neste tutorial, criamos um servidor WebSocket básico com python-socketio e o conectamos a um cliente JavaScript. Também aprendemos como os WebSockets funcionam e abordamos algumas considerações de segurança para garantir que você os use com segurança.
Assim como muitos outros protocolos da web, os WebSockets dependem de medidas semelhantes (TLS, CORS, limitação de taxa etc.) para proteger as comunicações adequadamente. Nenhuma dessas medidas é exclusiva dos WebSockets, mas entendê-las é essencial para evitar problemas de segurança desnecessários.
Ferramenta gratuita online para verificar código
Proteja seu código antes do próximo commit.
