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 });