WebSocket security in AI-generated real-time apps
Von Seaworthy · 18. Juni 2026
Photo by Scott Rodgerson on Unsplash
Live dashboards and real-time chat are everywhere now. The moment a product needs anything streaming, a WebSocket shows up. AI coding tools are very good at generating that scaffolding fast.
Too fast, sometimes.
The boilerplate that comes out of ChatGPT or Copilot usually works. You get a server that upgrades HTTP connections, broadcasts messages, maybe handles reconnection. What you don't get is the security layer. That part gets skipped, not because the AI can't write it, but because the typical prompt doesn't ask for it.
The upgrade request nobody checks
HTTP to WebSocket starts with a handshake. The client sends an Upgrade: websocket header, the server agrees, and from that point you have a persistent connection. Most AI-generated servers agree to that upgrade without asking who's knocking.
Here's the kind of code that comes out of a standard "build me a WebSocket server" prompt:
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws) => {
ws.on('message', (message) => {
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
});
});
This is a broadcast server. Anyone who connects gets everyone's messages. There's no authentication check on connection. No session validation. No origin check. Any page on the internet can open a WebSocket to this server if it knows the port.
That last point matters more than people realize. Browsers enforce same-origin policy on most requests, but WebSocket upgrades are not restricted by default. A malicious site can connect to ws://yourapp.com:8080 from a visitor's browser and start receiving their data.
Origin validation is not optional
The Upgrade request includes an Origin header. Your server should be checking it.
const wss = new WebSocketServer({ noServer: true });
server.on('upgrade', (request, socket, head) => {
const origin = request.headers.origin;
const allowed = ['https://yourapp.com', 'https://www.yourapp.com'];
if (!allowed.includes(origin)) {
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
socket.destroy();
return;
}
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request);
});
});
This is not complicated code. It's also almost never in the AI-generated version, because it requires knowing the allowed origins at generation time, and that means the prompt has to include them.
Authentication before the handshake
The other common mistake is treating WebSocket auth like REST auth: send a message after connecting, include a token, and the server validates it. This is backwards.
By the time the client sends that first message, the connection is already open. If your server does any work before checking credentials, an unauthenticated client gets to participate in that work. And if broadcast logic isn't careful, they'll see responses meant for authenticated users.
Authentication should happen at the HTTP upgrade stage. Parse a cookie or a token from the query string and validate it there, before the connection is established.
server.on('upgrade', (request, socket, head) => {
const token = new URL(request.url, 'https://x').searchParams.get('token');
verifyJWT(token)
.then((user) => {
wss.handleUpgrade(request, socket, head, (ws) => {
ws.user = user;
wss.emit('connection', ws, request);
});
})
.catch(() => {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
});
});
Tokens in query strings show up in server logs. For production, prefer cookies. The principle stays the same: validate before you upgrade, not after.
The broadcast problem
Broadcast-everything is the default in almost every tutorial and AI-generated snippet. It works fine for a public chat room. It's a data exposure bug for anything else.
If your app has user-specific data or permission levels, you need to track which connections belong to which users and filter accordingly. AI tools won't do this unless you tell them the data model. When you prompt for "add WebSocket support," the model doesn't know your users have different permission levels unless you say so explicitly.
This is how vibe-coded apps leak data. Not through spectacular exploits, just through code that was never told there were secrets to keep.
Rate limiting on the connection
WebSocket connections are persistent, which means a single connected client can send thousands of messages per second without touching the rate limits you have on your HTTP endpoints.
AI-generated servers almost never include per-connection rate limiting. Even a simple counter per connection will stop the obvious abuse:
const messageCount = new Map();
wss.on('connection', (ws) => {
messageCount.set(ws, 0);
ws.on('message', (message) => {
const count = (messageCount.get(ws) || 0) + 1;
messageCount.set(ws, count);
if (count > 100) {
ws.close(1008, 'Rate limit exceeded');
return;
}
// handle message
});
ws.on('close', () => messageCount.delete(ws));
});
A production version would reset counts on a rolling interval. But even this catches the low-effort abuse that shows up in real logs.
What the model doesn't know
AI coding tools optimize for "does this work." Security properties are invisible at generation time. The model can't see that your app will eventually have paying users, or that the data flowing through these connections is sensitive.
You have to ask. Specifically. "Add WebSocket support with authentication, origin validation, and rate limiting" gets very different output than "add WebSocket support." The model will write the security code if you describe what you need. It just won't volunteer it.
Seaworthy flags missing WebSocket authentication and open broadcast patterns as part of its security and data exposure checks, so these gaps surface before they reach production.
Dieser Artikel wurde von KI generiert und fasst öffentlich verfügbare Quellen zusammen.
Seaworthy scannt dein Repository nach den Problemen, die in Artikeln wie diesem behandelt werden.
Sicherheitslücken, offengelegte Geheimnisse und Fehlkonfigurationen — erkannt, bevor du bereitstellst. Kostenlos, kein Konto nötig.