Problem/Motivation

User password reset URLs can be used to enumerate usernames.

For example [site_url}/user/reset/[user_id]/1/1

For Drupal 7 or Drupal 8, if you're logged in and visit a URL like the above it happens via this message:

Another user (%other_user) is already logged into the site on this computer, but you tried to use a one-time link for user %resetting_user.

For Drupal 8 only, if you're logged out and visit a URL like the above, you get a "Reset password" form with the following content (which includes the username):

This is a one-time login for %user_name and will expire on %expiration_date.

Click on this button to log in to the site and change your password.

When you submit the form it tells you then that the link is invalid. But it never should have shown the form in the first place.

I am reporting this issue because a customer(bank) pointed out it was a leakage of customer information in their scenario. It is currently possible (v7.51 + v8.19) to get the username of user in the system by requesting a password reset for that UID with a random timestamp and hash.

I reported this issue to the security team already and they concluded the following :

I believe this issue can be fixed in public without a security advisory because of our policy on username disclosure: https://www.drupal.org/node/1004778

Steps to reproduce

Proposed resolution

Remaining tasks

Has case 1 been fixed?

User interface changes

API changes

Data model changes

Release notes snippet

CommentFileSizeAuthor
#79 2828724-79.patch10.4 KBalexpott
#79 76-79-interdiff.txt698 bytesalexpott
#76 2828724-76.patch10.39 KBalexpott
#76 74-76-interdiff.txt3.83 KBalexpott
#74 2828724-74.patch11.09 KBalexpott
#74 73-74-interdiff.txt3.8 KBalexpott
#73 2828724-73.patch9.34 KBalexpott
#70 interdiff_68-70.txt953 bytesravi.shankar
#70 2828724-70.patch15.13 KBravi.shankar
#68 2828724-68.patch15.09 KBranjith_kumar_k_u
#63 2828724-62.patch15.07 KBelusivemind
#61 2828724-61.patch14.24 KBelusivemind
#55 interdiff_54-55.txt2.39 KBravi.shankar
#55 2828724-55.patch14.04 KBravi.shankar
#54 drupal-username_enumeration_one_time_login_route-2828724-54.patch13.98 KBbaikho
#52 2828724-52.patch10.78 KBspokje
#52 raw_diff_47_52.txt10.67 KBspokje
#51 2828724-50.patch10.77 KBspokje
#51 reroll_diff_47-50.txt9.23 KBspokje
#49 2828724-49-drupal7.patch7.43 KBjelle_s
#47 interdiff_44-47.txt2.77 KBsemiaddict
#47 2828724-47.patch10.92 KBsemiaddict
#45 interdiff_42-44.txt1.35 KBridhimaabrol24
#45 2828724-44.patch10.99 KBridhimaabrol24
#42 interdiff_39_40.txt11.17 KBhenry tran
#42 username_enumeration-2828724-40.patch11.17 KBhenry tran
#39 interdiff.txt978 bytesLal_
#39 username_enumeration-2828724-39.patch10.95 KBLal_
#37 interdiff.txt1.13 KBLal_
#37 username_enumeration-2828724-37.patch10.23 KBLal_
#34 reroll_diff_31-34.txt8.48 KBf.mazeikis
#34 username_enumeration-2828724-34.patch9.81 KBf.mazeikis
#31 interdiff-2828724-27-31.txt1 KByogeshmpawar
#31 username_enumeration-2828724-31.patch9.23 KByogeshmpawar
#27 username_enumeration-2828724-27.patch9.18 KBtatarbj
#25 username_enumeration-2828724-25.patch9.2 KBkleinmp
#6 username_enumeration-2828724-6.patch8.2 KBmalcomio
#9 username_enumeration-2828724-9.patch10.42 KBmalcomio
#14 2828724-14.patch11.94 KBvijaycs85
#15 2828724-15.patch11.74 KBsmaz
#15 interdiff-2828724-14-15.txt714 bytessmaz
#17 interdiff.txt814 bytespiggito
#17 username_enumeration-2828724-17.patch11.27 KBpiggito

