From b8833b4e607edeb8a49a8f69c4b3a102e1a067f2 Mon Sep 17 00:00:00 2001 From: Ralksta <16941050+ralksta@users.noreply.github.com> Date: Wed, 1 Apr 2026 12:45:46 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Fix=20global=20DoS=20vu?= =?UTF-8?q?lnerability=20in=20rate=20limiter=20(FIFO=20eviction)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- lib/__tests__/rate-limit.test.ts | 4 ++-- lib/rate-limit.ts | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/__tests__/rate-limit.test.ts b/lib/__tests__/rate-limit.test.ts index d02e3fd..3b64cdc 100644 --- a/lib/__tests__/rate-limit.test.ts +++ b/lib/__tests__/rate-limit.test.ts @@ -59,9 +59,9 @@ describe('checkRateLimit', () => { }); it('does not crash when store is at capacity and still accepts new IPs', () => { - // Fill the store beyond MAX_STORE_ENTRIES (1 000 in test env or 10 000 in prod) + // Fill the store beyond MAX_STORE_ENTRIES (10_000 in prod) // by hammering with unique IPs; evictIfFull must silently evict stale entries. - const uniqueIps = Array.from({ length: 200 }, (_, i) => `evict-test-ip-${i}-${Date.now()}`); + const uniqueIps = Array.from({ length: 10005 }, (_, i) => `evict-test-ip-${i}-${Date.now()}`); for (const ip of uniqueIps) { checkRateLimit(ip, 5); } diff --git a/lib/rate-limit.ts b/lib/rate-limit.ts index c27ae23..99f204c 100644 --- a/lib/rate-limit.ts +++ b/lib/rate-limit.ts @@ -63,8 +63,12 @@ export function checkRateLimit( // New window or expired if (!entry || now > entry.expiresAt) { if (store.size >= MAX_STORE_ENTRIES) { - // Store is completely full with active IPs -> block to prevent memory exhaustion & rate limit bypass - return { success: false, remaining: 0, resetAt: now + windowMs }; + // Store is full. Evict the oldest entry (FIFO) to prevent a global DoS + // where an attacker fills the store with spoofed IPs, locking out legitimate users. + const oldestKey = store.keys().next().value; + if (oldestKey !== undefined) { + store.delete(oldestKey); + } } const resetAt = now + windowMs; store.set(key, { count: 1, expiresAt: resetAt });