fix(auth): global login rate limit — XFF was spoofable and Map unbounded
The login rate limiter keyed on the client-controlled x-forwarded-for
header, letting an attacker rotate XFF for unlimited password guesses
while also growing the failures Map unboundedly (memory DoS). Since
this is a single-password instance, replace with one global
{count, resetAt} tracker per app instance: check the 15-min window and
reject at >=10 failures before verifying the password, increment on
failure, reset on success.
This commit is contained in:
@@ -13,7 +13,7 @@ const MAX_FAILURES = 10;
|
||||
export type AuthDeps = { db: Db };
|
||||
|
||||
export function authRoutes({ db }: AuthDeps) {
|
||||
const failures = new Map<string, { count: number; resetAt: number }>();
|
||||
let failures = { count: 0, resetAt: 0 };
|
||||
const app = new Hono();
|
||||
|
||||
const getHash = async () =>
|
||||
@@ -39,19 +39,18 @@ export function authRoutes({ db }: AuthDeps) {
|
||||
});
|
||||
|
||||
app.post("/login", async (c) => {
|
||||
const ip = c.req.header("x-forwarded-for") ?? "local";
|
||||
const f = failures.get(ip);
|
||||
if (f && f.count >= MAX_FAILURES && Date.now() < f.resetAt) return c.json({ error: "too many attempts" }, 429);
|
||||
const now = Date.now();
|
||||
if (now > failures.resetAt) failures = { count: 0, resetAt: now + WINDOW_MS };
|
||||
if (failures.count >= MAX_FAILURES) return c.json({ error: "too many attempts" }, 429);
|
||||
|
||||
const body = PasswordBody.safeParse(await c.req.json().catch(() => null));
|
||||
const hash = await getHash();
|
||||
const ok = body.success && !!hash && (await Bun.password.verify(body.data.password, hash));
|
||||
if (!ok) {
|
||||
const cur = f && Date.now() < f.resetAt ? f : { count: 0, resetAt: Date.now() + WINDOW_MS };
|
||||
failures.set(ip, { count: cur.count + 1, resetAt: cur.resetAt });
|
||||
failures.count += 1;
|
||||
return c.json({ error: "invalid password" }, 401);
|
||||
}
|
||||
failures.delete(ip);
|
||||
failures.count = 0;
|
||||
const id = randomBytes(32).toString("base64url");
|
||||
await db.insert(sessions).values({ id, createdAt: Date.now(), expiresAt: Date.now() + SESSION_MS });
|
||||
setCookie(c, "helios_session", id, { httpOnly: true, sameSite: "Lax", path: "/", maxAge: SESSION_MS / 1000 });
|
||||
|
||||
Reference in New Issue
Block a user