mirror of
https://github.com/flarum/framework.git
synced 2025-01-19 18:12:59 +08:00
fix: password reset leaks user existence (#3616)
This commit is contained in:
parent
fc4d5e3d43
commit
84c31165e5
|
@ -350,7 +350,7 @@ core:
|
|||
forgot_password:
|
||||
dismiss_button: => core.ref.okay
|
||||
email_placeholder: => core.ref.email
|
||||
email_sent_message: We've sent you an email containing a link to reset your password. Check your spam folder if you don't receive it within the next minute or two.
|
||||
email_sent_message: If the email you entered is registered with this site, we'll send you an email containing a link to reset your password. Check your spam folder if you don't receive it within the next minute or two.
|
||||
not_found_message: There is no user registered with that email address.
|
||||
submit_button: Recover Password
|
||||
text: Enter your email address and we will send you a link to reset your password.
|
||||
|
|
|
@ -9,10 +9,11 @@
|
|||
|
||||
namespace Flarum\Api\Controller;
|
||||
|
||||
use Flarum\User\Command\RequestPasswordReset;
|
||||
use Flarum\User\UserRepository;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Flarum\User\Job\RequestPasswordResetJob;
|
||||
use Illuminate\Contracts\Queue\Queue;
|
||||
use Illuminate\Contracts\Validation\Factory;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laminas\Diactoros\Response\EmptyResponse;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
@ -21,23 +22,19 @@ use Psr\Http\Server\RequestHandlerInterface;
|
|||
class ForgotPasswordController implements RequestHandlerInterface
|
||||
{
|
||||
/**
|
||||
* @var \Flarum\User\UserRepository
|
||||
* @var Queue
|
||||
*/
|
||||
protected $users;
|
||||
protected $queue;
|
||||
|
||||
/**
|
||||
* @var Dispatcher
|
||||
* @var Factory
|
||||
*/
|
||||
protected $bus;
|
||||
protected $validatorFactory;
|
||||
|
||||
/**
|
||||
* @param \Flarum\User\UserRepository $users
|
||||
* @param Dispatcher $bus
|
||||
*/
|
||||
public function __construct(UserRepository $users, Dispatcher $bus)
|
||||
public function __construct(Queue $queue, Factory $validatorFactory)
|
||||
{
|
||||
$this->users = $users;
|
||||
$this->bus = $bus;
|
||||
$this->queue = $queue;
|
||||
$this->validatorFactory = $validatorFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -47,10 +44,19 @@ class ForgotPasswordController implements RequestHandlerInterface
|
|||
{
|
||||
$email = Arr::get($request->getParsedBody(), 'email');
|
||||
|
||||
$this->bus->dispatch(
|
||||
new RequestPasswordReset($email)
|
||||
$validation = $this->validatorFactory->make(
|
||||
compact('email'),
|
||||
['email' => 'required|email']
|
||||
);
|
||||
|
||||
if ($validation->fails()) {
|
||||
throw new ValidationException($validation);
|
||||
}
|
||||
|
||||
// Prevents leaking user existence by not throwing an error.
|
||||
// Prevents leaking user existence by duration by using a queued job.
|
||||
$this->queue->push(new RequestPasswordResetJob($email));
|
||||
|
||||
return new EmptyResponse;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\User\Command;
|
||||
|
||||
class RequestPasswordReset
|
||||
{
|
||||
/**
|
||||
* The email of the user to request a password reset for.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $email;
|
||||
|
||||
/**
|
||||
* @param string $email The email of the user to request a password reset for.
|
||||
*/
|
||||
public function __construct($email)
|
||||
{
|
||||
$this->email = $email;
|
||||
}
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\User\Command;
|
||||
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\Mail\Job\SendRawEmailJob;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\PasswordToken;
|
||||
use Flarum\User\UserRepository;
|
||||
use Illuminate\Contracts\Queue\Queue;
|
||||
use Illuminate\Contracts\Validation\Factory;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class RequestPasswordResetHandler
|
||||
{
|
||||
/**
|
||||
* @var UserRepository
|
||||
*/
|
||||
protected $users;
|
||||
|
||||
/**
|
||||
* @var SettingsRepositoryInterface
|
||||
*/
|
||||
protected $settings;
|
||||
|
||||
/**
|
||||
* @var Queue
|
||||
*/
|
||||
protected $queue;
|
||||
|
||||
/**
|
||||
* @var UrlGenerator
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
protected $translator;
|
||||
|
||||
/**
|
||||
* @var Factory
|
||||
*/
|
||||
protected $validatorFactory;
|
||||
|
||||
/**
|
||||
* @param UserRepository $users
|
||||
* @param SettingsRepositoryInterface $settings
|
||||
* @param Queue $queue
|
||||
* @param UrlGenerator $url
|
||||
* @param TranslatorInterface $translator
|
||||
* @param Factory $validatorFactory
|
||||
*/
|
||||
public function __construct(
|
||||
UserRepository $users,
|
||||
SettingsRepositoryInterface $settings,
|
||||
Queue $queue,
|
||||
UrlGenerator $url,
|
||||
TranslatorInterface $translator,
|
||||
Factory $validatorFactory
|
||||
) {
|
||||
$this->users = $users;
|
||||
$this->settings = $settings;
|
||||
$this->queue = $queue;
|
||||
$this->url = $url;
|
||||
$this->translator = $translator;
|
||||
$this->validatorFactory = $validatorFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RequestPasswordReset $command
|
||||
* @return \Flarum\User\User
|
||||
* @throws ModelNotFoundException
|
||||
*/
|
||||
public function handle(RequestPasswordReset $command)
|
||||
{
|
||||
$email = $command->email;
|
||||
|
||||
$validation = $this->validatorFactory->make(
|
||||
compact('email'),
|
||||
['email' => 'required|email']
|
||||
);
|
||||
|
||||
if ($validation->fails()) {
|
||||
throw new ValidationException($validation);
|
||||
}
|
||||
|
||||
$user = $this->users->findByEmail($email);
|
||||
|
||||
if (! $user) {
|
||||
throw new ModelNotFoundException;
|
||||
}
|
||||
|
||||
$token = PasswordToken::generate($user->id);
|
||||
$token->save();
|
||||
|
||||
$data = [
|
||||
'username' => $user->display_name,
|
||||
'url' => $this->url->to('forum')->route('resetPassword', ['token' => $token->token]),
|
||||
'forum' => $this->settings->get('forum_title'),
|
||||
];
|
||||
|
||||
$body = $this->translator->trans('core.email.reset_password.body', $data);
|
||||
$subject = $this->translator->trans('core.email.reset_password.subject');
|
||||
|
||||
$this->queue->push(new SendRawEmailJob($user->email, $subject, $body));
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
60
framework/core/src/User/Job/RequestPasswordResetJob.php
Normal file
60
framework/core/src/User/Job/RequestPasswordResetJob.php
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\User\Job;
|
||||
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\Mail\Job\SendRawEmailJob;
|
||||
use Flarum\Queue\AbstractJob;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\PasswordToken;
|
||||
use Flarum\User\UserRepository;
|
||||
use Illuminate\Contracts\Queue\Queue;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class RequestPasswordResetJob extends AbstractJob
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $email;
|
||||
|
||||
public function __construct(string $email)
|
||||
{
|
||||
$this->email = $email;
|
||||
}
|
||||
|
||||
public function handle(
|
||||
SettingsRepositoryInterface $settings,
|
||||
UrlGenerator $url,
|
||||
TranslatorInterface $translator,
|
||||
UserRepository $users,
|
||||
Queue $queue
|
||||
) {
|
||||
$user = $users->findByEmail($this->email);
|
||||
|
||||
if (! $user) {
|
||||
return;
|
||||
}
|
||||
|
||||
$token = PasswordToken::generate($user->id);
|
||||
$token->save();
|
||||
|
||||
$data = [
|
||||
'username' => $user->display_name,
|
||||
'url' => $url->to('forum')->route('resetPassword', ['token' => $token->token]),
|
||||
'forum' => $settings->get('forum_title'),
|
||||
];
|
||||
|
||||
$body = $translator->trans('core.email.reset_password.body', $data);
|
||||
$subject = $translator->trans('core.email.reset_password.subject');
|
||||
|
||||
$queue->push(new SendRawEmailJob($user->email, $subject, $body));
|
||||
}
|
||||
}
|
|
@ -70,4 +70,19 @@ class SendPasswordResetEmailTest extends TestCase
|
|||
|
||||
$this->assertEquals(429, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function request_password_reset_does_not_leak_user_existence()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('POST', '/api/forgot', [
|
||||
'authenticatedAs' => 3,
|
||||
'json' => [
|
||||
'email' => 'missing_user@machine.local'
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(204, $response->getStatusCode());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user