WebSocket (in)Security e Autenticazione - Parte 1

WebSocket (in)Security e Autenticazione - Parte 1

Everyone posts everything in real time as it happens. - John Cho

Le applicazioni Web real-time sono sempre più diffuse al giorno d'oggi, soprattutto per la necessità di dover gestire flussi di dati in costante aggiornamento via via maggiori. Basti pensare, per esempio, agli innumerevoli segnali provenienti dai sensori nel campo dell'Internet of Things. Tradizionalmente, l'approccio per sviluppare applicazioni Web che richiedono una comunicazione in tempo reale fra client e server ha richiesto un eccessivo abuso del protocollo HTTP, in quanto era necessario effettuare un continuo polling del server per ricevere aggiornamenti e, dall'altro lato, l'invio di flussi di dati tramite diverse chiamate HTTP. Ciò comportava naturalmente dei problemi non indifferenti che hanno portato all'affermazione di una soluzione più efficace e appropriata: il protocollo WebSocket. Una miriade di applicazioni Web utilizzano infatti tale protocollo per visualizzazione di dati, chat in tempo reale, aggiornamenti di stato, e così via.

Designed by vectorjuice / Freepik

In questo primo articolo introdurremo il protocollo WebSocket e vedremo una panoramica degli aspetti di sicurezza ad esso relativi. Successivamente, nella seconda parte, approfondiremo con un'analisi della problematica dell'autenticazione relativa al protocollo e implementeremo un proof-of-concept per una versione "hardenizzata" di un server WebSocket, includendo appunto un meccanismo di autenticazione come spunto iniziale per casi d'uso reali.

Piccola nota: questa mini-serie è frutto di un approfondimento che ho voluto svolgere per mera curiosità a completamento della stesura del report tecnico INAF WebSocket Integration in Django, nel quale ho tralasciato, per ovvi motivi, i dettagli sugli aspetti di sicurezza e il focus sulla problematica dell'autenticazione.

WebSocket Protocol

Il protocollo WebSocket, definito nell'RFC 6455, prevede canali di comunicazione full-duplex tra un client e un server su una singola connessione TCP. Il protocollo è costituito da un handshake di apertura - inizializzato via una richiesta HTTP Upgrade - seguito da frame di messaggi base, stratificati su TCP. L'obiettivo del protocollo è quello di fornire un meccanismo per applicazioni browser-based che necessitano di una comunicazione bidirezionale con i server, evitando l'apertura di più connessioni HTTP. Questa tecnica risulta utile e può essere adottata per una varietà di applicazioni in ambito Web, tra cui giochi multiplayer, modifica di documenti condivisi, messaggistica istantanea, analisi e/o visualizzazione in tempo reale di dati, notifiche push, e così via. In breve il protocollo funziona come segue: una volta che il client e il server hanno inviato entrambi i loro messaggi di handshake e la procedura ha avuto successo, inizia la parte di trasferimento dati. Il metodo si traduce in un canale di comunicazione a due vie in cui ciascuna delle parti può, indipendentemente dall'altra, inviare dati a piacimento. Dopo l'handshake, client e server trasferiscono i dati nelle unità concettuali specificate dalo standard come "messaggi". A livello fisico, un messaggio è composto da uno o più frame. Il protocollo WebSocket rappresenta una delle soluzioni più adatte per superare i problemi illustrati nell'introduzione: infatti, le sue implementazioni sono comunemente usate nelle moderne applicazioni Web.

WebSocket Protocol

Le connessioni WebSocket sono in genere di lunga durata poiché normalmente rimangono aperte e inattive fino a quando il client o il server non sono pronti per inviare un messaggio. Supponendo che un server WebSocket sia configurato correttamente e sia in attesa di connessioni in entrata, un client WebSocket può normalmente utilizzare del codice JavaScript client-side per avviare una connessione verso il server. Al termine dell'handshake, la connessione di rete rimane aperta e può essere utilizzata per inviare messaggi WebSocket in entrambe le direzioni, finché una delle parti non invia un frame di chiusura (o si disconnette), in modo asincrono. Secondo lo standard, i messaggi WebSocket possono contenere stringhe UTF-8 o dati binari e, a questo proposito, nelle applicazioni moderne è molto comune che il formato JSON venga utilizzato per inviare dati strutturati - o BSON (JSON binario) quando è coinvolta la serializzazione dei dati -, all'interno dei messaggi WebSocket.

