Artigo original: https://www.freecodecamp.org/news/how-to-secure-your-websocket-connections-d0be0996c556/

A web está crescendo em uma velocidade enorme. As aplicações para a web estão cada vez mais dinâmicas, imersivas e não exigem que o usuário recarregue a página para que ocorram atualizações. O suporte para tecnologias de comunicação de baixa latência como websockets também está em constante evolução. Websockets nos permitem realizar comunicações em tempo real entre diferentes clients conectados a um mesmo servidor.

Muitas pessoas não sabem como proteger suas conexões websocket de alguns ataques comuns. Vamos ver quais são eles e o que você pode fazer para deixar seus websockets seguros.

Nº 0: Habilite o CORS

WebSocket não possui CORS nativo. Isso significa que qualquer site pode se conectar com qualquer outro através de uma conexão websocket sem qualquer restrição! Eu não vou entrar em detalhes acerca dos motivos para que isso seja assim, mas uma maneira rápida de corrigir isso é verificar o cabeçalho de requisição Origin no handshake do WebSocket.

Claro, o cabeçalho de requisição Origin pode ser falsificado por um invasor, mas não importa, porque, para explorar isso, o invasor precisa falsificar o cabeçalho Origin no navegador da vítima. Navegadores modernos não permitem que códigos convencionais em JavaScript no navegador alterem o cabeçalho Origin.

Ademais, se você estiver autenticando usuários usando, preferencialmente, cookies, isso não será um problema (mais sobre esse assunto no tópico nº 4).

Nº 1: Implementando taxa de limitação

A taxa de limitação é importante. Sem ela, clients podem – cientes ou não – realizar um ataque do tipo DoS (Denial of Service) no seu servidor. Em outras palavras, DoS significa que um único client está fazendo com que o servidor fique ocupado a ponto de não conseguir lidar com com outros clients.

Na maioria dos casos, é uma tentativa deliberada vinda de um invasor visando derrubar o servidor. Às vezes, uma implementação ruim no front-end também pode gerar um ataque DoS por clients normais.

Faremos uso do algoritmo do balde furado (em inglês, leaky bucket – o qual, aparentemente, é um algoritmo muito usado para implementação de redes) para implementar a taxa de limitação em nossos websockets.

A ideia é que você tenha um balde com um furo de tamanho fixo no fundo. Você começa colocando água nele e a água sai pelo buraco no fundo. Agora, se o balde receber mais água do que está sendo escoado por um longo período de tempo, em algum momento o balde ficará cheio e começará a vazar. É isso.

Agora, vamos entender como isso tem relação com nosso websocket:

A água é o tráfego do websocket enviado pelo usuário.

A água passa pelo buraco no fundo do balde. Isso significa que o servidor processou aquela requisição em particular com sucesso.

A água que está no balde e ainda não vazou é basicamente o tráfego pendente. O servidor processará esse tráfego depois. Isso pode ser também um tráfego em massa, ou seja, muito tráfego de uma vez só, contanto que o balde não vaze).

A água que está vazando é o tráfego descartado pelo servidor (muito tráfego proveniente de um único usuário).

O ponto aqui é que você tem que verificar a atividade do seu websocket e determinar esses números. Você estabelecerá um balde para cada usuário. Decidiremos o tamanho que o balde deve ter (tráfego o qual um único usuário poderá enviar durante o período específico) dependendo do tamanho do buraco ao fundo do balde (quanto tempo em média o seu servidor leva para processar uma única requisição websocket, digamos que salvando uma mensagem enviada pelo usuário em um banco de dados, por exemplo).

Essa é uma implementação simples que eu estou usando no codedamn para implementar o algoritmo de balde furado para websockets. É em NodeJS, mas os conceitos são sempre os mesmos.

if(this.limitCounter >= Socket.limit) {
  if(this.burstCounter >= Socket.burst) {
     return 'O balde está vazando'
  }
  ++this.burstCounter
  return setTimeout(() => {
  this.verify(callingMethod, ...args)
  setTimeout(_ => --this.burstCounter, Socket.burstTime)
  }, Socket.burstDelay)
}
++this.limitCounter