Issue fork drupal-2828724

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

Mike_info created an issue. See original summary.

David_Rothstein’s picture

Version: 8.1.9 » 8.2.x-dev
Component: other » user.module
Issue tags: -core +Needs backport to D7

Note that for Drupal 8 this happens regardless of whether you are logged in or logged out, whereas for Drupal 7 it only happens if you're logged in:

  1. For Drupal 7 or Drupal 8, if you're logged in and visit a URL like the above it happens via this message:

    Another user (%other_user) is already logged into the site on this computer, but you tried to use a one-time link for user %resetting_user.

  2. For Drupal 8 only, if you're logged out and visit a URL like the above, you get a "Reset password" form with the following content (which includes the username):

    This is a one-time login for %user_name and will expire on %expiration_date.

    Click on this button to log in to the site and change your password.

    When you submit the form it tells you then that the link is invalid. But it never should have shown the form in the first place.

The fix in both cases should be to prioritize the "link is invalid" message over anything else, so it always shows when the link is invalid.

Side note: The overall issue is exacerbated a bit by the fact that the raw username is displayed here (not the display name that contrib modules which hide the username would use) but that part is covered by #2801645: Use \Drupal\user\UserInterface::getDisplayName() on user pages instead of the plain User::getUsername() and related issues.

dpi’s picture

Title: [D6, D7, D8] Usernames retrievable through password reset » Username enumeration via one time login route
Issue tags: +user enumeration, +password reset
Related issues: +#1521996: Password reset form reveals whether an email or username is in use

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

malcomio’s picture

Issue summary: View changes

There is a patch on #2863468: User password reset form exposes usernames even when hash is invalid that addresses the case of anonymous users, but not authenticated users.

malcomio’s picture

Version: 8.3.x-dev » 8.4.x-dev
Status: Active » Needs review
StatusFileSize
new8.2 KB

Here is a patch, based on the work by @cburschka and @pwolanin in #2863468: User password reset form exposes usernames even when hash is invalid, that should address both scenarios.

I'm seeing some errors when running tests locally though - will try to sort those out.

malcomio’s picture

Issue summary: View changes
malcomio’s picture

Status: Needs review » Needs work

The patch breaks the scenario where a logged in user clicks a valid one-time login link for another user.

malcomio’s picture

Status: Needs work » Needs review
StatusFileSize
new10.42 KB

Another attempt, which seems to work, as far as I can understand the various scenarios.

However, 4 tests are failing in UserPasswordResetTest, and 2 in testResetImpersonation, but I'm not sure why :(

Status: Needs review » Needs work

The last submitted patch, 9: username_enumeration-2828724-9.patch, failed testing.

David_Rothstein’s picture

vijaycs85’s picture

Status: Needs work » Needs review
StatusFileSize
new11.94 KB

Fixed all fails and had to make some code changes to match with issue summary.

smaz’s picture

StatusFileSize
new11.74 KB
new714 bytes

Just testing this, it seems to work in the majority however if you replace the user ID with a non-existing user ID (or anything else) then you get an error.

i.e. changing:
http://local.dev/user/reset/1/1234567890/ABCXYZ....
to
http://local.dev/user/reset/9999999/1234567890/ABCXYZ....
or
http://local.dev/user/reset/abc/1234567890/ABCXYZ....

Causes:

Call to a member function getLastLoginTime() on null in Drupal\user\Controller\UserController->getResetPassForm() (line 185 of /var/www/drupalvm/web/core/modules/user/src/Controller/UserController.php)

The patch has removed a check for $user not being NULL & the user being active in getResetPassForm().

I've added this back in, and the user gets an Access Denied message now, as per before this patch.

Does this need a test?

Status: Needs review » Needs work

The last submitted patch, 15: 2828724-15.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

piggito’s picture

Status: Needs work » Needs review
StatusFileSize
new11.27 KB
new814 bytes

The test was already there but it was modified along with the check removed so I'm adding a patch with the roll back for test.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

