Commit ad358fff authored by catch's avatar catch
Browse files

Issue #2828724 by Spokje, alexpott, ravi.shankar, Lal_, malcomio, ElusiveMind,...

Issue #2828724 by Spokje, alexpott, ravi.shankar, Lal_, malcomio, ElusiveMind, smaz, yogeshmpawar, ridhimaabrol24, semiaddict, piggito, f.mazeikis, tvhung, tatarbj, ranjith_kumar_k_u, vijaycs85, baikho, Jelle_S, kleinmp, bbrala, Mike_info, David_Rothstein, pwolanin, cburschka: Username enumeration via one time login route

(cherry picked from commit ae061b39)
parent a54f89a2
Loading
Loading
Loading
Loading
+54 −19
Original line number Diff line number Diff line
@@ -14,6 +14,7 @@
use Drupal\user\UserStorageInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;

@@ -149,6 +150,12 @@ public function resetPass(Request $request, $uid, $timestamp, $hash) {
      }
    }

    /** @var \Drupal\user\UserInterface $reset_link_user */
    $reset_link_user = $this->userStorage->load($uid);
    if ($redirect = $this->determineErrorRedirect($reset_link_user, $timestamp, $hash)) {
      return $redirect;
    }

    $session = $request->getSession();
    $session->set('pass_reset_hash', $hash);
    $session->set('pass_reset_timeout', $timestamp);
