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:
@@ -42,15 +42,18 @@ describe("auth", () => {
|
||||
expect((await app.request("/api/settings")).status).toBe(401);
|
||||
});
|
||||
|
||||
test("11th failed login from one IP is rate-limited", async () => {
|
||||
test("11th failed login is globally rate-limited, even for the correct password", async () => {
|
||||
const app = makeApp();
|
||||
await app.request("/api/setup", json({ password: "hunter2hunter2" }));
|
||||
const hdrs = { "content-type": "application/json", "x-forwarded-for": "10.9.8.7" };
|
||||
let last = 0;
|
||||
for (let i = 0; i < 11; i++) {
|
||||
last = (await app.request("/api/login", { method: "POST", headers: hdrs, body: JSON.stringify({ password: "wrong-wrong-1" }) })).status;
|
||||
last = (await app.request("/api/login", json({ password: "wrong-wrong-1" }))).status;
|
||||
}
|
||||
expect(last).toBe(429);
|
||||
|
||||
// The global window applies to everyone, including a request with the correct password.
|
||||
const blocked = await app.request("/api/login", json({ password: "hunter2hunter2" }));
|
||||
expect(blocked.status).toBe(429);
|
||||
});
|
||||
|
||||
test("unknown /api/* returns 404 when authed, 401 when unauthed", async () => {
|
||||
|
||||
Reference in New Issue
Block a user