mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Stall login/pw reset to prevent email leaking (#7105)
This commit is contained in:
@@ -18,6 +18,8 @@ import { ActivityService } from '../services/activity';
|
||||
import { AbstractServiceOptions, Accountability, Action, SchemaOverview, Session } from '../types';
|
||||
import { SettingsService } from './settings';
|
||||
import { merge } from 'lodash';
|
||||
import { performance } from 'perf_hooks';
|
||||
import { stall } from '../utils/stall';
|
||||
|
||||
type AuthenticateOptions = {
|
||||
email: string;
|
||||
@@ -52,6 +54,9 @@ export class AuthenticationService {
|
||||
async authenticate(
|
||||
options: AuthenticateOptions
|
||||
): Promise<{ accessToken: any; refreshToken: any; expires: any; id?: any }> {
|
||||
const STALL_TIME = 100;
|
||||
const timeStart = performance.now();
|
||||
|
||||
const settingsService = new SettingsService({
|
||||
knex: this.knex,
|
||||
schema: this.schema,
|
||||
@@ -97,8 +102,10 @@ export class AuthenticationService {
|
||||
emitStatus('fail');
|
||||
|
||||
if (user?.status === 'suspended') {
|
||||
await stall(STALL_TIME, timeStart);
|
||||
throw new UserSuspendedException();
|
||||
} else {
|
||||
await stall(STALL_TIME, timeStart);
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
}
|
||||
@@ -125,17 +132,20 @@ export class AuthenticationService {
|
||||
if (password !== undefined) {
|
||||
if (!user.password) {
|
||||
emitStatus('fail');
|
||||
await stall(STALL_TIME, timeStart);
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
if ((await argon2.verify(user.password, password)) === false) {
|
||||
emitStatus('fail');
|
||||
await stall(STALL_TIME, timeStart);
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
}
|
||||
|
||||
if (user.tfa_secret && !otp) {
|
||||
emitStatus('fail');
|
||||
await stall(STALL_TIME, timeStart);
|
||||
throw new InvalidOTPException(`"otp" is required`);
|
||||
}
|
||||
|
||||
@@ -144,6 +154,7 @@ export class AuthenticationService {
|
||||
|
||||
if (otpValid === false) {
|
||||
emitStatus('fail');
|
||||
await stall(STALL_TIME, timeStart);
|
||||
throw new InvalidOTPException(`"otp" is invalid`);
|
||||
}
|
||||
}
|
||||
@@ -193,6 +204,8 @@ export class AuthenticationService {
|
||||
await loginAttemptsLimiter.set(user.id, 0, 0);
|
||||
}
|
||||
|
||||
await stall(STALL_TIME, timeStart);
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
|
||||
@@ -20,6 +20,7 @@ import { AuthenticationService } from './authentication';
|
||||
import { ItemsService, MutationOptions } from './items';
|
||||
import { MailService } from './mail';
|
||||
import { SettingsService } from './settings';
|
||||
import { stall } from '../utils/stall';
|
||||
|
||||
export class UsersService extends ItemsService {
|
||||
knex: Knex;
|
||||
@@ -345,8 +346,14 @@ export class UsersService extends ItemsService {
|
||||
}
|
||||
|
||||
async requestPasswordReset(email: string, url: string | null, subject?: string | null): Promise<void> {
|
||||
const STALL_TIME = 500;
|
||||
const timeStart = performance.now();
|
||||
|
||||
const user = await this.knex.select('id').from('directus_users').where({ email }).first();
|
||||
if (!user) throw new ForbiddenException();
|
||||
if (!user) {
|
||||
await stall(STALL_TIME, timeStart);
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
const mailService = new MailService({
|
||||
schema: this.schema,
|
||||
@@ -375,6 +382,8 @@ export class UsersService extends ItemsService {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await stall(STALL_TIME, timeStart);
|
||||
}
|
||||
|
||||
async resetPassword(token: string, password: string): Promise<void> {
|
||||
|
||||
36
api/src/utils/stall.ts
Normal file
36
api/src/utils/stall.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { performance } from 'perf_hooks';
|
||||
|
||||
/**
|
||||
* Wait a specific time to meet the stall ms. Useful in cases where you need to make sure that every
|
||||
* path in a function takes at least X ms (for example authenticate).
|
||||
*
|
||||
* @param {number} ms - Stall time to wait until
|
||||
* @param {number} start - Current start time of the function
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* const STALL_TIME = 100;
|
||||
*
|
||||
* // Function will always take (at least) 100ms
|
||||
* async function doSomething() {
|
||||
* const timeStart = performance.now();
|
||||
*
|
||||
* if (something === true) {
|
||||
* await heavy();
|
||||
* }
|
||||
*
|
||||
* stall(STALL_TIME, timeStart);
|
||||
* return 'result';
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export async function stall(ms: number, start: number) {
|
||||
const now = performance.now();
|
||||
const timeElapsed = now - start;
|
||||
const timeRemaining = ms - timeElapsed;
|
||||
|
||||
if (timeRemaining <= 0) return;
|
||||
|
||||
return new Promise((resolve) => setTimeout(resolve, timeRemaining));
|
||||
}
|
||||
Reference in New Issue
Block a user