WebSocket Security

Generalmente l'impiego del protocollo WebSocket può esporre un'applicazione a diversi rischi. Lo standard non considera, infatti, alcune implicazioni di sicurezza by design, dunque è necesssario adottare alcune misure per evitare incidenti indesiderati. In teoria, ogni tipo di vulnerabilità Web che si può presentare con HTTP potrebbe anche scaturire in relazione al protocollo WebSocket. Prima di vedere una panoramica dei principali attacchi e misure da adottare, è utile osservare che diversi header previsti nell'handshake WS, nonostante i nomi un po' fuorvianti, non servano di fatto quasi a nulla in termini di sicurezza:

  • Gli header Connection e Upgrade nella richiesta e nella risposta indicano che si tratta di un handshake WebSocket;
  • Il request header Origin viene utilizzato per proteggere dall'uso non autorizzato cross-origin di un server WS da script che utilizzano l'API WebSocket in un browser Web;
  • Il request header Sec-WebSocket-Version specifica la versione del protocollo WebSocket che il client desidera utilizzare;
  • Il request header Sec-WebSocket-Key contiene un valore casuale, con codifica Base64, che dovrebbe essere generato casualmente a ogni richiesta di handshake;
  • Il response header Sec-WebSocket-Accept contiene un hash del valore inviato nel request header Sec-WebSocket-Key, concatenato con un Globally Unique Identifier (GUID) specificato dal protocollo come segue: "258EAFA5-E914-47DA- 95CA-C5AB0DC85B11". Tale stringa sarà - così giustifica l'RFC  - "difficilmente utilizzata da endpoint che non comprendono il protocollo WebSocket" e serve quindi ad aggiungere integrità al protocollo stesso.

Per quanto riguarda i tre header Sec-WebSocket-*, il prefisso "Sec-" non deve quindi trarre in inganno: esso fa parte dei cosiddetti forbidden HTTP request header names, insieme al prefisso "Proxy-" e una lista di nomi riservati. Tali prefissi e nomi vengono scelti per evitare che gli header vengano impostati o cambiati arbitrariamente - per esempio tramite codice JavaScript -, impedendo quindi a siti malevoli di fare inviare allo user agent metadati modificati insieme alla request e, dunque, offrendo maggiore sicurezza ai siti Web rispetto al contenuto e al valore di tali request headers.

Validazione e sanitizzazione

In primis, è estremamente importante trattare i dati provenienti sia da client che server in maniera untrusted, dunque risulta fondamentale validare e sanitizzare adeguatamente gli input provenienti dal client (e gli output del server): il server potrebbe risultare esposto ad Input Data Attacks, come SQL Injection, XML External Entity Injection e simili; il client potrebbe invece essere esposto ad attacchi Cross-Site Scripting (XSS). A questo proposito, è sconsigliabile assegnare il contenuto dei messaggi provenienti dal server direttamente al DOM o, peggio, trattarlo come codice pronto da eseguire. Nel caso di messaggi JSON, ricordiamo che è sempre una buona idea utilizzare il metodo JSON.parse() in combinazione con la gestione delle eccezioni e, se necessario, metodi di sanitizzazione appropriati anche lato client.

DoS e Tunneling

Il protocollo consente a un numero illimitato di connessioni di raggiungere il server, rendendo quest'ultimo vulnerabile ad attacchi di negazione del servizio (DoS, DDoS). È quindi consigliato adottare un meccanismo di rate limiting, in modo da controllare il tasso di richieste ricevute e limitare anche il Web scraping. Inoltre, è da evitare il tunneling over WS di un servizio TCP arbitrario, come per esempio FTP o VNC. Il caso d'uso più diffuso consiste nell'effettuare il tunneling per bypassare le restrizioni dei firewall. Tuttavia, oltre a non essere il modo più appropriato per risolvere il problema, questo processo esporrebbe il sistema a ulteriore rischi: nel caso di XSS, per esempio, quei servizi risulterebbero esposti in quanto il codice arbitrario che verrà eseguito può comunicarvi in maniera diretta.

