Security in Real-Time Applications
Security is paramount in real-time applications due to their persistent connections and bidirectional communication. In this lesson, we'll cover comprehensive security measures to protect WebSocket and Socket.io applications.
WebSocket Security Threats
Real-time applications face unique security challenges including unauthorized access, message tampering, denial of service attacks, and data injection.
Common Threats:
- Cross-Site WebSocket Hijacking (CSWSH): Similar to CSRF, attackers establish unauthorized WebSocket connections
- Message Injection: Malicious clients send crafted messages to exploit server logic
- DoS/DDoS: Overwhelming the server with connection requests or messages
- Man-in-the-Middle: Intercepting unencrypted WebSocket traffic
- Replay Attacks: Re-sending captured messages to perform unauthorized actions
Origin Validation
Origin validation prevents unauthorized domains from connecting to your WebSocket server.
// Origin validation middleware
const allowedOrigins = [
'https://myapp.com',
'https://www.myapp.com',
process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null
].filter(Boolean);
io.use((socket, next) => {
const origin = socket.handshake.headers.origin;
if (!origin) {
return next(new Error('Origin header missing'));
}
if (!allowedOrigins.includes(origin)) {
console.warn(`Rejected connection from unauthorized origin: ${origin}`);
return next(new Error('Unauthorized origin'));
}
next();
});
// WebSocket Server origin validation
const wss = new WebSocket.Server({
verifyClient: (info, callback) => {
const origin = info.origin || info.req.headers.origin;
if (!allowedOrigins.includes(origin)) {
callback(false, 403, 'Forbidden origin');
return;
}
callback(true);
}
});
Authentication and Authorization
Implement robust authentication for WebSocket connections and validate permissions for each action.
// JWT authentication middleware
const jwt = require('jsonwebtoken');
io.use(async (socket, next) => {
try {
const token = socket.handshake.auth.token ||
socket.handshake.headers.authorization?.split(' ')[1];
if (!token) {
throw new Error('Authentication token required');
}
// Verify JWT
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Attach user info to socket
socket.userId = decoded.userId;
socket.username = decoded.username;
socket.roles = decoded.roles || [];
// Optional: Validate token in database
const isValid = await validateTokenInDB(token);
if (!isValid) {
throw new Error('Invalid or expired token');
}
next();
} catch (error) {
console.error('Authentication failed:', error.message);
next(new Error('Authentication failed'));
}
});
// Authorization for specific actions
io.on('connection', (socket) => {
socket.on('admin:broadcast', async (data) => {
// Check if user has admin role
if (!socket.roles.includes('admin')) {
socket.emit('error', {
message: 'Unauthorized: Admin access required'
});
return;
}
// Proceed with admin action
io.emit('announcement', data);
});
socket.on('room:delete', async (roomId) => {
// Check if user owns the room
const room = await Room.findById(roomId);
if (room.ownerId !== socket.userId) {
socket.emit('error', {
message: 'Unauthorized: Only room owner can delete'
});
return;
}
await room.delete();
io.to(roomId).emit('room:deleted');
});
});
Rate Limiting Connections
Rate limiting prevents abuse by limiting the number of connections and messages from a single source.
// Connection rate limiting
const connectionAttempts = new Map();
io.use((socket, next) => {
const ip = socket.handshake.address;
const now = Date.now();
const windowMs = 60000; // 1 minute
const maxAttempts = 10;
// Get or create attempt record
let attempts = connectionAttempts.get(ip) || [];
// Remove old attempts
attempts = attempts.filter(time => now - time < windowMs);
// Check if limit exceeded
if (attempts.length >= maxAttempts) {
const resetTime = Math.ceil((attempts[0] + windowMs - now) / 1000);
return next(new Error(`Too many connections. Try again in ${resetTime}s`));
}
// Record this attempt
attempts.push(now);
connectionAttempts.set(ip, attempts);
next();
});
// Message rate limiting per socket
const messageLimits = new Map();
function rateLimit(socket, eventName, maxMessages, windowMs) {
const key = `${socket.id}:${eventName}`;
const now = Date.now();
let messages = messageLimits.get(key) || [];
messages = messages.filter(time => now - time < windowMs);
if (messages.length >= maxMessages) {
socket.emit('error', {
code: 'RATE_LIMIT_EXCEEDED',
message: `Too many ${eventName} events. Please slow down.`
});
return false;
}
messages.push(now);
messageLimits.set(key, messages);
return true;
}
// Usage
io.on('connection', (socket) => {
socket.on('chat:send', (data) => {
// Allow 10 messages per minute
if (!rateLimit(socket, 'chat:send', 10, 60000)) {
return;
}
// Process message
io.emit('chat:message', data);
});
});
DoS Prevention
Implement measures to prevent denial of service attacks targeting your real-time infrastructure.
// Maximum connection limits
const MAX_CONNECTIONS_PER_USER = 5;
const MAX_TOTAL_CONNECTIONS = 10000;
const userConnections = new Map();
io.use((socket, next) => {
// Check total connections
if (io.engine.clientsCount >= MAX_TOTAL_CONNECTIONS) {
return next(new Error('Server at capacity'));
}
// Check per-user connections
const userId = socket.userId;
const userSockets = userConnections.get(userId) || [];
if (userSockets.length >= MAX_CONNECTIONS_PER_USER) {
return next(new Error('Maximum connections per user exceeded'));
}
next();
});
io.on('connection', (socket) => {
// Track user connections
const userId = socket.userId;
const userSockets = userConnections.get(userId) || [];
userSockets.push(socket.id);
userConnections.set(userId, userSockets);
socket.on('disconnect', () => {
const sockets = userConnections.get(userId) || [];
const index = sockets.indexOf(socket.id);
if (index > -1) {
sockets.splice(index, 1);
}
if (sockets.length === 0) {
userConnections.delete(userId);
} else {
userConnections.set(userId, sockets);
}
});
});
// Message size limits
const MAX_MESSAGE_SIZE = 10 * 1024; // 10KB
io.use((socket, next) => {
socket.use((packet, next) => {
const messageSize = JSON.stringify(packet).length;
if (messageSize > MAX_MESSAGE_SIZE) {
socket.emit('error', {
message: 'Message size exceeds maximum allowed'
});
return next(new Error('Message too large'));
}
next();
});
next();
});
Best Practice: Implement graceful degradation when under attack. Prioritize authenticated users, implement queue systems, and use CDN with DDoS protection for WebSocket endpoints.
Input Validation for Messages
Always validate and sanitize incoming messages to prevent injection attacks and data corruption.
// Comprehensive input validation
const Joi = require('joi');
const validator = require('validator');
const xss = require('xss');
// Schema validation
const chatMessageSchema = Joi.object({
message: Joi.string().min(1).max(1000).required(),
roomId: Joi.string().pattern(/^[a-zA-Z0-9-_]+$/).required(),
replyTo: Joi.string().optional()
});
io.on('connection', (socket) => {
socket.on('chat:send', async (data) => {
try {
// Schema validation
const { error, value } = chatMessageSchema.validate(data);
if (error) {
socket.emit('error', {
message: 'Invalid message format',
details: error.details
});
return;
}
// XSS sanitization
const sanitizedMessage = xss(value.message, {
whiteList: {}, // Remove all HTML
stripIgnoreTag: true
});
// Additional validation
if (validator.isEmpty(sanitizedMessage.trim())) {
socket.emit('error', { message: 'Empty message' });
return;
}
// Check for spam patterns
if (isSpam(sanitizedMessage)) {
socket.emit('error', { message: 'Spam detected' });
logSuspiciousActivity(socket.userId, 'spam');
return;
}
// Verify room membership
const isMember = await verifyRoomMembership(
socket.userId,
value.roomId
);
if (!isMember) {
socket.emit('error', { message: 'Not a room member' });
return;
}
// Process valid message
const messageData = {
id: generateMessageId(),
userId: socket.userId,
username: socket.username,
message: sanitizedMessage,
roomId: value.roomId,
timestamp: Date.now()
};
await saveMessage(messageData);
io.to(value.roomId).emit('chat:message', messageData);
} catch (error) {
console.error('Message processing error:', error);
socket.emit('error', { message: 'Internal error' });
}
});
});
function isSpam(message) {
const spamPatterns = [
/(?:https?:\/\/){2,}/i, // Multiple URLs
/(.)\1{10,}/, // Repeated characters
/\b(?:buy now|click here|free money)\b/i
];
return spamPatterns.some(pattern => pattern.test(message));
}
Encryption with WSS (wss://)
Always use secure WebSocket connections (wss://) in production to encrypt data in transit.
// Setting up WSS with HTTPS server
const https = require('https');
const fs = require('fs');
const { Server } = require('socket.io');
const httpsServer = https.createServer({
key: fs.readFileSync('/path/to/private-key.pem'),
cert: fs.readFileSync('/path/to/certificate.pem'),
ca: fs.readFileSync('/path/to/ca-bundle.pem') // Optional
});
const io = new Server(httpsServer, {
cors: {
origin: 'https://myapp.com',
methods: ['GET', 'POST']
}
});
httpsServer.listen(3000, () => {
console.log('Secure WebSocket server running on wss://localhost:3000');
});
// Client connection
const socket = io('https://myapp.com', {
secure: true,
rejectUnauthorized: true, // Verify SSL certificate
transports: ['websocket'] // Use WebSocket only
});
// Additional encryption for sensitive data
const crypto = require('crypto');
function encryptSensitiveData(data, key) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
encrypted,
iv: iv.toString('hex'),
authTag: authTag.toString('hex')
};
}
function decryptSensitiveData(encryptedData, key) {
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
key,
Buffer.from(encryptedData.iv, 'hex')
);
decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return JSON.parse(decrypted);
}
XSS Prevention in Chat
Prevent Cross-Site Scripting attacks in real-time chat applications by sanitizing user-generated content.
// Client-side XSS prevention
function displayMessage(message) {
const messageElement = document.createElement('div');
messageElement.className = 'message';
// Use textContent instead of innerHTML
const textNode = document.createTextNode(message.content);
messageElement.appendChild(textNode);
// Or use DOMPurify for rich content
const clean = DOMPurify.sanitize(message.content, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
});
messageElement.innerHTML = clean;
document.getElementById('messages').appendChild(messageElement);
}
// Server-side sanitization
const sanitizeHtml = require('sanitize-html');
socket.on('chat:send', (data) => {
const sanitized = sanitizeHtml(data.message, {
allowedTags: ['b', 'i', 'em', 'strong'],
allowedAttributes: {},
disallowedTagsMode: 'escape'
});
// Additional checks
if (containsMaliciousContent(sanitized)) {
logSecurityEvent(socket.userId, 'xss_attempt');
socket.emit('error', { message: 'Invalid content detected' });
return;
}
io.emit('chat:message', {
userId: socket.userId,
message: sanitized,
timestamp: Date.now()
});
});
Security Checklist:
- ✓ Use WSS (wss://) in production
- ✓ Validate origin headers
- ✓ Implement JWT authentication
- ✓ Rate limit connections and messages
- ✓ Validate and sanitize all inputs
- ✓ Set connection and message size limits
- ✓ Log security events for monitoring
- ✓ Implement CORS properly
- ✓ Use security headers (CSP, HSTS)
- ✓ Regular security audits and updates
Practice Exercise:
- Implement a complete authentication system with JWT for Socket.io connections
- Create a rate limiting middleware that tracks both connection attempts and message frequency
- Build an input validation system that sanitizes chat messages while preserving safe formatting
- Set up WSS with proper SSL certificates and origin validation
- Implement a security logging system that tracks suspicious activities and alerts administrators