smaz’s picture

Status: Needs review » Reviewed & tested by the community

This is now working fine, thanks!

smaz’s picture

Status: Reviewed & tested by the community » Needs work

I take that back, sorry!

With this patch, appending /login to any reset URL will log you in automatically for the specified ID, so for example:

Changing
http://local.dev/user/reset/2/1234567890/ABCXYZ
to
http://local.dev/user/reset/1/1234567890/ABCXYZ/login

Will successfully log you in as that user.

I'll try and look into this today.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

gg4’s picture

Issue tags: +Security improvements

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

kleinmp’s picture

StatusFileSize
new9.2 KB

Here's the patch roled against 8.6.x. I replaced all of the drupal_set_messages iwth Drupal::messenger...

sk33lz’s picture

The patch in #25 fails against 8.8.x and needs a re-roll.

tatarbj’s picture

Status: Needs work » Needs review
StatusFileSize
new9.18 KB

patch is re-rolled on 8.8.x-dev

tatarbj’s picture

Issue tags: +Seattle2019

Status: Needs review » Needs work

The last submitted patch, 27: username_enumeration-2828724-27.patch, failed testing. View results

yogeshmpawar’s picture

Assigned: Unassigned » yogeshmpawar
yogeshmpawar’s picture

Assigned: yogeshmpawar » Unassigned
Status: Needs work » Needs review
StatusFileSize
new9.23 KB
new1 KB

Maybe it will solve the problem of test fails & also added an interdiff for patches.

Status: Needs review » Needs work

The last submitted patch, 31: username_enumeration-2828724-31.patch, failed testing. View results

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

f.mazeikis’s picture

StatusFileSize
new9.81 KB
new8.48 KB

On 8.8.1 #31 was failing to apply for me, rerolled to be compatible with 8.9.x and added some changes to tests.

ravi.shankar’s picture

Status: Needs work » Needs review
nicrodgers’s picture

Status: Needs review » Needs work

Fails testing of this scenario described in the description:

Drupal 8, if you're logged in and visit a URL like the above it happens via this message:
Another user (%other_user) is already logged into the site on this computer, but you tried to use a one-time link for user %resetting_user.

The message still appears and displays the username, even with patch 34 applied.

Steps to reproduce:
1. Install core and apply patch 34
2. Login as user 1
3. Create a new user account, get their user id, we'll assume it's uid 2 for this next step
4. Go to /user/reset/2/1/1

Actual behaviour:
* Message shows username of user 2, eg: Another user (test2) is already logged into the site on this computer, but you tried to use a one-time link for user admin. Please log out and try using the link again.
* https://www.dropbox.com/s/be4bgjr8urh1pld/Screenshot%202020-04-07%2014.4...

Expected behaviour:
* Message should not reveal the username of the other user.

Lal_’s picture

Status: Needs work » Needs review
StatusFileSize
new10.23 KB
new1.13 KB

I have edited the message but if it requires any other fixes please let me know.

Status: Needs review » Needs work

The last submitted patch, 37: username_enumeration-2828724-37.patch, failed testing. View results

Lal_’s picture

Status: Needs work » Needs review
StatusFileSize
new10.95 KB
new978 bytes

forgot the test changes...

fabianderijk’s picture

Thanks for the patch. It works for me, I get redirected to the password reset form with the message that the link is expired when I go www.example.com/user/reset/1/1/1. When I use a actual link the user get's logged in.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

henry tran’s picture

StatusFileSize
new11.17 KB
new11.17 KB

I've re rolled to 8.9.1

Status: Needs review » Needs work

The last submitted patch, 42: username_enumeration-2828724-40.patch, failed testing. View results

hash6’s picture

Assigned: Unassigned » hash6
ridhimaabrol24’s picture

Status: Needs work » Needs review
StatusFileSize
new10.99 KB
new1.35 KB

Fixing test cases.

ridhimaabrol24’s picture

Hi @hash6. Didnt notice you assigned this to yourself. I worked on it and uploaded the patch. it seems u just assigned the patch few seconds back. Sorry for that.

