WebSockets
Last updated
Last updated
Bypassing reverse proxies using
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.
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:
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.
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:
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.
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.
npm install ws
const WebSocket = require('ws');
const ws = new WebSocket.Server({ port: 8080 });
ws.on('connection', conn => {
console.log('Client connected.');
conn.on('message', message => {
console.log(`Received from client: ${message}`);
conn.send(`Server received: ${message}`);
});
conn.on('close', () => {
console.log('Client disconnected.');
});
conn.send('Welcome to the WebSocket server!');
});
console.log('WebSocket server is running on ws://localhost:1337');
const socket = new WebSocket("ws://localhost:1337");
socket.addEventListener("open", (event) => {
socket.send("Hello Server!");
});
socket.addEventListener("message", (event) => {
console.log("Message from server ", event.data);
});
ws = new WebSocketClient('ws://localhost:8080')
console.log("Received:", await ws.recv())
ws.send("Hello, from JavaScript!")
console.log("Received:", await ws.recv())
ws.close()
pip install websocket-client
with WebSocketClient("ws://localhost:1337") as ws:
print(f"Received: {ws.recv()!r}")
ws.send("Hello from Python!")
print(f"Received: {ws.recv()!r}")
npm install socket.io express
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = new Server(server);
io.on('connection', (socket) => {
console.log('Client connected');
// Emitting events
socket.emit('event1', 'This is sent to the connecting socket only');
io.emit('event2', 'This is sent to all connected sockets');
socket.broadcast.emit('event3', 'This is sent to all sockets except the sender');
// Rooms
socket.join('room1');
io.to('room1').emit('roomEvent', 'Message to room1');
// Listening for events
socket.on('clientEvent', (data) => {
console.log('Received from client:', data);
});
socket.on('disconnect', () => {
console.log('Client disconnected');
});
});
const PORT = 1337;
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
<script src="https://cdn.socket.io/4.8.1/socket.io.js"></script>
<script>
...
</script>
<!-- or -->
<script type="module">
import { io } from "https://cdn.socket.io/4.8.1/socket.io.esm.min.js";
...
</script>
npm install socket.io-client
import { io } from "socket.io-client";
// or
const { io } = require("socket.io-client");
const socket = io("http://localhost:1337");
function recv(socket, event) {
return new Promise((resolve) => {
function handler(data) {
socket.off(event, handler);
resolve(data);
}
socket.on(event, handler);
});
}
socket.on('connect', async () => {
console.log('Connected to server');
socket.on('someEvent', (data) => {
console.log('Received from server:', data);
});
const listener = recv(socket, 'response');
socket.emit('clientEvent', 'Hello from client');
const response = await listener;
console.log('Response received:', response);
socket.close();
});
socket.on('disconnect', () => {
console.log('Disconnected from server');
});
pip install "python-socketio[client]"
import socketio
with socketio.SimpleClient() as socket:
socket.connect('http://localhost:1337')
socket.emit('my message', {'foo': 'bar'})
event = socket.receive()
print(f'received event: "{event[0]}" with arguments {event[1:]}')
@socket.event
def message(data):
print('I received a message!')
@socket.on('my message')
def on_message(data):
print('I received a message!')
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.
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.
Important for this to have any security impact is if the 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.
WebSocket
Client APISocket.IO
library's API methodsSocket.IO-Client
library's API methodspython-socketio
client library