From dd2c239aa1665c46731c964cea53af61448bf8fe Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 16 Mar 2026 20:45:05 -0700 Subject: [PATCH] Have Worker rate limit ws messages (#3449) ## Description: Prevent client from spamming ws messages before joining a game server. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: evan --- src/server/Worker.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/server/Worker.ts b/src/server/Worker.ts index e72276819..497904334 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -3,6 +3,7 @@ import express, { NextFunction, Request, Response } from "express"; import rateLimit from "express-rate-limit"; import http from "http"; import ipAnonymize from "ip-anonymize"; +import { RateLimiter } from "limiter"; import path from "path"; import { fileURLToPath } from "url"; import { WebSocket, WebSocketServer } from "ws"; @@ -289,6 +290,11 @@ export async function startWorker() { : // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing forwarded || req.socket.remoteAddress || "unknown"; + if (!getWsIpLimiter(ip).tryRemoveTokens(1)) { + ws.close(1008, "Rate limit exceeded"); + return; + } + try { // Parse and handle client messages const parsed = ClientMessageSchema.safeParse( @@ -609,3 +615,21 @@ function generateGameIdForWorker(): GameID | null { log.warn(`Failed to generate game ID for worker ${workerId}`); return null; } + +// Per-IP rate limiter for pre-join WebSocket messages. +// Prevents unauthenticated connections from spamming messages +// (e.g. pings) before joining a game. +const wsIpLimiters = new Map(); +function getWsIpLimiter(ip: string): RateLimiter { + let limiter = wsIpLimiters.get(ip); + if (!limiter) { + limiter = new RateLimiter({ + tokensPerInterval: 5, + interval: "second", + }); + wsIpLimiters.set(ip, limiter); + } + return limiter; +} +// Clean up stale IP limiters every 10 minutes +setInterval(() => wsIpLimiters.clear(), 10 * 60 * 1000);