Então, o que está acontecendo aqui? Basicamente, se o limite for alcançado tanto pelo limite de tráfego (que são constantes definidas), a conexão websocket cai. Caso contrário, após um certo tempo, resetaremos o contador de tráfego. Isso deixa espaço livre para outro tráfego em massa.

Nº 2: Restrinja o tamanho da carga

Isso deve ser utilizado como um recurso do seu framework de back-end. Se o seu não tem, é hora de mudar para um melhor! Você deve limitar o tamanho máximo da mensagem que será enviada pelo seu websocket. Teoricamente, não há limite. Claro, é bem provável que receber uma carga muito grande congestionará essa instância de websocket específica e consumirá mais recursos do sistema do que deveria.

Por exemplo, se você está usando a biblioteca WS para Node para criar websockets no servidor, você pode usar a opção maxPayload para especificar o tamanho máximo da carga em bytes. Se o tamanho da carga for maior que o definido, a biblioteca, nativamente, derrubará a conexão.

Não tente implementar isso por conta própria determinando o tamanho da mensagem. Não queremos ler a mensagem inteira na memória RAM do sistema. Se a mensagem for 1 byte acima do limite, descarte-a. Isso pode ser implementado pelo framework (que lidará com as mensagens como fluxo de bytes em vez de strings fixas).

Nº 3: Crie um protocolo de comunicação sólido

Como agora você está em uma conexão duplex, você pode enviar qualquer coisa para o servidor. O servidor também pode enviar qualquer texto de volta para o client. Você pode precisar de um caminho para assegurar a comunicação efetiva entre os dois.

Você não pode enviar mensagens brutas se quiser escalar o aspecto das mensagens do seu site. Eu prefiro usar JSON, mas existem outros meios optimizados de configurar a comunicação. No entanto, considerando o JSON, é assim que um esquema básico de mensagens deve parecer em um site genérico:

Do client para o servidor (ou vice-versa):

{ status: "ok"|"error", event: EVENT_NAME, data: <dados de qualquer tipo> }

Agora, fica mais fácil para você validar eventos e formatos no back-end. Derrube a conexão imediatamente e registre o endereço de IP do usuário, caso o formato da mensagem for diferente. De modo algum o formato pode mudar, a menos que alguém esteja manualmente mexendo na sua conexão websocket. Se você está utilizando o Node, eu recomendo usar a biblioteca Joi para as próximas validações de dados provenientes do usuário.

Nº 4: Autentique os usuários antes de estabilizar a conexão WS

Se você está utilizando websockets para usuários autenticados, é uma ótima ideia permitir que apenas usuários autenticados estabeleçam com sucesso uma conexão websocket. Não permita que ninguém estabeleça uma conexão e espere que eles se autentiquem no próprio websocket. Primeiramente, estabelecer uma conexão websocket é sempre um pouco caro. Logo, você não quer que pessoas sem autorização acessem seu websocket e monopolizem as conexões que poderiam ser utilizadas por outras pessoas.

Para isso, quando estiver estabelecendo uma conexão no front-end, envie algum dado de autenticação para o websocket. Pode ser um header como X-Auth-Token: <token atribuído a este client no login>. Por padrão, cookies serão passados de qualquer jeito.

De novo, realmente depende da biblioteca que você está utilizando para implementar websockets. Se, contudo, você estiver usando Node e a WS, existe uma função chamada verifyClient que te dá acesso às informações do objeto passado pela conexão websocket (assim como você tem acesso ao objeto req para solicitações HTTP).

Nº 5: Use SSL com websockets

Isso não é trivial, mas é necessário ser dito. Use wws:// em vez de ws://. Isso adiciona uma camada de segurança à sua conexão. Use um servidor como o Nginx para realizar proxy reverso nos websockets e habilitar SSL acima deles. Como configurar o Nginx será um tutorial à parte, eu vou deixar algumas diretivas que você precisa utilizar no Nginx para as pessoas que são familiarizadas com ele. Mais informações aqui.

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

Aqui, assumimos que o seu servidor websocket está ouvindo na porta 1337 e que seus usuários estão conectados ao seu websocket assim:

const ws = new WebSocket('wss://seusite.com/local-do-seu-websocket')

Obrigado pela leitura!