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 constructor and a request like the following is sent:
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: (). The status code will be 101 "Switching Protocols", and the TCP connection stays open.
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 behaviour. Be sure to test for the standard type of vulnerabilities within fields of a WebSocket message.
SocketIO
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
Dependencies
npm install ws
server.js
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');
WebSocket Client - JavaScript
client.js
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);
});
WebSocketClient.js
class WebSocketClient {
constructor(url) {
this.socket = new WebSocket(url);
this._messageQueue = [];
this._pendingResolvers = [];
this.socket.addEventListener('message', (event) => {
const message = event.data;
if (this._pendingResolvers.length > 0) {
this._pendingResolvers.shift()(message);
} else {
this._messageQueue.push(message);
}
});
}
send(message) {
if (this.socket.readyState === WebSocket.OPEN) {
this.socket.send(message);
} else {
throw new Error("WebSocket is not open");
}
}
recv() {
return new Promise((resolve) => {
if (this._messageQueue.length > 0) {
resolve(this._messageQueue.shift());
} else {
this._pendingResolvers.push(resolve);
}
});
}
close() {
if (this.socket.readyState === WebSocket.OPEN) {
this.socket.close();
} else {
throw new Error("WebSocket is already closed or not opened");
}
}
}
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()
with WebSocketClient("ws://localhost:1337") as ws:
print(f"Received: {ws.recv()!r}")
ws.send("Hello from Python!")
print(f"Received: {ws.recv()!r}")
SocketIO Server
Dependencies
npm install socket.io express
server-socketio.js
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}`);
});
SocketIO Client - JavaScript
Importing (Browser)
<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>
Dependencies (NodeJS)
npm install socket.io-client
Importing (NodeJS)
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');
});
SocketIO Client - Python
Dependencies
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!')
Cross-Site WebSocket Hijacking
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.
A common wrapper around WebSockets in the wild is . 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 that can be seen as completely different connections to different applications. Almost always, this is implicitly the main namespace (/). A namespace contains which can be seen as types of .
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 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 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.