How to Prevent WebSocket Hijacking in Vibe-Coded Apps
OverMCP Team
Quick answer
WebSocket hijacking occurs when an attacker intercepts or takes over a WebSocket connection due to missing authentication and authorization checks. In vibe-coded apps, this often happens because AI tools generate real-time features without proper origin validation, token verification, or connection lifecycle management. To prevent it, always authenticate WebSocket connections on upgrade, validate the Origin header, and use token-based handshake mechanisms.
What is WebSocket Hijacking?
WebSocket hijacking is a security vulnerability where an attacker can establish or hijack a WebSocket connection to your server, bypassing normal HTTP security controls. Since WebSocket connections are long-lived and often used for real-time data (chat, notifications, live updates), a hijacked connection can lead to data leaks, unauthorized actions, or full account takeover.
In vibe-coded apps—built rapidly with AI tools like Cursor, Bolt.new, or v0—developers often skip proper WebSocket security because AI models generate functional but insecure code. The AI focuses on making the feature work, not on securing the transport layer. This leaves your app open to attacks like:
What to check first
Before deploying a vibe-coded app with WebSockets, run through this checklist:
Origin header on the WebSocket upgrade request?You can use a free AI app security scanner to automatically check for these issues in your vibe-coded app.
Step-by-step fix
1. Authenticate the WebSocket upgrade
Never accept a WebSocket connection without verifying the user's identity. The most common approach is to pass a token (e.g., JWT) as a query parameter during the upgrade request.
Example (Node.js with `ws` library and JWT):
const WebSocket = require('ws');
const jwt = require('jsonwebtoken');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws, req) => {
// Extract token from query string
const params = new URLSearchParams(req.url.split('?')[1]);
const token = params.get('token');
if (!token) {
ws.close(4001, 'Authentication required');
return;
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
ws.user = decoded; // Attach user info to the connection
console.log(`User ${decoded.id} connected`);
} catch (err) {
ws.close(4001, 'Invalid token');
return;
}
// Handle messages
ws.on('message', (message) => {
// Always authorize actions based on ws.user
handleMessage(ws, message);
});
});Client-side (vibe-coded with Cursor or Bolt.new):
const token = localStorage.getItem('jwt');
const ws = new WebSocket(`wss://yourdomain.com/ws?token=${token}`);2. Validate the Origin header
Even with token authentication, an attacker might trick a user's browser into connecting. Check the Origin header to ensure it matches your domain.
wss.on('connection', (ws, req) => {
const origin = req.headers.origin;
const allowedOrigins = ['https://yourdomain.com', 'https://app.yourdomain.com'];
if (!allowedOrigins.includes(origin)) {
ws.close(4002, 'Invalid origin');
return;
}
// ... rest of connection logic
});3. Authorize every message
Don't assume the user who connected is the same user for every message. Check permissions for each action.
function handleMessage(ws, message) {
const data = JSON.parse(message);
switch (data.type) {
case 'send_message':
// Verify ws.user can send to this chat room
if (!canSend(ws.user.id, data.roomId)) {
ws.send(JSON.stringify({ error: 'Forbidden' }));
return;
}
// Process message
break;
// other cases
}
}4. Use secure WebSocket (wss://) in production
Always use wss:// (WebSocket over TLS) in production to encrypt the communication. Most hosting platforms like Vercel support this natively. You can run a Vercel security scanner to verify your deployment.
5. Implement connection limits and timeouts
Prevent resource exhaustion by limiting connections per user and setting idle timeouts.
const connectionsPerUser = new Map();
wss.on('connection', (ws, req) => {
// ... after authentication
const userId = ws.user.id;
if (connectionsPerUser.has(userId) && connectionsPerUser.get(userId) >= 5) {
ws.close(4003, 'Too many connections');
return;
}
connectionsPerUser.set(userId, (connectionsPerUser.get(userId) || 0) + 1);
ws.on('close', () => {
connectionsPerUser.set(userId, connectionsPerUser.get(userId) - 1);
});
// Idle timeout
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
});
// Ping every 30 seconds
setInterval(() => {
wss.clients.forEach((ws) => {
if (!ws.isAlive) {
ws.terminate();
return;
}
ws.isAlive = false;
ws.ping();
});
}, 30000);Common mistakes
How OverMCP helps
Instead of manually checking every connection, you can use continuous security monitoring to automatically scan your vibe-coded app for WebSocket vulnerabilities, leaked secrets, and misconfigurations. OverMCP integrates with your Vercel, GitHub, or direct deployment to catch issues before they reach production.
FAQ
How do I test if my WebSocket is secure?
You can use browser developer tools to inspect the WebSocket handshake. Look for tokens in the query string, check if the Origin header is validated, and try connecting from a different origin using a tool like wscat. Also, run a security headers checker to ensure your server sends proper headers like Sec-WebSocket-Origin.
Can I use cookies for WebSocket authentication?
Yes, but it's less secure because cookies are automatically sent by the browser, making them vulnerable to CSWSH (Cross-Site WebSocket Hijacking). If you use cookies, ensure SameSite=Strict and validate the Origin header. A token in the query string is generally safer.
What if my WebSocket server doesn't support headers?
Some WebSocket libraries don't easily expose the upgrade request headers. If that's the case, send a token as the first message after connection, and close the connection if it's invalid. This is still better than no authentication, but it's vulnerable to a race condition where the server might send data before the auth message arrives. Always prefer header-based authentication.