fix: harden leave room rate limiting

This commit is contained in:
Timo
2026-07-02 00:14:55 +02:00
parent 9fe83f89b2
commit c022f6f490
11 changed files with 52 additions and 32 deletions
+5 -1
View File
@@ -22,6 +22,7 @@ import {
healthCounts,
adminMetricsAuthCounts,
roomListCooldowns,
leaveRoomCounts,
rateLimitDenied,
checkAuthRate,
recordAuthFailure,
@@ -115,7 +116,8 @@ app.get('/health', (req, res) => {
health: healthCounts.size,
adminMetricsAuth: adminMetricsAuthCounts.size,
authFailures: failedAuthAttempts.size,
roomList: roomListCooldowns.size
roomList: roomListCooldowns.size,
leaveRoom: leaveRoomCounts.size
},
rateLimitDenied
})
@@ -664,6 +666,7 @@ io.on('connection', (socket) => {
socket.on(EVENTS.LEAVE_ROOM, () => {
if (!checkLeaveRoomRate(socket.id)) {
log('SECURITY', `LEAVE_ROOM rate limit exceeded for socket: ${socket.id}`);
socket.disconnect(true);
return;
}
try {
@@ -844,6 +847,7 @@ io.on('connection', (socket) => {
socket.on('disconnect', () => {
eventCounts.delete(socket.id);
roomListCooldowns.delete(socket.id);
leaveRoomCounts.delete(socket.id);
const mapping = socketToRoom.get(socket.id);
if (mapping) {
try {
+4 -2
View File
@@ -97,14 +97,16 @@ export function buildHealthPayload({
health: rateLimitSizes.health || 0,
adminMetricsAuth: rateLimitSizes.adminMetricsAuth || 0,
authFailures: rateLimitSizes.authFailures || 0,
roomList: rateLimitSizes.roomList || 0
roomList: rateLimitSizes.roomList || 0,
leaveRoom: rateLimitSizes.leaveRoom || 0
},
denied: {
connections: rateLimitDenied.connections || 0,
events: rateLimitDenied.events || 0,
health: rateLimitDenied.health || 0,
adminMetricsAuth: rateLimitDenied.adminMetricsAuth || 0,
roomList: rateLimitDenied.roomList || 0
roomList: rateLimitDenied.roomList || 0,
leaveRoom: rateLimitDenied.leaveRoom || 0
}
},
memory: {
+1 -1
View File
@@ -164,7 +164,7 @@ export function startRateLimitCleanup(io) {
}
}
for (const [socketId, entry] of leaveRoomCounts.entries()) {
if (now > entry.resetTime) {
if (now > entry.resetTime || !io.sockets.sockets.has(socketId)) {
leaveRoomCounts.delete(socketId);
}
}
+12 -8
View File
@@ -4,21 +4,20 @@ import {
LEAVE_ROOM_RATE_LIMIT,
LEAVE_ROOM_RATE_WINDOW_MS,
rateLimitDenied,
leaveRoomCounts
leaveRoomCounts,
clearRateLimitMaps
} from './rate-limiter.js';
describe('LEAVE_ROOM Rate Limiter', () => {
const testSocketId = 'test-socket-123';
beforeEach(() => {
// Reset state before each test
leaveRoomCounts.clear();
clearRateLimitMaps();
rateLimitDenied.leaveRoom = 0;
});
afterEach(() => {
// Clean up after each test
leaveRoomCounts.clear();
clearRateLimitMaps();
});
it('should allow LEAVE_ROOM within limit', () => {
@@ -80,19 +79,24 @@ describe('LEAVE_ROOM Rate Limiter', () => {
});
it('should increment rateLimitDenied counter on block', () => {
// Fill up to the limit
for (let i = 0; i < LEAVE_ROOM_RATE_LIMIT; i++) {
checkLeaveRoomRate(testSocketId);
}
// First block
checkLeaveRoomRate(testSocketId);
expect(rateLimitDenied.leaveRoom).toBe(1);
// Second block
checkLeaveRoomRate(testSocketId);
expect(rateLimitDenied.leaveRoom).toBe(2);
});
it('should be cleared by the shared reset helper', () => {
checkLeaveRoomRate(testSocketId);
expect(leaveRoomCounts.size).toBe(1);
clearRateLimitMaps();
expect(leaveRoomCounts.size).toBe(0);
});
});
describe('Rate Limit Constants', () => {