WebSocket connections begin with an HTTP handshake that includes an Origin header identifying where the request came from. If a server accepts WebSocket…
WebSocket connections begin with an HTTP handshake that includes an Origin header identifying where the request came from. If a server accepts WebSocket connections without validating this header, a malicious website can trick a victim's browser into opening a WebSocket to the target server on the attacker's behalf. This allows the attacker to send and receive messages as if they were the victim, potentially reading sensitive data or performing unauthorized actions.
02How It Happens
When a browser initiates a WebSocket connection, it automatically includes an Origin header in the handshake request. A properly secured WebSocket server should check this header and reject the connection if it does not match an allowlist of trusted domains. Without this validation, any website can initiate a WebSocket handshake to the target server. Because the victim's browser automatically includes authentication cookies with the request, the server treats the connection as legitimate, even though it originated from a malicious third party. This is a form of cross-site request forgery (CSRF) specific to WebSocket protocols.
03Real-World Impact
An attacker can establish a WebSocket connection to a vulnerable server and send or receive messages in the victim's authenticated session. This could allow reading private messages, stealing real-time data, modifying settings, or triggering actions the victim did not authorize. The impact depends on what the WebSocket endpoint does — a chat application could leak conversations, a trading platform could execute unauthorized trades, and a control system could trigger unintended operations.
04Vulnerable & Fixed Patterns
Vulnerable pattern
import asyncio
import websockets
async def handler(websocket, path):
# No origin validation — accepts connections from any domain
async for message in websocket:
await websocket.send(f"Echo: {message}")
async def main():
async with websockets.serve(handler, "0.0.0.0", 8765):
await asyncio.Future()
asyncio.run(main())
Why it's vulnerable: The server accepts all WebSocket connections without checking the Origin header, allowing any website to establish a connection on behalf of a victim's browser.
Fixed pattern
import asyncio
import websockets
from urllib.parse import urlparse
ALLOWED_ORIGINS = {"https://example.com", "https://app.example.com"}
async def handler(websocket, path):
origin = websocket.request_headers.get("Origin")
if origin not in ALLOWED_ORIGINS:
await websocket.close(code=1008, reason="Unauthorized origin")
return
async for message in websocket:
await websocket.send(f"Echo: {message}")
async def main():
async with websockets.serve(handler, "0.0.0.0", 8765):
await asyncio.Future()
asyncio.run(main())
Vulnerable pattern
<?php
// Using a WebSocket library (e.g., Ratchet)
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
class ChatHandler implements MessageComponentInterface {
public function onOpen(ConnectionInterface $conn) {
// No origin validation — accepts all connections
echo "New connection\n";
}
public function onMessage(ConnectionInterface $from, $msg) {
$from->send("Echo: " . $msg);
}
public function onClose(ConnectionInterface $conn) {}
public function onError(ConnectionInterface $conn, \Exception $e) {}
}
Why it's vulnerable: The handler does not inspect or validate the Origin header from the WebSocket handshake, allowing cross-origin connections.
Fixed pattern
<?php
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
class ChatHandler implements MessageComponentInterface {
private $allowedOrigins = ["https://example.com", "https://app.example.com"];
public function onOpen(ConnectionInterface $conn) {
$origin = $conn->httpRequest->getHeader("Origin");
if (!in_array($origin, $this->allowedOrigins, true)) {
$conn->close();
return;
}
echo "Authorized connection from: " . $origin . "\n";
}
public function onMessage(ConnectionInterface $from, $msg) {
$from->send("Echo: " . $msg);
}
public function onClose(ConnectionInterface $conn) {}
public function onError(ConnectionInterface $conn, \Exception $e) {}
}
05Prevention Checklist
Define an explicit allowlist of trusted origins (domains) that are permitted to establish WebSocket connections to your server.
Validate the Origin header on every WebSocket handshake request before accepting the connection.
Reject connections from origins not on the allowlist with an appropriate HTTP error code (e.g., 403 Forbidden or WebSocket close code 1008).
Use HTTPS and WSS (WebSocket Secure) to prevent origin headers from being stripped or modified in transit.
Document which origins are allowed and review the list regularly as your application's trusted domains change.
Test WebSocket endpoints by attempting to connect from different domains and verify that only authorized origins succeed.
06Signs You May Already Be Affected
Check your WebSocket server logs for connection attempts from unexpected origins or domains. If you see successful connections paired with Origin headers that do not match your application's domain, an attacker may be exploiting this weakness. Additionally, if users report unexpected messages, data changes, or actions they did not perform while using your application, investigate whether unauthorized WebSocket connections are being established.