semiaddict’s picture

StatusFileSize
new10.92 KB
new2.77 KB

Hi,
I'm also trying to deal with this issue, which I consider to be a vulnerability in my setup.
Thank you for the work already done.

I am attaching a new patch that removes some unnecessary checks in UserController::getResetPassForm. It also moves the timeout check above the more expensive validPassResetHash check.

tanubansal’s picture

Patch #45 is working fine.
This can be moved to RTBC if no further code changes are there

jelle_s’s picture

StatusFileSize
new7.43 KB

I took a stab at a patch for D7. I moved the checks for validating the hash, timestamp, ... to the top of the function and altered the message when an other user is already logged in like they did in the patch for D8/9.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

spokje’s picture

Assigned: hash6 » Unassigned
StatusFileSize
new9.23 KB
new10.77 KB

Reroll of patch #47 against 9.2.x

spokje’s picture

StatusFileSize
new10.67 KB
new10.78 KB

Seems that have inherited some spacing issues, retry

jjmackow’s picture

I tried applying patch #42 to Drupal 8.9.9 and it failed.

That being said, would it be adequate from a security standpoint to simply add a new TWIG template to the theme and override the form.html.twig file with say, form--user--pass--reset.html.twig?

the 'children' variable within that TWIG returns a string of:

This is a one-time login for jsmith and will expire on January 01, 1970.

I was thinking of getting 'children' rewritten with

This is a one-time login for j***h

and displaying the username string with a mask.

baikho’s picture

Added a reroll of #52 with username mask suggestion of #53.

ravi.shankar’s picture

StatusFileSize
new14.04 KB
new2.39 KB

Added a patch will fix CS issue of patch #54.

Status: Needs review » Needs work

The last submitted patch, 55: 2828724-55.patch, failed testing. View results

spokje’s picture

Onwards and upwards in the MR.

spokje’s picture

Status: Needs work » Needs review

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

elusivemind’s picture

StatusFileSize
new14.24 KB

Re-Roll of Patch 55 for Drupal 9.2

Status: Needs review » Needs work

The last submitted patch, 61: 2828724-61.patch, failed testing. View results

elusivemind’s picture

StatusFileSize
new15.07 KB

Re-submitting for testing

zatox’s picture

I don't understand why we are trying to mask the usernames. It's only partialy solving the issue.

If the link is valid then we can show the real username. That's not a problem.
If the link is not valid then we shouldn't show the form at all (that's the problem) but do something else like show a 403 for example or show a page that says "the one time login link you used is invalid".

There is no point in showing the form with a masked username if the link used is not valid.
Am I missing something?

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

dieterholvoet’s picture

Status: Needs work » Needs review
ranjith_kumar_k_u’s picture

StatusFileSize
new15.09 KB

Rerolled #63

Status: Needs review » Needs work

The last submitted patch, 68: 2828724-68.patch, failed testing. View results

ravi.shankar’s picture

StatusFileSize
new15.13 KB
new953 bytes

Fixing failed tests of patch #68.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

alexpott’s picture

I think the changes in #70 are not quite right. The problem is that messages should be the same if you use the form - or use the one time link that bypasses the form and logs you in directly. The changes in #70 make it so that the link that logs you in directly does not provide the same messages as it currently does about expired links and that feels wrong.

alexpott’s picture

Status: Needs work » Needs review
StatusFileSize
new9.34 KB

Patched attached moves the redirect on error to it's own function from \Drupal\user\Controller\UserController::resetPassLogin(). This allows that function and \Drupal\user\Controller\UserController::resetPass() do exactly the same thing regardless if a user goes to /user/reset/1/HASH/TIME or /user/reset/1/HASH/TIME/login.

My patch also fixes a bug in \Drupal\Tests\user\Functional\UserLoginHttpTest caused by not refreshing the container. The test on HEAD assumes that after pressing the log in button on the reset pass form you are logged in but actually because of the stale container that don't actually get correct login links. With this change this log in links no redirect and have an error message whereas before you'd have to press the log in button to see the message.

