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
+3 -3
View File
@@ -890,7 +890,7 @@ describe('POST /api/admin/members', () => {
.where(eq(localCredentials.userId, body.id))
.limit(1);
expect(credRow).toBeDefined();
expect(verifyPassword(credRow.passwordHash, initialPassword)).toBe(true);
expect(await verifyPassword(credRow.passwordHash, initialPassword)).toBe(true);
});
it('Test 2: duplicate username returns 409 — transaction rolls back (no orphaned users row)', async () => {
@@ -967,8 +967,8 @@ describe('POST /api/admin/members', () => {
.where(eq(localCredentials.userId, newMemberId))
.limit(1);
expect(credRow).toBeDefined();
expect(verifyPassword(credRow.passwordHash, newPassword)).toBe(true);
expect(verifyPassword(credRow.passwordHash, 'old-password-123')).toBe(false);
expect(await verifyPassword(credRow.passwordHash, newPassword)).toBe(true);
expect(await verifyPassword(credRow.passwordHash, 'old-password-123')).toBe(false);
});
it('Test 4: non-admin gets 403 on POST /members and POST /members/:id/password', async () => {
+2 -2
View File
@@ -164,7 +164,7 @@ async function getLocalCredentials() {
describe('POST /api/auth/local/login', () => {
it('Test 1: valid username+password → 200 { ok:true } and issueLocalSessionCookie called', async () => {
const { hashPassword } = await getLocalCredentials();
const hash = hashPassword('correcthorse');
const hash = await hashPassword('correcthorse');
mockCredRow = { userId: 5, passwordHash: hash };
const app = await getApp();
@@ -179,7 +179,7 @@ describe('POST /api/auth/local/login', () => {
it('Test 2: wrong password → 401 { error: "Invalid credentials" }', async () => {
const { hashPassword } = await getLocalCredentials();
const hash = hashPassword('correcthorse');
const hash = await hashPassword('correcthorse');
mockCredRow = { userId: 5, passwordHash: hash };
const app = await getApp();
+4 -4
View File
@@ -274,7 +274,7 @@ describe('POST /api/me/password — self-change password (AUTH-LOCAL-09)', () =>
const oldPassword = 'old-password-correct-123';
const newPassword = 'new-password-secure-456';
const storedHash = hashPassword(oldPassword);
const storedHash = await hashPassword(oldPassword);
let updatedHash: string | null = null;
// Mock sequence: resolveUserId (devBypass sets user), then:
@@ -326,15 +326,15 @@ describe('POST /api/me/password — self-change password (AUTH-LOCAL-09)', () =>
// The updatedHash must verify the new password
expect(updatedHash).not.toBeNull();
const { verifyPassword } = await import('../../src/auth/localCredentials.js');
expect(verifyPassword(updatedHash!, newPassword)).toBe(true);
expect(verifyPassword(updatedHash!, oldPassword)).toBe(false);
expect(await verifyPassword(updatedHash!, newPassword)).toBe(true);
expect(await verifyPassword(updatedHash!, oldPassword)).toBe(false);
});
it('Test 2: wrong currentPassword → 403 and update is NOT called', async () => {
const { db } = await import('../../src/db/client.js');
const realPassword = 'real-password-correct-789';
const storedHash = hashPassword(realPassword);
const storedHash = await hashPassword(realPassword);
let updateWasCalled = false;
let callCount = 0;