@@ -222,26 +229,12 @@ public function getResetPassForm(Request $request, $uid) {
   *   If $uid is for a blocked user or invalid user ID.
   */
  public function resetPassLogin($uid, $timestamp, $hash, Request $request) {
    // The current user is not logged in, so check the parameters.
    $current = REQUEST_TIME;
    /** @var \Drupal\user\UserInterface $user */
    $user = $this->userStorage->load($uid);

    // Verify that the user exists and is active.
    if ($user === NULL || !$user->isActive()) {
      // Blocked or invalid user ID, so deny access. The parameters will be in
      // the watchdog's URL for the administrator to check.
      throw new AccessDeniedHttpException();
    if ($redirect = $this->determineErrorRedirect($user, $timestamp, $hash)) {
      return $redirect;
    }

    // Time out, in seconds, until login URL expires.
    $timeout = $this->config('user.settings')->get('password_reset_timeout');
    // No time out for first time login.
    if ($user->getLastLoginTime() && $current - $timestamp > $timeout) {
      $this->messenger()->addError($this->t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'));
      return $this->redirect('user.pass');
    }
    elseif ($user->isAuthenticated() && ($timestamp >= $user->getLastLoginTime()) && ($timestamp <= $current) && hash_equals($hash, user_pass_rehash($user, $timestamp))) {
    user_login_finalize($user);
    $this->logger->notice('User %name used one-time login link at time %timestamp.', ['%name' => $user->getDisplayName(), '%timestamp' => $timestamp]);
    $this->messenger()->addStatus($this->t('You have just used your one-time login link. It is no longer necessary to use this link to log in. Please set your password.'));
@@ -261,6 +254,48 @@ public function resetPassLogin($uid, $timestamp, $hash, Request $request) {
    );
  }

  /**
   * Validates user, hash, and timestamp.
   *
   * This method allows the 'user.reset' and 'user.reset.login' routes to use
   * the same logic to check the user, timestamp and hash and redirect to the
   * same location with the same messages.
   *
   * @param \Drupal\user\UserInterface|null $user
   *   User requesting reset. NULL if the user does not exist.
   * @param int $timestamp
   *   The current timestamp.
   * @param string $hash
   *   Login link hash.
   *
   * @return \Symfony\Component\HttpFoundation\RedirectResponse|null
   *   Returns a redirect if the information is incorrect. It redirects to
   *   'user.pass' route with a message for the user.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
   *   If $uid is for a blocked user or invalid user ID.
   */
  protected function determineErrorRedirect(?UserInterface $user, int $timestamp, string $hash): ?RedirectResponse {
    $current = REQUEST_TIME;
    // Verify that the user exists and is active.
    if ($user === NULL || !$user->isActive()) {
      // Blocked or invalid user ID, so deny access. The parameters will be in
      // the watchdog's URL for the administrator to check.
      throw new AccessDeniedHttpException();
    }

    // Time out, in seconds, until login URL expires.
    $timeout = $this->config('user.settings')->get('password_reset_timeout');
    // No time out for first time login.
    if ($user->getLastLoginTime() && $current - $timestamp > $timeout) {
      $this->messenger()->addError($this->t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'));
      return $this->redirect('user.pass');
    }
    elseif ($user->isAuthenticated() && ($timestamp >= $user->getLastLoginTime()) && ($timestamp <= $current) && hash_equals($hash, user_pass_rehash($user, $timestamp))) {
      // The information provided is valid.
      return NULL;
    }

    $this->messenger()->addError($this->t('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.'));
    return $this->redirect('user.pass');
  }
+4 −0
Original line number Diff line number Diff line
@@ -106,6 +106,8 @@ public function testLogin() {

    // Enable serialization so we have access to additional formats.
    $this->container->get('module_installer')->install(['serialization']);
    $this->rebuildAll();

    $this->doTestLogin('json');
    $this->doTestLogin('xml');
  }
@@ -243,6 +245,7 @@ public function testPasswordReset() {

    // Enable serialization so we have access to additional formats.
    $this->container->get('module_installer')->install(['serialization']);
    $this->rebuildAll();

    $this->doTestPasswordReset('json', $account);
    $this->doTestPasswordReset('xml', $account);
@@ -572,6 +575,7 @@ protected function loginFromResetEmail() {
    $resetURL = $urls[0];
    $this->drupalGet($resetURL);
    $this->submitForm([], 'Log in');
    $this->assertSession()->pageTextContains('You have just used your one-time login link. It is no longer necessary to use this link to log in. Please set your password.');
  }

}
+13 −4
Original line number Diff line number Diff line
@@ -151,7 +151,8 @@ public function testUserPasswordReset() {
    // Log out, and try to log in again using the same one-time link.
    $this->drupalLogout();
    $this->drupalGet($resetURL);
    $this->submitForm([], 'Log in');
    $this->assertSession()->pageTextContains('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.');
    $this->drupalGet($resetURL . '/login');
    $this->assertSession()->pageTextContains('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.');

    // Request a new password again, this time using the email address.
@@ -177,7 +178,8 @@ public function testUserPasswordReset() {
    $bogus_timestamp = REQUEST_TIME - $timeout - 60;
    $_uid = $this->account->id();
    $this->drupalGet("user/reset/$_uid/$bogus_timestamp/" . user_pass_rehash($this->account, $bogus_timestamp));
    $this->submitForm([], 'Log in');
    $this->assertSession()->pageTextContains('You have tried to use a one-time login link that has expired. Please request a new one using the form below.');
    $this->drupalGet("user/reset/$_uid/$bogus_timestamp/" . user_pass_rehash($this->account, $bogus_timestamp) . '/login');
    $this->assertSession()->pageTextContains('You have tried to use a one-time login link that has expired. Please request a new one using the form below.');

    // Create a user, block the account, and verify that a login link is denied.
@@ -186,6 +188,8 @@ public function testUserPasswordReset() {
    $blocked_account->save();
    $this->drupalGet("user/reset/" . $blocked_account->id() . "/$timestamp/" . user_pass_rehash($blocked_account, $timestamp));
    $this->assertSession()->statusCodeEquals(403);
    $this->drupalGet("user/reset/" . $blocked_account->id() . "/$timestamp/" . user_pass_rehash($blocked_account, $timestamp) . '/login');
    $this->assertSession()->statusCodeEquals(403);

    // Verify a blocked user can not request a new password.
    $this->drupalGet('user/password');
@@ -203,7 +207,8 @@ public function testUserPasswordReset() {
    $this->account->setEmail("1" . $this->account->getEmail());
    $this->account->save();
    $this->drupalGet($old_email_reset_link);
    $this->submitForm([], 'Log in');
    $this->assertSession()->pageTextContains('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.');
    $this->drupalGet($old_email_reset_link . '/login');
    $this->assertSession()->pageTextContains('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.');

    // Verify a password reset link will automatically log a user when /login is
@@ -563,7 +568,11 @@ public function testResetImpersonation() {
    $reset_url = user_pass_reset_url($user1);
    $attack_reset_url = str_replace("user/reset/{$user1->id()}", "user/reset/{$user2->id()}", $reset_url);
    $this->drupalGet($attack_reset_url);
    $this->submitForm([], 'Log in');
    // Verify that the invalid password reset page does not show the user name.
    $this->assertSession()->pageTextNotContains($user2->getAccountName());
    $this->assertSession()->addressEquals('user/password');
    $this->assertSession()->pageTextContains('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.');
    $this->drupalGet($attack_reset_url . '/login');
    // Verify that the invalid password reset page does not show the user name.
    $this->assertSession()->pageTextNotContains($user2->getAccountName());
    $this->assertSession()->addressEquals('user/password');