Files
webmail/lib/__tests__/admin-rate-limit.test.ts
Linus Rath 32280b9cc1 fix: cap admin sign-in attempts across all clients (GHSA-7pj2-232x-6698)
The admin login limit keys on the client IP taken from
X-Forwarded-For. With no reverse proxy in front, that header is
whatever the client sends (Next only fills it from the socket when the
request carries none), so each forged address got a fresh bucket of
five guesses. Route handlers cannot see the socket address, so the
limiter now also stops at 50 attempts per 15 minutes across all
clients. `.env.example` says why a proxy that appends the header is
still needed for trustworthy per-IP limits and audit addresses.
2026-09-26 15:07:09 +02:00

45 lines
1.4 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
// The per-IP key is X-Forwarded-For, which a client writes itself when the
// app is reachable without a reverse proxy: a fresh forged address used to
// mean a fresh bucket of five guesses, without end.
async function load() {
vi.resetModules();
return import('@/lib/admin/rate-limit');
}
describe('admin login rate limit', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('allows five attempts per address', async () => {
const { checkRateLimit } = await load();
for (let i = 0; i < 5; i++) expect(checkRateLimit('203.0.113.1').allowed).toBe(true);
expect(checkRateLimit('203.0.113.1').allowed).toBe(false);
expect(checkRateLimit('203.0.113.2').allowed).toBe(true);
});
it('stops rotating forged addresses at the global ceiling', async () => {
const { checkRateLimit } = await load();
let allowed = 0;
for (let i = 0; i < 200; i++) {
if (checkRateLimit(`198.51.100.${i}`).allowed) allowed++;
}
expect(allowed).toBe(50);
expect(checkRateLimit('192.0.2.77').allowed).toBe(false);
});
it('opens again after the window', async () => {
const { checkRateLimit } = await load();
for (let i = 0; i < 60; i++) checkRateLimit(`198.51.100.${i}`);
vi.advanceTimersByTime(15 * 60 * 1000 + 1);
expect(checkRateLimit('192.0.2.77').allowed).toBe(true);
});
});