RFC 6455, pubblicata dall’IETF nel dicembre 2011, definisce un canale TCP bidirezionale e persistente fra browser e server, aperto da un normale handshake HTTP. Il dettaglio da guardare non è l’handshake, ma una regola apparentemente arbitraria della sezione 5: ogni frame che il client manda al server deve essere mascherato con una chiave XOR a 32 bit. Quella regola è il residuo di un attacco concreto contro l’infrastruttura di rete dell’epoca.

Contesto

Prima di RFC 6455 la comunicazione a bassa latenza fra browser e server si reggeva su workaround montati sopra HTTP: short polling, long polling, le tecniche che andavano sotto il nome di Comet, e i Server-Sent Events per il flusso unidirezionale dal server al client. Tutte riusano la semantica richiesta/risposta di HTTP per simulare un canale che HTTP non dà. Per il traffico bidirezionale a bassa latenza il prezzo è salato: una richiesta per ogni messaggio, header ripetuti, connessioni tenute aperte a forza.

WebSocket nasce per dare un canale TCP esplicito. Il client apre con una richiesta GET che porta Upgrade: websocket e Connection: Upgrade; il server che accetta risponde 101 Switching Protocols. Da lì in poi la connessione TCP non parla più HTTP: trasporta frame WebSocket. Gli URI usano gli schemi ws:// (porta 80) e wss:// (porta 443, sopra TLS).

L’handshake include un controllo che non è autenticazione ma conferma di protocollo. Il client manda Sec-WebSocket-Key con 16 byte casuali in base64. Il server vi concatena la stringa fissa 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, ne calcola lo SHA-1, codifica il risultato in base64 e lo restituisce in Sec-WebSocket-Accept. Serve a garantire che la controparte stia davvero parlando WebSocket e non stia rimbalzando una richiesta HTTP qualunque. Sul contenuto non garantisce nulla.

Il problema dei proxy trasparenti

La prima bozza diffusa, nota come Hixie-76, non mascherava i dati del client. Nel novembre 2010 Lin-Shung Huang, Eric Chen, Adam Barth, Eric Rescorla e Collin Jackson portarono al gruppo di lavoro un’analisi — poi pubblicata come Talking to Yourself for Fun and Profit — di un attacco contro i proxy trasparenti di rete.

Il meccanismo è questo. Molti proxy intermedi ispezionano il traffico sulla porta 80 e lo trattano come HTTP, anche quando l’handshake ha già negoziato un upgrade che il proxy non capisce. Un attaccante che controlla del JavaScript dentro una pagina apre una connessione WebSocket e scrive sul canale dei byte che il proxy, leggendoli come se fossero una richiesta HTTP, interpreta come un GET verso una risorsa di terze parti. Il proxy esegue la richiesta contraffatta e mette in cache la risposta dell’attaccante sotto l’URL legittimo. Da quel momento ogni client servito da quel proxy riceve il contenuto avvelenato. Gli autori stimarono un costo sotto il dollaro per esecuzione, e non riuscirono a identificare i proxy difettosi proprio perché trasparenti.

La reazione fu immediata. Firefox 4, dalla beta 8 (dicembre 2010), disabilitò WebSocket; Opera fece lo stesso. Mozilla e Opera chiesero al gruppo hybi una modifica del protocollo prima di riattivarlo.

L’architettura della contromisura

La soluzione scelta dal gruppo di lavoro toglie all’attaccante il controllo sui byte che finiscono sul filo. Ogni frame mandato dal client porta un bit di mask attivo e una chiave di mascheramento di 32 bit, estratta da una sorgente con entropia forte e diversa a ogni frame. Il payload va in XOR con la chiave ripetuta byte per byte; il server, che riceve la chiave dentro il frame stesso, applica lo stesso XOR e ricostruisce i dati.

 0               1               2               3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| op    |M| Payload len |    Extended payload length    |
|I|S|S|S| code  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |                               |
+-+-+-+-+-------+-+-------------+- - - - - - - - - - - - - - - -+
|     Masking-key (presente se MASK=1)          | Payload Data  |
+-----------------------------------------------+- - - - - - - -+

Siccome la chiave è casuale e cambia a ogni frame, l’attaccante non può più predeterminare la sequenza di byte che il proxy leggerebbe: non può comporre un GET valido né scegliere l’URL da avvelenare. Il framing definisce gli opcode per i dati (0x1 text, 0x2 binary) e per il controllo del canale (0x8 close, 0x9 ping, 0xA pong), con il bit FIN che governa la frammentazione dei messaggi.

Il punto critico

Il mascheramento è asimmetrico: obbligatorio dal client al server, assente nella direzione opposta. La ragione sta nel modello di minaccia. Il pericolo viene dal codice ostile che gira nel browser di una vittima dietro un proxy condiviso; è il client che si può indurre a scrivere byte scelti dall’attaccante. Il server non sta sotto il JavaScript di terze parti, quindi in quella direzione non c’è alcun payload da neutralizzare.

Da qui segue che il mascheramento non è cifratura e non aggiunge riservatezza: la chiave viaggia in chiaro dentro il frame stesso. Chi vuole confidenzialità e integrità contro un intermediario attivo deve usare wss:// su TLS, non affidarsi al mask. Il mask risolve un solo problema — impedire al mittente di scegliere i byte grezzi sul filo — e lo risolve al prezzo di una passata XOR per frame su entrambi i lati.

Implicazioni e limiti

Per chi scrive un server WebSocket la regola è vincolante: un server conforme chiude la connessione se riceve dal client un frame non mascherato, e un client conforme chiude se riceve dal server un frame mascherato. Le prime librerie del 2011 — fra cui ws per Node.js e i fallback di Socket.IO per i browser senza supporto nativo — incorporano già la regola.

Il costo si misura. Lo XOR su ogni frame in uscita dal client e su ogni frame in ingresso sul server consuma cicli proporzionali alla dimensione del payload; sui flussi ad alta frequenza si sente. Resta un compromesso accettato per tenere WebSocket usabile attraverso l’infrastruttura HTTP già installata, che non si aggiorna al ritmo dello standard.

Il limite di fondo è che il mascheramento difende l’infrastruttura, non l’applicazione. Non autentica le parti, non protegge dai messaggi cross-origin verso un server che non controlla l’header Origin, non sostituisce TLS. È una contromisura mirata a una classe di proxy difettosi diffusi nel 2011, scritta nello standard perché quei proxy esistono e non spariranno tanto presto.


Immagine di copertina: Diagramma di sequenza tra un client (laptop) e un server: handshake con HTTP Upgrade, messaggi bidirezionali su connessione… — diagramma di Brivadeneira, CC BY-SA 4.0 — https://commons.wikimedia.org/wiki/File:Websocket_connection.png