fix(19): WR-03 make scrypt hashing async (threadpool) to avoid event-loop starvation DoS

This commit is contained in:
Lucas Berger
2026-06-17 20:30:28 -04:00
parent 322929aebe
commit 30ad25c026
8 changed files with 81 additions and 42 deletions
+7 -4
View File
@@ -99,8 +99,10 @@ export function resetLoginAttempts(username: string): void {
// Pre-computed dummy hash used to run verifyPassword on unknown-username paths
// (timing defense — prevents timing-oracle username enumeration, RESEARCH Pitfall 2).
// Computed once at module load time; the actual value is never used for auth.
const DUMMY_HASH = hashPassword('dummy-constant-time-filler-xyzzy');
// WR-03: hashPassword is now async. Kick off the computation once at module load and keep
// the PROMISE; the login handler awaits it. The value is never used for auth — only to make
// the unknown-username path perform the same scrypt work as the known-username path.
const dummyHashPromise: Promise<string> = hashPassword('dummy-constant-time-filler-xyzzy');
// ---------------------------------------------------------------------------
// POST /local/login → POST /api/auth/local/login
@@ -160,9 +162,10 @@ localAuthRouter.post('/local/login', zValidator('json', loginSchema, noEchoHook)
// ALWAYS run verifyPassword — even for unknown usernames — to prevent timing-oracle
// username enumeration (T-19-12 / RESEARCH Pitfall 2). Use a pre-computed dummy hash
// so the scrypt work is always performed regardless of whether username was found.
// WR-03: verifyPassword is async (threadpool scrypt) — await it.
const valid = cred
? verifyPassword(cred.passwordHash, password)
: verifyPassword(DUMMY_HASH, password);
? await verifyPassword(cred.passwordHash, password)
: await verifyPassword(await dummyHashPromise, password);
if (!valid || !cred) {
// Increment failure counter (keyed on username)