WebSocket Secure (WSS)

Essendo lo scambio di dati in plain text, questi risultano vulnerabili ad attacchi Man-in-the-Middle (MitM) che potrebbero comportare il tampering dei messaggi, quindi eliminare o manipolare o modificare questi ultimi, e ovviamente il leak dei dati stessi. Similmente ad HTTP, è quindi consigliato aggiungere un livello di cifratura utilizzando il protocollo TLS: in tal modo si può utilizzare il protocollo WSS (WebSocket Secure) al posto del regolare WS, garantendo la cifratura della comunicazione e l'integrità dei dati.

Autenticazione e Autorizzazione

È bene precisare che l'adozione del protocollo WSS non implica un metodo di autenticazione. Quest'ultima, insieme all'autorizzazione, non viene intrinsecamente considerata nello standard: infatti non è consentito al server di autenticare il client durante il processo di handshake. Contrariamente a quanto si potrebbe pensare, l'header Sec-WebSocket-Key - come abbiamo visto sopra - contiene un valore casuale per prevenire errori di memorizzazione nelle cache dei proxy e non viene utilizzato per scopi di autenticazione o gestione della sessione - NON è quindi un token CSRF. Come possiamo risolvere allora questa mancanza di autenticazione? La risposta potrebbe sembrare banale, ma è necessario scavare un po' più a fondo. Finora abbiamo osservato che le problematiche a cui potremmo andare incontro quando abbiamo a che fare con il protocollo WebSocket sono le stesse su cui potremmo imbatterci in ambito Web in generale. La soluzione al problema dell'autenticazione potrebbe, pertanto, essere l'utilizzo dei normali meccanismi di autenticazione utilizzati per HTTP: autenticazione tramite HTTP basic authentication, tramite cookie, tramite TLS. Nel prossimo articolo analizzeremo meglio questo aspetto e ci renderemo conto che è necessario adottare una soluzione differente se si vuole puntare su un'implementazione più generale - e al tempo stesso quanto più sicura - che funzioni tranquillamente con i browser come client.

A ogni modo, nel caso volessimo fare affidamento ai cookie HTTP per la gestione della sessione, ci troveremmo davanti a una vulnerabilità di Cross-Site WebSocket Hijacking (CSWSH), ovvero Cross-Site-Request-Forgey (CSRF) sul messaggio di handshake WS. Un attaccante potrebbe quindi creare una pagina Web malevola sul proprio dominio che stabilisce una connessione WebSocket tra i siti all'applicazione vulnerabile, la quale gestirà la connessione nel contesto della sessione dell'utente vittima. A tal proposito è quindi bene proteggere il messaggio di handshake WebSocket contro CSRF, proprio per evitare vulnerabilità CSWSH. Per concludere, è importante evitare di fare affidamento sull'header Origin come prova di autenticazione in quanto esso è sostanzialmente informativo e può essere facilmente manipolato da un attaccante. L'header Origin risulta comunque utile come misura contro CSWSH in combinazione con un token CSRF.


Conclusioni

Il protocollo WebSocket rappresenta sicuramente la migliore soluzione per lo sviluppo di applicazioni Web real-time. In questa prima parte abbiamo presentato le caratteristiche principali del protocollo e ne abbiamo analizzato le principali problematiche di sicurezza le quali, come illustrato, si possono sostanzialmente configurare nelle classiche vulnerabilità Web. Tuttavia, abbiamo (volutamente) lasciato in sospeso l'analisi e le possibili soluzioni per la problematica dell'autenticazione, purtroppo non contemplata nelle specifiche del protocollo. Nel prossimo articolo ci addentreremo più nel dettaglio su quest'ultima parte e implementeremo un proof-of-concept per una versione hardenizzata di un server WebSocket al quale aggiungeremo, appunto, un meccanismo di autenticazione.


References