Skip to content

Commit 3d0e818

Browse files
authored
Merge pull request #34632 from nextcloud/fix/rate-limit-recovery-emails
Add rate limiting on lost password emails
2 parents 39c3907 + 1cb0c2a commit 3d0e818

File tree

3 files changed

+27
-11
lines changed

3 files changed

+27
-11
lines changed

core/Controller/LostController.php

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,6 @@
3636
namespace OC\Core\Controller;
3737

3838
use Exception;
39-
use OC\Authentication\TwoFactorAuth\Manager;
40-
use OC\Core\Events\BeforePasswordResetEvent;
41-
use OC\Core\Events\PasswordResetEvent;
42-
use OC\Core\Exception\ResetPasswordException;
4339
use OCP\AppFramework\Controller;
4440
use OCP\AppFramework\Http\JSONResponse;
4541
use OCP\AppFramework\Http\TemplateResponse;
@@ -56,8 +52,14 @@
5652
use OCP\IUser;
5753
use OCP\IUserManager;
5854
use OCP\Mail\IMailer;
59-
use OCP\Security\VerificationToken\InvalidTokenException;
6055
use OCP\Security\VerificationToken\IVerificationToken;
56+
use OCP\Security\VerificationToken\InvalidTokenException;
57+
use OC\Authentication\TwoFactorAuth\Manager;
58+
use OC\Core\Events\BeforePasswordResetEvent;
59+
use OC\Core\Events\PasswordResetEvent;
60+
use OC\Core\Exception\ResetPasswordException;
61+
use OC\Security\RateLimiting\Exception\RateLimitExceededException;
62+
use OC\Security\RateLimiting\Limiter;
6163
use Psr\Log\LoggerInterface;
6264
use function array_filter;
6365
use function count;
@@ -84,6 +86,7 @@ class LostController extends Controller {
8486
private IInitialState $initialState;
8587
private IVerificationToken $verificationToken;
8688
private IEventDispatcher $eventDispatcher;
89+
private Limiter $limiter;
8790

8891
public function __construct(
8992
string $appName,
@@ -100,7 +103,8 @@ public function __construct(
100103
Manager $twoFactorManager,
101104
IInitialState $initialState,
102105
IVerificationToken $verificationToken,
103-
IEventDispatcher $eventDispatcher
106+
IEventDispatcher $eventDispatcher,
107+
Limiter $limiter
104108
) {
105109
parent::__construct($appName, $request);
106110
$this->urlGenerator = $urlGenerator;
@@ -116,6 +120,7 @@ public function __construct(
116120
$this->initialState = $initialState;
117121
$this->verificationToken = $verificationToken;
118122
$this->eventDispatcher = $eventDispatcher;
123+
$this->limiter = $limiter;
119124
}
120125

121126
/**
@@ -267,6 +272,12 @@ protected function sendEmail(string $input): void {
267272
throw new ResetPasswordException('Could not send reset e-mail since there is no email for username ' . $input);
268273
}
269274

275+
try {
276+
$this->limiter->registerUserRequest('lostpasswordemail', 5, 1800, $user);
277+
} catch (RateLimitExceededException $e) {
278+
throw new ResetPasswordException('Could not send reset e-mail, 5 of them were already sent in the last 30 minutes', 0, $e);
279+
}
280+
270281
// Generate the token. It is stored encrypted in the database with the
271282
// secret being the users' email address appended with the system secret.
272283
// This makes the token automatically invalidate once the user changes

lib/private/Security/RateLimiting/Limiter.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public function __construct(IBackend $backend) {
4545
/**
4646
* @param string $methodIdentifier
4747
* @param string $userIdentifier
48-
* @param int $period
48+
* @param int $period in seconds
4949
* @param int $limit
5050
* @throws RateLimitExceededException
5151
*/
@@ -66,7 +66,7 @@ private function register(string $methodIdentifier,
6666
*
6767
* @param string $identifier
6868
* @param int $anonLimit
69-
* @param int $anonPeriod
69+
* @param int $anonPeriod in seconds
7070
* @param string $ip
7171
* @throws RateLimitExceededException
7272
*/
@@ -85,7 +85,7 @@ public function registerAnonRequest(string $identifier,
8585
*
8686
* @param string $identifier
8787
* @param int $userLimit
88-
* @param int $userPeriod
88+
* @param int $userPeriod in seconds
8989
* @param IUser $user
9090
* @throws RateLimitExceededException
9191
*/

tests/Core/Controller/LostControllerTest.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use OC\Core\Events\BeforePasswordResetEvent;
2727
use OC\Core\Events\PasswordResetEvent;
2828
use OC\Mail\Message;
29+
use OC\Security\RateLimiting\Limiter;
2930
use OCP\AppFramework\Http\JSONResponse;
3031
use OCP\AppFramework\Http\TemplateResponse;
3132
use OCP\AppFramework\Services\IInitialState;
@@ -43,8 +44,8 @@
4344
use OCP\Mail\IMailer;
4445
use OCP\Security\VerificationToken\InvalidTokenException;
4546
use OCP\Security\VerificationToken\IVerificationToken;
46-
use Psr\Log\LoggerInterface;
4747
use PHPUnit\Framework\MockObject\MockObject;
48+
use Psr\Log\LoggerInterface;
4849
use Test\TestCase;
4950

5051
/**
@@ -82,6 +83,8 @@ class LostControllerTest extends TestCase {
8283
private $verificationToken;
8384
/** @var IEventDispatcher|MockObject */
8485
private $eventDispatcher;
86+
/** @var Limiter|MockObject */
87+
private $limiter;
8588

8689
protected function setUp(): void {
8790
parent::setUp();
@@ -129,6 +132,7 @@ protected function setUp(): void {
129132
$this->initialState = $this->createMock(IInitialState::class);
130133
$this->verificationToken = $this->createMock(IVerificationToken::class);
131134
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
135+
$this->limiter = $this->createMock(Limiter::class);
132136
$this->lostController = new LostController(
133137
'Core',
134138
$this->request,
@@ -144,7 +148,8 @@ protected function setUp(): void {
144148
$this->twofactorManager,
145149
$this->initialState,
146150
$this->verificationToken,
147-
$this->eventDispatcher
151+
$this->eventDispatcher,
152+
$this->limiter
148153
);
149154
}
150155

0 commit comments

Comments
 (0)