Problem/Motivation

The issue: #1521996: Password reset form reveals whether an email or username is in use fixed the enumeration of email addresses and usernames via the password reset form. This issue is a follow-up to apply the same fix to the password reset json endpoint in \Drupal\user\Controller\UserAuthenticationController::resetPassword.

Steps to reproduce

Run

curl -X POST 'https://mydrupalsite.dev/user/password?_format=json' \
-d '{"mail": "myemail@example.com"}'

if myemail@example.com does not correspond to an account then the following payload will be returned:

{"message":"Unrecognized username or email address."}

if myemail@example.com does correspond to an account but the account is blocked or not activated, then the following payload is returned:

{"message":"The user has not been activated or is blocked."}

if myemail@example.com does correspond to an active not-blocked account then an empty code 200 response is returned.

Proposed resolution

Make the "Unrecognized username or email address." scenario return an empty code 200 response, as in return new Response();, but log the error in watchdog.
Discuss whether to prevent enumeration of non-active/blocked accounts in another issue.

Remaining tasks

Review the attached patch.

User interface changes

Same empty response regardless of whether the user exists with an active account or does not exist at all.

This decreases usability for users since they might be resetting their password with the wrong email address and then wonder why they never get an email. Right now you immediately know that you typed in a wrong email address. However, decreased usability vs. better privacy is probably a tradeoff we accept.

API changes

Headless clients will need to be adjusted for the new response.

Data model changes

None.

Release notes snippet

Issue fork drupal-3261663

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

ShaunDychko created an issue. See original summary.

shaundychko’s picture

shaundychko’s picture

shaundychko’s picture

Issue summary: View changes
shaundychko’s picture

Issue tags: +Novice
shaundychko’s picture

Issue summary: View changes
Status: Active » Needs review
StatusFileSize
new2.26 KB

The attached patch fixes enumeration of active accounts. Enumeration of non-active/blocked accounts is a lower priority, and perhaps whether to fix that could be discussed in another issue?

shaundychko’s picture

Component: user interface text » user.module
schillerm’s picture

Status: Needs review » Reviewed & tested by the community

Hi there, I have reviewed and tested this patch.

Set up local testing site, Drupal version 9.4.0-dev, PHP 8.0.15.

Created test user Joe Blogs, ran the curl commands got the expected responses back (successfully recreated error).

Applied patch #6, ran curl commands again.
Running curl command for non existent user returns empty 200 and logs message in watchdog.
Running curl command for existing Joe Blogs user also returns 200 and logs to watchdog.
Running curl command for locked Joe Blogs account returns {"message":"The user has not been activated or is blocked."} as expected.

Looked over the patch code changes, seems ok to me but less confident in reviewing this.

Status: Reviewed & tested by the community » Needs work
cmlara’s picture

Status: Needs work » Reviewed & tested by the community

The fail in #9 was a known random #3269085: [random test failure] Random test fail in EntityAutocompleteTest.
ShaunDychko added re-test on March 17th that passed.

Restoring #8's RTBC

dww’s picture

Status: Reviewed & tested by the community » Needs review
Issue tags: +Bug Smash Initiative

