fix(server): prevent login timing oracle by always running bcrypt

Always call compareBcrypt in the login path regardless of whether the
email is registered. When no user is found, a dummy hash is used so the
bcrypt KDF still runs and response latency is constant, making it
impossible to enumerate valid email addresses by measuring response time.
This commit is contained in:
timonrieger
2026-04-29 22:53:16 +02:00
parent c1bd7a116b
commit 8011adadb6
2 changed files with 10 additions and 9 deletions
+4
View File
@@ -42,6 +42,10 @@ export const VECTOR_INDEX_TABLES = {
export const VECTORCHORD_LIST_SLACK_FACTOR = 1.2;
export const SALT_ROUNDS = 10;
// Syntactically valid bcrypt hash (cost 10) used as a dummy in login() so that
// bcrypt.compareSync always runs even when the email is not registered, preventing
// timing-based user enumeration.
export const LOGIN_DUMMY_HASH = '$2b$10$abcdefghijklmnopqrstuuABCDEFGHIJKLMNOPQRSTUVWXYZabcde';
export const IWorker = 'IWorker';
+6 -9
View File
@@ -2,7 +2,7 @@ import { BadRequestException, ForbiddenException, Injectable, UnauthorizedExcept
import { parse } from 'cookie';
import { DateTime } from 'luxon';
import { IncomingHttpHeaders } from 'node:http';
import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
import { LOGIN_DUMMY_HASH, LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
import { AuthSharedLink, AuthUser, UserAdmin } from 'src/database';
import {
AuthDto,
@@ -62,15 +62,12 @@ export class AuthService extends BaseService {
throw new UnauthorizedException('Password login has been disabled');
}
let user = await this.userRepository.getByEmail(dto.email, { withPassword: true });
if (user) {
const isAuthenticated = this.validateSecret(dto.password, user.password);
if (!isAuthenticated) {
user = undefined;
}
}
const user = await this.userRepository.getByEmail(dto.email, { withPassword: true });
// Always run bcrypt so response time is constant regardless of whether the email
// is registered, preventing timing-based user enumeration.
const authenticated = this.cryptoRepository.compareBcrypt(dto.password, user?.password ?? LOGIN_DUMMY_HASH);
if (!user) {
if (!user || !user.password || !authenticated) {
this.logger.warn(`Failed login attempt for user ${dto.email} from ip address ${details.clientIp}`);
throw new UnauthorizedException('Incorrect email or password');
}