From 0086f2f19f636140da0471ad87a1ac1938b95177 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Wed, 28 Sep 2022 10:24:43 +0200 Subject: [PATCH] Fix: prevent password reset email flooding (#2076) * fix: prevent password reset email flooding * feat: add tests to user service for password reset --- src/lib/services/user-service.test.ts | 106 ++++++++++++++++++++++++++ src/lib/services/user-service.ts | 10 +++ 2 files changed, 116 insertions(+) diff --git a/src/lib/services/user-service.test.ts b/src/lib/services/user-service.test.ts index 0bec13fbb8..8f8568e076 100644 --- a/src/lib/services/user-service.test.ts +++ b/src/lib/services/user-service.test.ts @@ -1,3 +1,4 @@ +import { URL } from 'url'; import UserService from './user-service'; import UserStoreMock from '../../test/fixtures/fake-user-store'; import EventStoreMock from '../../test/fixtures/fake-event-store'; @@ -306,3 +307,108 @@ test('Should be a valid password with special chars', async () => { expect(valid).toBe(true); }); + +test('Should send password reset email if user exists', async () => { + const userStore = new UserStoreMock(); + const eventStore = new EventStoreMock(); + const accessService = new AccessServiceMock(); + const resetTokenStore = new FakeResetTokenStore(); + const resetTokenService = new ResetTokenService( + { resetTokenStore }, + config, + ); + const emailService = new EmailService(config.email, config.getLogger); + const sessionStore = new FakeSessionStore(); + const sessionService = new SessionService({ sessionStore }, config); + const settingService = new SettingService( + { + settingStore: new FakeSettingStore(), + eventStore: new FakeEventStore(), + }, + config, + ); + + const service = new UserService({ userStore, eventStore }, config, { + accessService, + resetTokenService, + emailService, + sessionService, + settingService, + }); + + const unknownUser = service.createResetPasswordEmail('unknown@example.com'); + expect(unknownUser).rejects.toThrowError('Could not find user'); + + await userStore.insert({ + id: 123, + name: 'User', + username: 'Username', + email: 'known@example.com', + permissions: [], + imageUrl: '', + seenAt: new Date(), + loginAttempts: 0, + createdAt: new Date(), + isAPI: false, + generateImageUrl: () => '', + }); + + const knownUser = service.createResetPasswordEmail('known@example.com'); + expect(knownUser).resolves.toBeInstanceOf(URL); +}); + +test('Should throttle password reset email', async () => { + const userStore = new UserStoreMock(); + const eventStore = new EventStoreMock(); + const accessService = new AccessServiceMock(); + const resetTokenStore = new FakeResetTokenStore(); + const resetTokenService = new ResetTokenService( + { resetTokenStore }, + config, + ); + const emailService = new EmailService(config.email, config.getLogger); + const sessionStore = new FakeSessionStore(); + const sessionService = new SessionService({ sessionStore }, config); + const settingService = new SettingService( + { + settingStore: new FakeSettingStore(), + eventStore: new FakeEventStore(), + }, + config, + ); + + const service = new UserService({ userStore, eventStore }, config, { + accessService, + resetTokenService, + emailService, + sessionService, + settingService, + }); + + await userStore.insert({ + id: 123, + name: 'User', + username: 'Username', + email: 'known@example.com', + permissions: [], + imageUrl: '', + seenAt: new Date(), + loginAttempts: 0, + createdAt: new Date(), + isAPI: false, + generateImageUrl: () => '', + }); + + jest.useFakeTimers(); + + const attempt1 = service.createResetPasswordEmail('known@example.com'); + await expect(attempt1).resolves.toBeInstanceOf(URL); + + const attempt2 = service.createResetPasswordEmail('known@example.com'); + await expect(attempt2).resolves.toBe(undefined); + + jest.runAllTimers(); + + const attempt3 = service.createResetPasswordEmail('known@example.com'); + await expect(attempt3).resolves.toBeInstanceOf(URL); +}); diff --git a/src/lib/services/user-service.ts b/src/lib/services/user-service.ts index 9d76707c7d..4be3795533 100644 --- a/src/lib/services/user-service.ts +++ b/src/lib/services/user-service.ts @@ -76,6 +76,8 @@ class UserService { private settingService: SettingService; + private passwordResetTimeouts: { [key: string]: NodeJS.Timeout } = {}; + constructor( stores: Pick, { @@ -400,11 +402,19 @@ class UserService { if (!receiver) { throw new NotFoundError(`Could not find ${receiverEmail}`); } + if (this.passwordResetTimeouts[receiver.id]) { + return; + } + const resetLink = await this.resetTokenService.createResetPasswordUrl( receiver.id, user.username || user.email, ); + this.passwordResetTimeouts[receiver.id] = setTimeout(() => { + delete this.passwordResetTimeouts[receiver.id]; + }, 1000 * 60); // 1 minute + await this.emailService.sendResetMail( receiver.name, receiver.email,