From a1d7a7536aff699b5717fa0b515e0e286dc3bc8a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 30 May 2026 18:59:34 -0400 Subject: [PATCH] fix(gateway): clamp auth limiter prune intervals --- src/gateway/auth-rate-limit.test.ts | 14 ++++++++++++++ src/gateway/auth-rate-limit.ts | 13 ++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/gateway/auth-rate-limit.test.ts b/src/gateway/auth-rate-limit.test.ts index 0749269db7c..87fbe2e4b13 100644 --- a/src/gateway/auth-rate-limit.test.ts +++ b/src/gateway/auth-rate-limit.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; +import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { AUTH_RATE_LIMIT_SCOPE_DEVICE_TOKEN, AUTH_RATE_LIMIT_SCOPE_HOOK_AUTH, @@ -238,6 +239,19 @@ describe("auth rate limiter", () => { } }); + it("clamps oversized positive auto-prune intervals", () => { + vi.useFakeTimers(); + try { + const setIntervalSpy = vi.spyOn(globalThis, "setInterval"); + + limiter = createAuthRateLimiter({ pruneIntervalMs: Number.MAX_SAFE_INTEGER }); + + expect(setIntervalSpy).toHaveBeenCalledWith(expect.any(Function), MAX_TIMER_TIMEOUT_MS); + } finally { + vi.useRealTimers(); + } + }); + // ---------- undefined / empty IP ---------- it("normalizes undefined IP to 'unknown'", () => { diff --git a/src/gateway/auth-rate-limit.ts b/src/gateway/auth-rate-limit.ts index 844f39fd0db..61a61bc5ab7 100644 --- a/src/gateway/auth-rate-limit.ts +++ b/src/gateway/auth-rate-limit.ts @@ -16,6 +16,7 @@ * {@link createAuthRateLimiter} and pass it where needed. */ +import { resolveTimerTimeoutMs } from "../shared/number-coercion.js"; import { isLoopbackAddress, resolveClientIp } from "./net.js"; // --------------------------------------------------------------------------- @@ -96,12 +97,22 @@ export function normalizeRateLimitClientIp(ip: string | undefined): string { return resolveClientIp({ remoteAddr: ip }) ?? "unknown"; } +function resolvePruneIntervalMs(value: number | undefined): number { + if (value === undefined) { + return PRUNE_INTERVAL_MS; + } + if (Number.isFinite(value) && value <= 0) { + return 0; + } + return resolveTimerTimeoutMs(value, PRUNE_INTERVAL_MS); +} + export function createAuthRateLimiter(config?: RateLimitConfig): AuthRateLimiter { const maxAttempts = config?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS; const windowMs = config?.windowMs ?? DEFAULT_WINDOW_MS; const lockoutMs = config?.lockoutMs ?? DEFAULT_LOCKOUT_MS; const exemptLoopback = config?.exemptLoopback ?? true; - const pruneIntervalMs = config?.pruneIntervalMs ?? PRUNE_INTERVAL_MS; + const pruneIntervalMs = resolvePruneIntervalMs(config?.pruneIntervalMs); const entries = new Map();