Thanks for working on this important oversight from previous efforts to fix this bug! Hate to do this, but I don't think this is ready to commit. Point 1 is a dubious nit. Points 2 and 3 are in support of RTBC. But point 4 is why I think this at least needs more review, and possibly more work.

  1. +++ b/core/modules/user/src/Controller/UserAuthenticationController.php
    @@ -244,10 +244,13 @@ public function resetPassword(Request $request) {
    +      $identifier = $credentials['name'];
           $users = $this->userStorage->loadByProperties(['name' => trim($credentials['name'])]);
    

    If we're going to add this as a local var, why not use it on the very next line?

  2. +++ b/core/modules/user/src/Controller/UserAuthenticationController.php
    @@ -270,7 +273,10 @@ public function resetPassword(Request $request) {
    +    $this->logger->error('Unable to send password reset email for unrecognized username or email address %identifier.', [
    +      '%identifier' => $identifier,
    +    ]);
    

    At first, I wasn't sure this was in scope, but it's mentioned in the summary. We added similar logging at #1521996. This seems good.

  3. +++ b/core/modules/user/src/Controller/UserAuthenticationController.php
    @@ -270,7 +273,10 @@ public function resetPassword(Request $request) {
    +    return new Response();
    

    I guess this is the equivalent of:

        // Make sure the status text is displayed even if no email was sent. This
        // message is deliberately the same as the success message for privacy.
        $this->messenger()
          ->addStatus($this->t('If %identifier is a valid account, an email will be sent with instructions to reset your password.', [
            '%identifier' => $form_state->getValue('name'),
          ]));
    

    which is what we added.

  4. +++ b/core/modules/user/tests/src/Functional/UserLoginHttpTest.php
    @@ -527,10 +527,10 @@ protected function doTestPasswordReset($format, $account) {
         $account
           ->block()
    

    The next few lines of this test are:

        $response = $this->passwordRequest(['name' => $account->getAccountName()], $format);
        $this->assertHttpResponseWithMessage($response, 400, 'The user has not been activated or is blocked\
    .', $format);
    
        $response = $this->passwordRequest(['mail' => $account->getEmail()], $format);
        $this->assertHttpResponseWithMessage($response, 400, 'The user has not been activated or is blocked\
    .', $format);
    

    Doesn't that reveal the email address for blocked users exists? Is that not also a problem? Shouldn't we fix this as part of the same issue? That was part of what was originally fixed at #1521996: Password reset form reveals whether an email or username is in use for the web UI case.

Thanks again!
-Derek

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

Issue summary: View changes

dieterholvoet’s picture

I processed dww's feedback and pushed everything to a MR. I also replaced the The user has not been activated or is blocked error message with a 200 response and an error log. I updated the tests to check for 200 responses, but shouldn't we also test whether the errors are logged? Not sure how to do that though, I came across #3274834: Allow functional tests to fail or expect logged errors but that doesn't seem like an option yet.

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.

smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Needs Review Queue Initiative

This issue is being reviewed by the kind folks in Slack, #needs-review-queue-initiative. We are working to keep the size of Needs Review queue [2700+ issues] to around 400 (1 month or less), following Review a patch or merge request as a guide.

Ran the tests locally without to the fix to make sure they failed. The response was really long but did fail

Caused by
 ErrorException: Method "IteratorAggregate::getIterator()" might add "\Traversable" as a native return type declaration in the future. Do the same in implementation "Drupal\Core\Entity\ContentEntityBase" now to avoid errors or add an explicit @return annotation to suppress this message.
shouldn't we also test whether the errors are logged?

If that's something you want to test you could probably query the database.

Will move to NW for that but if it's actually not needed please send back.

Everything else looks good.

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

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

s.messaris made their first commit to this issue’s fork.

s.messaris’s picture

Status: Needs work » Needs review

Rerolled for 11.x and added the missing test scenarios

smustgrave’s picture

Status: Needs review » Needs work

Seems the reroll caused test fialures.

s.messaris’s picture

Wasn't the reroll, was the new test I added, I fixed it. Got an unrelated fail though, seems random, trying again.

s.messaris’s picture

Status: Needs work » Needs review

Test bot seems to like this after all, back to review :)

smustgrave’s picture

Status: Needs review » Reviewed & tested by the community

Change looks good but I imagine there may be some pushback on

$logged = Database::getConnection()->select('watchdog')

But not sure if there's a trait that can be used but will see

Good work everyone!

s.messaris’s picture

@smustgrave I found it used like that 10 times across 4 files, not including the new addition, that's why I did it this way.

core/modules/comment/tests/src/Kernel/CommentIntegrationTest.php
core/modules/dblog/tests/src/Functional/DbLogTest.php
core/modules/dblog/tests/src/Kernel/DbLogTest.php
core/modules/system/tests/src/Functional/Form/StorageTest.php

smustgrave’s picture

Have no issue with it. Just have seen that get kicked back before.

larowlan’s picture

Hiding patches and updating issue credits

  • larowlan committed 720d213c on 10.1.x
    Issue #3261663 by s.messaris, DieterHolvoet, ShaunDychko, smustgrave,...

  • larowlan committed 98618df7 on 11.x
    Issue #3261663 by s.messaris, DieterHolvoet, ShaunDychko, smustgrave,...
larowlan’s picture

Status: Reviewed & tested by the community » Fixed

Committed 98618df and pushed to 11.x. Thanks!

Backported to 10.1.x as this is an important security fix.

Added #3377275: Move \Drupal\Tests\system\Functional\Module\ModuleTestBase::assertLogMessage to a trait as a followup

larowlan’s picture

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

Status: Fixed » Closed (fixed)

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