WebSockets

Bypassing reverse proxies using WebSocket & h2c Smuggling

Description

WebSockets allow two-way communication over a single connection where both the server and client can send messages whenever they like. It is functionally similar to a raw TCP connection sending data to and between, but is wrapped in WebSocket frames and used by the browser.

Protocol

Creating a WebSocket connection starts with an HTTP request. In the browser, you call the WebSocket() constructor and a request like the following is sent:

Request
GET /some/ws HTTP/1.1
Host: example.com
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: ut/YSNzdtIkvnTCSQtTx9g==

The server has a websocket handler for /some/ws, so it responds with a Sec-WebSocket-Accept: header derived from the request's Sec-WebSocket-Key: (source). The status code will be 101 "Switching Protocols", and the TCP connection stays open.

Response
HTTP/1.1 101 Switching Protocols
Connection: upgrade
Upgrade: websocket
Sec-WebSocket-Accept: /fCJAu1M5mY53eHwube2Xl1leKM=

After this handshake, any party can send websocket frames that the other will decode and handle accordingly. On the wire this is a binary protocol, and looks something like this:

Wireshark capture of WebSocket frame with "Hello, world!" text payload

Messages have a few different types:

  • Text data frame: Simple UTF-8 strings as message content

  • Binary data frame: Raw bytes as message content

  • Ping/Pong: Used to keep the connection alive and avoid timeouts

  • Close: The party sending a close frame cannot send more frames after doing so. The other may still send frames, but most often it will automatically send a closing handshake response to end the connection from both sides.

Implementations with WebSockets often work completely differently than regular HTTP endpoints, which may cause them to have less validation or more dangerous behavior. Be sure to test for the standard type of vulnerabilities within fields of a WebSocket message.

SocketIO

A common wrapper around WebSockets in the wild is SocketIO. This has backwards compatability support by falling back on streaming HTTP responses if WebSockets fail for any reason, and has built some more features like session/room management that are common for web applications.

At the highest level, there are namespaces that can be seen as completely different connections to different applications. Almost always, this is implicitly the main namespace (/). A namespace contains rooms which can be seen as types of events.

Only the server can put you into a room, you cannot decide this for yourself. This is often used for authorization, after completing some verification. This puts you into a private room with other connected clients where sensitive information may be shared.

Snippets

WebSocket Server

WebSocket Client - JavaScript

Documentation for JavaScript WebSocket Client API
WebSocketClient.js

WebSocket Client - Python

WebSocketClient.py

SocketIO Server

Documentation for Socket.IO library's API methods

SocketIO Client - JavaScript

Documentation for Socket.IO-Client library's API methods

SocketIO Client - Python

Documentation for python-socketio client library

Cross-Site WebSocket Hijacking

Explanation of the CSWSH technique with labs

WebSocket connections can also be made cross-site, and if these are automatically authenticated by cookies, you can get into a dangerous scenario where an attacker's site can not only send, but also receive messages. This is because CORS doesn't apply to WebSockets, you are always able to read incoming messages cross-origin.

Important for this to have any security impact is if the Same-site rules allow any site to send authentication cookies with requests. Because WebSockets are background requests, the SameSite= attribute needs to be None for Chromium, or unset for Firefox. Note that if you can gain control over a same-site origin like a subdomain or different port with XSS, you can even get SameSite=Strict cookies to be sent.

Protections

Some common protections include:

  • Checking the Origin: header matches a trusted value. Make sure this is no vulnerable prefix/suffix matching, or that dots in a regex match any character.

  • Requiring the authentication token to be sent as a websocket message, not automatically in a cookie during the handshake. The attacker cannot then abuse any authentication because it needs to happen manually.

Last updated