Back to Blog

WebSocket CORS in Browser Games: Security and Implementation Guide

Learn how WebSockets bypass CORS restrictions in browser games, understand security implications, and implement safe real-time multiplayer connections.

WebSocket CORS in Browser Games: Security and Implementation Guide

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

  1. WebSockets and the Same-Origin Policy
  2. Why WebSockets Bypass CORS
  3. Security Implications
  4. Implementing Secure WebSockets
  5. Server-Side Origin Validation
  6. Content Security Policy
  7. Common Game Scenarios
  8. 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:

  1. Sends the request to the server
  2. Receives the response
  3. Checks for CORS headers
  4. Blocks the response if CORS headers are missing

With WebSockets:

  1. The browser initiates an HTTP upgrade request
  2. The connection upgrades to WebSocket protocol (WS/WSS)
  3. No CORS headers are checked on the response
  4. 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:

  1. Different Protocol: After the initial handshake, communication happens over WS/WSS, not HTTP
  2. No Response Headers: There’s no HTTP response to check for CORS headers
  3. 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:

  1. Open a WebSocket to your game server
  2. Use the victim’s cookies/credentials
  3. 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:

  1. Third-Party Cookie Blocking: Firefox’s Total Cookie Protection isolates cookies per site
  2. Private Network Access: Chrome (130+) blocks connections from public sites to private IPs
  3. SameSite Cookies: Cookies marked SameSite=Strict or SameSite=Lax aren’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

  1. Open DevTools → Network tab
  2. Filter by “WS” to see WebSocket connections
  3. Click on a connection to see messages
  4. 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:

  1. Always validate the Origin header on your server
  2. Use token-based authentication instead of cookies
  3. Implement Content Security Policy to restrict allowed connections
  4. 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.

Related blog posts

Create a free Account to fix CORS Errors in Production

Say goodbye to CORS errors and get back to building great web applications. It's free!

CORSPROXY Dashboard