alexpott’s picture

StatusFileSize
new3.8 KB
new11.09 KB

Here's test evidence that the automatic login route and the go via a form and log button route now work the same.

bbrala’s picture

Status: Needs review » Needs work

I've ran through the code. Also tested manually with an reset link on a deactivated user and a normal user. I get a 403 with an deactivated user. And a redirect regarding an invalid link with another user.

  1. +++ b/core/modules/user/src/Controller/UserController.php
    @@ -149,13 +150,19 @@ public function resetPass(Request $request, $uid, $timestamp, $hash) {
    +    $redirect = $this->determineErrorRedirect($reset_link_user, $timestamp, $hash);
    +    if ($redirect === NULL) {
    +      $session = $request->getSession();
    +      $session->set('pass_reset_hash', $hash);
    +      $session->set('pass_reset_timeout', $timestamp);
    +      $redirect = $this->redirect(
    +        'user.reset.form',
    +        ['uid' => $uid]
    +      );
    +    }
    +    return $redirect;
    

    It is great this is refactored to use a method! Way cleaner to have a single method to determine the error.

    I would invert this if though and either:

    // The supplied information is invalid, redirect to reset password form.
    if ($redirect !== NULL) {
      return $redirect;
    }
    

    or

    // The supplied information is invalid, redirect to reset password form.
    if ($redirect instanceof RedirectResponse) {
      return $redirect;
    }
    

    This would be more readable mentally in my opinion.

  2. +++ b/core/modules/user/src/Controller/UserController.php
    @@ -222,26 +229,11 @@ public function getResetPassForm(Request $request, $uid) {
    +    $redirect = $this->determineErrorRedirect($user, $timestamp, $hash);
    +    if ($redirect === NULL) {
    

    This is the same pattern, i really like to have an easy condition and then return early :)

  3. +++ b/core/modules/user/src/Controller/UserController.php
    @@ -260,6 +252,46 @@ public function resetPassLogin($uid, $timestamp, $hash, Request $request) {
    +  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;
    +    }
    

    My first look at this had me questioning this method name. I mean, only one of the 3 outcomes is a redirect. As the docblock mentions, it validates user, hash and timestamp.

    Not sure how this would be better though. A thought was, perhaps it should only throw exceptions if something is wrong, but that kinda complicates the implementation as a whole.

    Basically, it does what is advertised, it just feels like a weird method with those 3 totally different outcomes.

  4. +++ b/core/modules/user/tests/src/Functional/UserLoginHttpTest.php
    @@ -106,6 +106,8 @@ public function testLogin() {
    +    $this->rebuildAll();
    
    @@ -243,6 +245,7 @@ public function testPasswordReset() {
    +    $this->rebuildAll();
    

    Tested the tests without this, because i was skeptical. But seems they fail otherwise.

  5. +++ b/core/modules/user/tests/src/Functional/UserPasswordResetTest.php
    @@ -151,7 +151,8 @@ public function testUserPasswordReset() {
    +    $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');
    
    @@ -177,7 +178,8 @@ public function testUserPasswordReset() {
    +    $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');
    
    @@ -186,6 +188,8 @@ public function testUserPasswordReset() {
    +    $this->drupalGet("user/reset/" . $blocked_account->id() . "/$timestamp/" . user_pass_rehash($blocked_account, $timestamp) . '/login');
    +    $this->assertSession()->statusCodeEquals(403);
    
    @@ -203,7 +207,8 @@ public function testUserPasswordReset() {
    +    $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');
    
    @@ -563,7 +568,11 @@ public function testResetImpersonation() {
    +    $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');
    

    Love the extra coverage for the login links <3

Great progress, thanks @alexpott, this seems like it is quite close.

alexpott’s picture

Status: Needs work » Needs review
StatusFileSize
new3.83 KB
new10.39 KB

Re #75.1 / 2 sure - fixed in the patch attached
Re #75.3 I added more docs. I think the 3 outcomes - exception / redirect / null is okay - and part of making things consistent.
Re #75.4 Yeah the test atm is not currently testing what it thinks it is. If you install a module in a test you need to ensure the container attached to the test object is updated. This is why in Browser tests I think we should not have the container on the test object. This is an old argument.
Re #75.5 that's to ensure we don't lose test coverage as that's what we were testing prior to this change.

bbrala’s picture

Status: Needs review » Reviewed & tested by the community

Thanks for the changes and the explaination.

I did consider the 403 for a non existant user also. Since that does expose the fact a uid exists, but I concluded that this is fine. Since the most important thing to solve is the exposure of usernames.

This looks great! I'm all for getting this in. I'll probably generate a patch for 9.4.x later also, since I think this is an improtant improvement in privacy that i'd roll out to every site we run.

ANyways, RTBC, thanks!

catch’s picture

+++ b/core/modules/user/src/Controller/UserController.php
@@ -222,11 +229,54 @@ public function getResetPassForm(Request $request, $uid) {
+   *
...
+   *   Returns a redirect if the information is incorrect. It redirects to
+   *   'user.pass' route with a message for the user.

Looks like this should be

@return \Symfony\Component\HttpFoundation\RedirectResponse|NULL ?

alexpott’s picture

StatusFileSize
new698 bytes
new10.4 KB

Indeed... thanks for the review @catch. Patch attached fixes the docs... the return typehint was already correct.

  • catch committed ae061b3 on 10.1.x
    Issue #2828724 by Spokje, alexpott, ravi.shankar, Lal_, malcomio,...

  • catch committed 04cc924 on 10.0.x
    Issue #2828724 by Spokje, alexpott, ravi.shankar, Lal_, malcomio,...
  • catch committed ad358ff on 9.5.x
    Issue #2828724 by Spokje, alexpott, ravi.shankar, Lal_, malcomio,...
catch’s picture

Version: 10.1.x-dev » 9.5.x-dev
Status: Reviewed & tested by the community » Fixed

Had a last look through and couldn't find anything to complain about, the code being moved is more or less identical it's just changing execution order so the redirect happens in more cases.

Double checked there's no actual string change here and there isn't making this eligible for 10.0 and 9.5 too, so committed/cherry-picked to the respective branches.

Did my best with issue credits but this is a long issue with a lot of people on it, so might not be perfect.

bbrala’s picture

That is awesome catch. Thanks! We just got a bit more prinlvacy friendly! :)

poker10’s picture

I have created a D7 backport issue. We probably want to backport this security/privacy improvement also there. See: #3326994: [D7] Username enumeration via one time login route

poker10’s picture

While working on the D7 backport, I have noticed that only one usecase for D9/10 was fixed. See #2 or #36.

1. For Drupal 7 or Drupal 8, if you're logged in and visit a URL like the above it happens via this message:

Another user (%other_user) is already logged into the site on this computer, but you tried to use a one-time link for user %resetting_user.

2. For Drupal 8 only, if you're logged out and visit a URL like the above, you get a "Reset password" form with the following content (which includes the username):

This is a one-time login for %user_name and will expire on %expiration_date.

I see that the usecase 2 is fixed now - when logged out, you are correctly redirected. But the username seems to be still revealed if the user is logged in and use that URL with specific pattern (usecase 1) - tested on 10.1.0-dev. Was there a decision to fix only the part when the user is logged out or it was missed?

Thanks!

quietone’s picture

Issue summary: View changes
Status: Fixed » Needs work

I am setting this back to NW for the question raised in #86, which is asking if Case 1 has been fixed. I skimmed the comments and didn't see an answer.

alexpott’s picture

Issue tags: +Needs follow-up

@poker10 nice catch - I think that that means we should add a follow-up issue to address that case as it will require a string change. Once the follow-up exists I think this issue should be marked as fixed.

alexpott’s picture

quietone’s picture

@alexpott, thanks.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

quietone’s picture

Issue tags: -Needs follow-up

I forgot to remove the needs follow up tag.