Problem/Motivation

The actual password reset mechanism exposes a user enumeration security vulnerability defined as Testing for User Enumeration and Guessable User Account (OWASP-AT-002) by OWASP - Open Web Application Security Project.

When resetting the password, the actual status messages displayed to the user, depending if the user exists and is active, are:

  1. The username (or Email) belongs to a valid and active account: Further instructions have been sent to your email address..
  2. The username (or Email) belongs to a blocked user: %name is blocked or has not been activated yet.
  3. No account exists for that username or Email: %name is not recognized as a username or an email address.

The last 2 cases are helping a potential attacker, that is doing a user enumeration, to restrict the list candidates and identify usernames or user Email addresses that are real active users (as well as blocked users).

Along with the security aspect comes also the privacy side. Think of the following scenario:

Alice wants to check if her fiancé Bob is registered at "adult-dating.example.com", a well known Internet dating site run by Drupal. She visits adult-dating.example.com/user/password and enters his mail address bob@doe-family.example. If she gets the message "Further instructions have been sent to your e-mail address.", she'll know that there is a user registered with Bobs mail address (= Bob himself) or with a username matching his mail address (unlikely that it would be someone else).

Proposed resolution

  1. Remove message from validateForm that display user name and/or email.
  2. Change message in submitForm to display user input so they can check for typos but status that if it is valid an email is sent

Message agreed upon.
Patch #257 - "If %identifier is a valid account, an email will be sent with instructions to reset your password."

There have been suggested wording in the following comments.
#29 - "Due to privacy concerns, the policy of this web site is not to disclose the existence of registered email addresses. Hence, if you entered a valid email address, a password reset link are now being sent to it.
If the email address you entered does not exist, you will not get a reset link, and will neither get any confirmation or warning about that here. If you get an email, it should generally arrive within 30 minutes or so, sometimes instantly."
#39 - Suggested edits to the previous message.
#55 - cilefen thought the previous suggestions were too long and suggested "If the email address or username you provided is associated with a site account, you will receive an email with instructions for resetting your password."
#96 - "If an account exists with these credentials, password reset instructions will be sent to the email associated with the account."
#149 - "If this account is valid, you will receive password reset instructions in your inbox."
#229 - "If a matching active account was found, further instructions will have been sent to your email address."
#236 - "If that email address is in our database, we will send you an email to reset your password." From the OWASP Authentication Cheat Sheet Password recovery.
#247 - "If that email address is in our database, an email will be sent with instructions to reset your password."
#250 - "If someone@org.nz is in our database, an email will be sent with instructions to reset your password."
#255 - "If @identifier is a valid account, an email will be sent with instructions to reset your password."
#257 - "If %identifier is a valid account, an email will be sent with instructions to reset your password."

Remaining tasks

  1. Agree on wording of the password reset message
  2. Review
  3. Commit to 9.2.0.
  4. Debate backports.
  5. Re-roll as appropriate.
  6. Decide on a follow-up so that the password recovery message is configurable from the UI. See #237

User interface changes

Same message regardless the user exists, is active or blocked.

This also 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.

To avoid https://owasp.org/www-community/attacks/Content_Spoofing and to partially mitigate the UX regression that there's no feedback if you're typing something invalid, the form now at least validates that the input is either a valid username or a valid email address. If the input is neither, you get a validation error message about it (since that's not a privacy violation):

Form validation error message for an invalid username

Decreased usability vs. better privacy is probably a tradeoff we accept.

Before

For both invalid username and email

After

Message shown on the password reset form for both real and unknown users.

or
Form validation error message for an invalid username or emai

API changes

2 new injected services are needed in the UserPasswordForm constructor. @see https://www.drupal.org/node/3189310

Data model changes

None.

Release notes snippet

Password reset form no longer indicates whether the provided username or email address is in the database.

CommentFileSizeAuthor
#310 1521996-310.patch21.85 KBankitv18
#298 interdiff-286-298.txt1.41 KBsokru
#298 1521996-298.patch16.39 KBsokru
#292 Before.png5.88 KBquietone
#292 Invalid.png4.02 KBquietone
#291 Screenshot_2020-12-23 Reset your password The Site Name(1).png3.98 KBquietone
#286 1521996-286.patch16.66 KBspokje
#286 interdiff_278_286.txt1.53 KBspokje
#279 1521996.276_278.interdiff.txt1.72 KBdww
#279 1521996-278.patch16.8 KBdww
#276 1521996-276.patch16.63 KBspokje
#276 interdiff_268_276.txt938 bytesspokje
#272 1521996.268_269.test.interdiff.txt473 bytesdww
#272 1521996-268.patch16.87 KBdww
#272 1521996-269.test-only.patch17.24 KBdww
#270 1521996-268.invalid-input.png18.25 KBdww
#270 1521996-268.shared-message.png25.15 KBdww
#270 1521996.261_268.interdiff.txt6.04 KBdww
#270 1521996.261_268-test.interdiff.txt2.42 KBdww
#270 1521996-268.patch16.87 KBdww
#270 1521996-268.test-only.patch17.24 KBdww
#261 1521996.260_261.interdiff.txt3.11 KBdww
#261 1521996-261.patch15.22 KBdww
#260 1521996.259_260.interdiff.txt1.97 KBdww
#260 1521996-260.patch12.61 KBdww
#259 interdiff_255_259.txt850 bytesspokje
#259 1521996-259.patch11.18 KBspokje
#255 1521996.254_255.interdiff.txt2.46 KBdww
#255 1521996-255.patch11.21 KBdww
#254 1521996.252_254.interdiff.txt1.76 KBdww
#254 1521996-254.patch11.09 KBdww
#253 Screenshot_2020-12-19 Welcome to dev1-web dev1-web.png7.4 KBquietone
#252 interdiff_248_252.txt4.16 KBspokje
#252 1521996-252.patch11.56 KBspokje
#248 1521996-248.patch9.76 KBquietone
#248 interdiff-245-248.txt1.8 KBquietone
#245 1521996.242_245.interdiff.txt1.33 KBdww
#245 1521996-245.patch9.77 KBdww
#242 1521996-242.patch9.69 KBquietone
#242 interdiff-239-242.txt1019 bytesquietone
#240 Screenshot_2020-12-16 Log in dev0-web(1).png6.9 KBquietone
#239 1521996-239.patch9.69 KBquietone
#239 interdiff-238-239.txt1.5 KBquietone
#238 1521996-238.patch9.62 KBquietone
#238 diff-235-238.txt428 bytesquietone
#235 1521996-235.patch9.63 KBquietone
#235 interdiff-232-235.txt1.71 KBquietone
#232 interdiff-226-232.patch6 KBmjpa
#232 1521996-232.patch9.61 KBmjpa
#229 1521996-229.patch5.76 KBmjpa
#227 1521996-226.patch4.02 KBquietone
#227 interdiff-224-226.txt504 bytesquietone
#224 1521996-224.patch4.29 KBquietone
#224 interdiff-220-224.txt3.57 KBquietone
#220 1521996-220.patch4.53 KBquietone
#220 interdiff-219-220.txt704 bytesquietone
#219 1521996-219.patch4.55 KBquietone
#219 diff-218-219.txt3.08 KBquietone
#218 interdiff_217_218.txt1.32 KBvivek panicker
#218 1521996-218.patch4.43 KBvivek panicker
#217 interdiff_214_217.txt2.59 KBvivek panicker
#217 1521996-217.patch4.43 KBvivek panicker
#214 Screenshot_2020-09-22 Reset your password Site-Install.png4.34 KBquietone
#214 1521996-214.patch4.43 KBquietone
#214 interdiff-212-214.txt1.67 KBquietone
#214 Screenshot_2020-09-22 Log in Site-Install.png3.57 KBquietone
#212 1521996-212.patch4.5 KBquietone
#212 interdiff-210-212.txt1.7 KBquietone
#210 1521996-210.patch3.62 KBquietone
#210 diff-204-210.txt2.12 KBquietone
#204 password-reset-text-1521996-204-8.8.8.patch4.31 KBjozzy_a
#202 password-reset-text-1521996-201-8.8.8.patch4.97 KBjozzy_a
#200 interdiff_198-200.txt897 bytesravi.shankar
#200 1521996-200.patch4.65 KBravi.shankar
#198 password-reset-text-1521996-198.patch4.64 KBnginex
#186 password-reset-text-1521996-186.patch4.49 KBdarrenwh
#185 password-reset-text-translatable.patch7.1 KBmatteodem
#183 password-reset-text-1521996-182.patch5.46 KBashu1629
#181 password-reset-text-1521996-181.patch5.5 KBashu1629
#180 password-reset-text-1521996-180.patch7.06 KBashu1629
#174 password-reset-text-1521996-174.patch7.09 KBalanhdev
#173 password-reset-text-1521996-173.patch6.21 KBalanhdev
#172 password-reset-text-1521996-171.patch5.36 KBalanhdev
#171 password-reset-text-1521996-173.patch8.25 KBronald.garcia
#168 password-reset-text-1521996-168.patch7.05 KBmangy.fox
#164 1521996.164.patch9.09 KBmangy.fox
#161 8.7.x-unrecognized_email.png56.89 KBelendev
#161 8.7.x-recognized_email.png42.27 KBelendev
#161 8.7.x-patched-156-fr.png34.25 KBelendev
#161 8.7.x-patched-156-en.png27.05 KBelendev
#159 1521996.159.patch9.22 KBmangy.fox
#156 1521996-156.patch9.92 KBizus
#154 interdiff_154-153.txt761 bytesizus
#154 1521996-154.patch9.92 KBizus
#153 1521996-153.patch10.03 KBizus
#75 password_reset_text-1521996-75.patch7.79 KBsophie.sk
#75 password_reset_text-1521996-75.patch3.9 KBsophie.sk
#75 interdiff-1521996-75.txt2.17 KBsophie.sk
#75 interdiff-1521996-75.txt877 bytessophie.sk
#74 1521996-74.patch7.39 KBaerozeppelin
#74 interdiff-1521996-66-74.txt3.21 KBaerozeppelin
#73 password_reset_text-1521996-72.patch3.9 KBlomasr
#66 password_reset_text-1521996-66.patch6.95 KBsophie.sk
#66 password_reset_text-1521996-66.patch3.9 KBsophie.sk
#64 password_reset_text-1521996-64.patch3.07 KBsophie.sk
#61 password_reset_text-1521996-59.patch6.95 KBsophie.sk
#59 password_reset_text-1521996-59.patch6.95 KBsophie.sk
#48 user-reset-password-privacy-1521996-48.patch6.96 KBluismagr
#44 user-reset-password-privacy-1521996-44.patch6.46 KBankitgarg
#42 user-reset-password-privacy-1521996-42.patch7.25 KBjain_deepak
#40 user-reset-password-privacy-1521996-40.patch7.13 KBaunion
#35 interdiff_33_35.txt1.46 KBcarsten müller
#35 user-reset-password-privacy-1521996-35.patch7.12 KBcarsten müller
#33 user-reset-password-privacy-1521996-33.patch6.65 KBcarsten müller
#26 user-reset-password-privacy-1521996-26.patch3.95 KBcarsten müller
#22 user-reset-password-privacy-1521996-22.patch3.93 KBcarsten müller
#10 user_reset_password_privacy-1521996-10.patch4.7 KBcarsten müller
#7 user_reset_password_privacy-1521996-7.patch4.64 KBcarsten müller
#3 user_reset_password_privacy-1521996-3.patch3.11 KBcarsten müller
#80 password_reset_text-1521996-80.patch9.02 KBsophie.sk
#80 interdiff-1521996-75-80.txt2.07 KBsophie.sk
#80 password_reset_text-1521996-80-d7.patch3.9 KBsophie.sk
#96 before-1521996-96.png185.99 KBpfructuoso
#96 after-1521996-96.png167.22 KBpfructuoso
#96 password_reset_text-1521996-96.patch8.93 KBpfructuoso
#105 password_reset_text-1521996-101-103.patch8.74 KBcjgratacos
#105 interdiff-1521996-96-101-103.txt5.38 KBcjgratacos
#108 password_reset_form-1521996-108-d7.patch4.78 KBgordon
#111 password_reset_text-1521996-111.patch8.06 KBsophie.sk
#117 1521996-117.patch8.29 KBrobpowell
#120 1521996-120.patch8.28 KBrobpowell
#120 interdiff-1521996-117-120.txt5.63 KBrobpowell
#123 Screen Shot 2018-02-13 at 10.07.37 PM.png151.27 KBkristen pol
#123 Screen Shot 2018-02-13 at 10.07.24 PM.png155.38 KBkristen pol
#124 Screen Shot 2018-02-13 at 10.59.27 PM.png193.6 KBkristen pol
#127 1521996-127-D8.patch8.28 KBmohit1604
#136 1521996-136-D8.patch8.35 KBwebchuck
#137 1521996-137.patch8.56 KBkim.pepper
#137 1521996-137-interdiff.txt1.41 KBkim.pepper
#139 1521996.139.patch9.21 KBkim.pepper
#139 1521996-139-interdiff.txt665 byteskim.pepper
#143 1521996-143.patch10.04 KBkim.pepper
#143 1521996-143-interdiff.txt1.57 KBkim.pepper
#146 interdiff_146-143.txt1.75 KBzuhair_ak
#146 1521996-146.patch9.99 KBzuhair_ak

Comments

Leeteq’s picture

Priority: Normal » Major

Good and important suggestion. :-)

pdrake’s picture

How about if it says "Further instructions have been sent to emailhere@emaildomain.com." in both cases (the email does exist and the email does not exist). If emailhere@emaildomain.com contains a typo, this gives the user the opportunity to realize that they have mis-typed their email address and need to try again. If this was an attempt to obtain membership information, no such information has been exposed.

carsten müller’s picture

+1 for this issue

i made a patch for it, containing the following changes:

  • Validation if no value is entered in the name field
  • Removed validation message if a wrong or none existing name or e-mail adress was entered
  • The message form #3 by pdrake, regardless if the name or e-mail adress is known by the system
  • A watchdog entry (warning) if the name or e-mail adress is not known in the system
carsten müller’s picture

Status: Active » Needs review
c-logemann’s picture

I agree with the privacy issue.
I can imagine a lot of strategies how to handle the password reset in different ways.
The default handling of the core should be very save (like the strategy above) and a more complex or more open strategy can be realized with contrib modules.

carsten müller’s picture

fixed the test and fixed the patch

carsten müller’s picture

Status: Needs work » Needs review
c-logemann’s picture

About patch of #7
Test with existing user OK.
Test with not existing gives the following error:

Notice: Undefined index: account in user_pass_submit() (line 102 of core/modules/user/user.pages.inc).

carsten müller’s picture

fixed the warning from #9
fixed whitespaces

rob c’s picture

Great stuff! but i do have 1 question:

Is it an idea to make this a configurable option? E.g. "B.S. mode" enabled? I had the same problem with a module i was working on to resend a user's name via e-email via a similiar way pass reset works. I ended up creating a config option that is enabled by default, so the admin has the choice. (because sometimes you just want to get the real info.)

Can (obviously) also start a new feature request / bug ones this issue is fixed. (I'll add it to my list of wanna-haves anyway, for when i have some time left. Thinking about making this a site wide option, used by multiple functions, registration / contact form can also make use use of it, if only for pranksters / bots.) And added it to the list of related on #1837054: [META] Refactor account workflow (and config)

yesct’s picture

updating issue summary

yesct’s picture

Issue summary: View changes

minor

webchick’s picture

Category: feature » bug
Priority: Major » Normal

This seems like something we could do at any time and not something that would be subject to feature freeze, so moving to a bug. However, since it's simply a security hardening thing, I'm making it a normal bug.

dave reid’s picture

Issue tags: +Privacy improvements
yesct’s picture

making it configurable could be in the scope of this issue. I dont think we need a new feature request right away. lets see what we can do here.

yesct’s picture

Issue summary: View changes

Updated issue summary to make it easer for new people

penyaskito’s picture

For the user story in the summary, a user also could try to register with the same email, and will get a validation error saying:
"The e-mail address bob@doe-family.example is already registered."

If we try to fix this in Core we should also take the registration into account, which is probably more difficult to handle (what should we do instead?).

penyaskito’s picture

Issue tags: +Security
carsten müller’s picture

Assigned: Unassigned » carsten müller
carsten müller’s picture

Issue summary: View changes
carsten müller’s picture

Updated issue summary - needs updating with issue summary template (how to: http://drupal.org/node/1427826)

carsten müller’s picture

Patch of #10 has to be rerolled.

carsten müller’s picture

added rerolled patch user-reset-password-privacy-1521996-22.patch
now matching the new code of the user module

carsten müller’s picture

Issue summary: View changes
carsten müller’s picture

Issue summary: View changes
carsten müller’s picture

Issue summary: View changes
carsten müller’s picture

fixed a bug in the #22 patch

carsten müller’s picture

(novice) manual testing (how to: http://drupal.org/node/1489010)
Tested locally on my machine, tests have been adjusted too, waiting for testbot to run the tests.

(novice) check coding standards and documentation (how to: http://drupal.org/node/1487976)
Tested locally with php code sniffer (phpcs) within PHPStorm with the Drupal settings. Fine, except the following errors:

UserPasswordForm.php

  • line 10: use Drupal\Core\Field\Plugin\Field\FieldType\EmailItem; - Alias is never used
  • line 16: use Symfony\Component\HttpFoundation\Request; - Alias is never used
  • line 117+120: $this->userStorage->loadByProperties() - Method loadByProperties() not found in class
  • line 124: form_set_value() function is depcricated

UserPasswordResetTest.php

  • line 34: function user_load() is depricated

Writing more tests? Needs a test only patch so the bot can show it fails. (how to: http://drupal.org/node/1468170)
Don't know if we need more tests for this. There is already the UserPasswordResetTest testing a wrong account and a valid account.

Needs an interdiff (how to: http://drupal.org/node/1488712)
The patch has to rerolled because there were a lot of changes in the structure of the user module. Code was sourced out into separate files. Therefore an interdiff is not really possible i think. But it is not much stuff ...

carsten müller’s picture

Assigned: carsten müller » Unassigned
Leeteq’s picture

Issue tags: +Needs backport to D7

I think it makes a lot of sense to have the text customizeable, so that each site can determine the exact wording.

I also think that we might improve on the suggested wording here.

How about something a bit more clarifying, like:

"Due to privacy concerns, the policy of this web site is not to disclose the existence of registered email addresses. Hence, if you entered a valid email address, a password reset link are now being sent to it.
If the email address you entered does not exist, you will not get a reset link, and will neither get any confirmation or warning about that here. If you get an email, it should generally arrive within 30 minutes or so, sometimes instantly."

Would also be great to have this backported to D7.

carsten müller’s picture

good idea. Is there already a settings form, where it should be integrated?
How about the user settings?

And yes, if this is done for D8, i can create a patch for D7

no2e’s picture

carsten müller’s picture

Assigned: Unassigned » carsten müller

i add the administration setting for the text

carsten müller’s picture

I added the setting for this text within the Account settings form - admin/config/people/accounts - AccountSettingsForm.php.
Just reinstall after aplying the patch and it is there. I added the text from Leeteq from #29.
The setting is just below the personalization form.

Maybe somebody has improvements for the texts ...

carsten müller’s picture

missed config settings schema for the new value
added it to the new patch

c-logemann’s picture

Status: Needs work » Needs review

@Carsten Müller: You've been busy today on this issue. Are you sprinting in Amsterdam?

carsten müller’s picture

Assigned: carsten müller » Unassigned
carsten müller’s picture

next steps

  • somebody should also test the patch
  • is the place of the settings within the user settings form admin/config/people/accounts ok?
  • text improvement ideas?
anagp’s picture

Following tests ran on patch #35:
1) Used reset password form with a valid email address already registered
2) Used reset password form with a valid email address not registered
3) Changed the string in the form admin/config/people/accounts

All tests worked as expected (from the users point of view). However it was possible to enter an empty string in 3. It might be a good idea not to allow this.

Regarding the placing of the settings in admin/config/people/accounts I agree that it is a good place.

I would like the texts to be a bit shorter and more to the point. So here are my suggestions taking into consideration that: I agree with the comment #2 that it might be a good idea to add the possibility of printing the entered username/email so that the user can check whether it was entered correctly; and that it might not be necessary for the user to know why the information about previous existing accounts is not printed.

1) Change:

Due to privacy concerns the texts for user password reset or registration should not tell anything about already registered users. So the messages should not contain the username or the email of the registered users.

To:

Due to privacy concerns the texts for user password reset or registration should not contain any information regarding previously registered users.

2) Change:

Due to privacy concerns, the policy of this web site is not to disclose the existence of registered email addresses. Hence, if you entered a valid email address or username, a password reset link is now being sent to it. If the email address you entered does not exist, you will not get a reset link, and will neither get any confirmation or warning about that here. If you get an email, it should generally arrive within 30 minutes or so, sometimes instantly.

To:

If (username/email) is a registered username or email address, you will receive an email within 30 minutes with instructions on how to reset your password.

3) Change:

This is the text displayed when a user uses the password reset form. Due to privacy concerns other users should not be able to check if an account is already registered.

To:

Text displayed after using the password reset form. (Due to privacy concerns it should not include information on whether the account is registered)

aunion’s picture

Due to the following commit the patch was no longer working, so I updated it
https://github.com/drupal/drupal/commit/66762a5361c1b1f2ab47d462681d804a...

yesct’s picture

Issue summary: View changes
Status: Needs review » Needs work
Issue tags: +Needs reroll, +Needs issue summary update
jain_deepak’s picture

Status: Needs work » Needs review
StatusFileSize
new7.25 KB

Rerolled

ankitgarg’s picture

Status: Needs work » Needs review
StatusFileSize
new6.46 KB

Rerolled.

jlbellido’s picture

Status: Needs review » Needs work
Issue tags: +SprintWeekend2015

After a little bit working on it @javiprada1 and me found this minor mistakes in #44:

  • +      ->set('password_reset_text', $form_state->getValue('user_password_reset_text'))
    

    is missed in #44

  • We have to remove a white line in 154.

We have worked on it at sprintweekend2015

We still need to update the strings suggested in #39

manningpete’s picture

Issue tags: -Needs reroll

Patch applies.

luismagr’s picture

Assigned: Unassigned » luismagr
Issue tags: +Needs reroll

The patch doesn't apply.

luismagr’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
StatusFileSize
new6.96 KB

I have rerolled the patch.

luismagr’s picture

Assigned: luismagr » Unassigned
David_Rothstein’s picture

I think the privacy concern here is somewhat overstated, given that Drupal does not verify email address. Once I have an account on a Drupal site, I can edit the account and change the email address to president@whitehouse.gov. That doesn't prove that Barack Obama or his staff are members of the site.

With #85494: Use email verification when changing user email addresses it would be more of an issue though.

rogerrogers’s picture

A fairly rudimentary privacy issue. Any plans on backporting this fix for Drupal 7?

rogerrogers’s picture

[Double posted]

cilefen’s picture

A fairly rudimentary privacy issue. Any plans on backporting this fix for Drupal 7?

It is tagged "Needs backport to D7" so, yes.

rogerrogers’s picture

Thanks @cilefen for pointing that out. If you are in the know, any idea of the priority/timeframe?

cilefen’s picture

Version: 8.0.x-dev » 8.1.x-dev
Status: Needs review » Needs work

According to the backport policy this must be fixed in Drupal 8 before making it to Drupal 7. According to Allowed changes during the Drupal 8 release cycle, since this issue makes "string, markup, user interface, or render array changes", it must be postponed to 8.1, which will release in about 6 months.

The best thing anybody can do to move this along right now is to provide a detailed review. Here is a partial review:

  1. +++ b/core/modules/user/src/AccountSettingsForm.php
    @@ -186,6 +186,39 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    +      '#title' => $this->t('Privacy settings'),
    +      '#description' => $this->t('Due to privacy concerns the texts for user password reset or
    +      registration should not tell anything about already registered users. So the messages should not contain the
    

    I would not use the word "texts" in a sentence like this. "messages" would be fine. The whole sentence is wordy for me.

  2. +++ b/core/modules/user/src/Form/UserPasswordForm.php
    @@ -141,16 +144,22 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
    +    $message = \Drupal::config('user.settings')->get('password_reset_text');
    

    How is this text going to exist without a hook_update_N() setting it?

+++ b/core/modules/user/config/install/user.settings.yml
@@ -14,3 +14,4 @@ cancel_method: user_cancel_block
+password_reset_text: 'Due to privacy concerns, the policy of this web site is not to disclose the existence of registered email addresses. Hence, if you entered a valid email address or username, a password reset link is now being sent to it. If the email address you entered does not exist, you will not get a reset link, and will neither get any confirmation or warning about that here. If you get an email, it should generally arrive within 30 minutes or so, sometimes instantly.'

The default message is unnecessarily long. What about something like "If the email address or username you provided is associated with a site account, you will receive an email with instructions for resetting your password."?

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

Drupal 8.1.0-beta1 was released on March 2, 2016, which means new developments and disruptive changes should now be targeted against the 8.2.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.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

sophie.sk’s picture

Assigned: Unassigned » sophie.sk
Issue tags: +Dublin2016

Picking this up, we have a client need.

sophie.sk’s picture

Patch re-rolled against latest 8.3 dev. Haven't included an interdiff as it's reasonably different but I can provide one if requested.

Some other changes:
* Removed 'include signatures' from the patch
* Changed text values so they are more concise
* Added an update function to set the value of password_reset_text for existing installations
* Removed extra form validation because that field is required
* Set the value in the admin form
Will work on a D7 backport. Gotta install D7 locally on shaky Dublin wifi first :-)

New patch attached. Did I hear that there should be a tests-only patch for testbot? This is the first time I've done core patches with tests so any advice on that would be appreciated.

sophie.sk’s picture

Status: Needs work » Needs review

Changing status,

sophie.sk’s picture

StatusFileSize
new6.95 KB

Patch for Drupal 7.x dev attached. Default variable values!

sophie.sk’s picture

Status: Needs work » Needs review
StatusFileSize
new3.07 KB

Nope, I need to change directory first. Reuploading the correct D7 patch. Now to figure out why the D8 patch failed.

sophie.sk’s picture

StatusFileSize
new3.9 KB
new6.95 KB

I did wonder if there were tests in d7 ... new patches for both d7 and d8. I don't know why the d8 one is failing - I only changed two lines in that file and it's failing on something elsewhere.

sophie.sk’s picture

Status: Needs work » Needs review
sophie.sk’s picture

Component: other » user system
Assigned: sophie.sk » Unassigned
Status: Needs work » Needs review

7.x patch is ready for review, but I'm really not sure why the 8.x version is failing testing. Any thoughts appreciated.

sophie.sk’s picture

Status: Needs review » Needs work
markpavlitski’s picture

@Sophie.SK the D8 test failure is because submission of the reset password form would previously have failed with a validation error, and so would re-present the same form.

Now it always appears to succeed with the generic message (and redirects to the login page.)

You'll need to amend testUserPasswordReset() and ensure that the form is loaded again with $this->drupalGet('user/password'); before the second *real* submission.

lomasr’s picture

StatusFileSize
new3.9 KB

Small typo in '#description' in #66 . Resubmitting the patch with correction.

aerozeppelin’s picture

Status: Needs work » Needs review
StatusFileSize
new3.21 KB
new7.39 KB

An attempt to fix failing tests from #66.

I wasn't sure how to proceed in a situation where a blocked user tries to reset his password ? Should the displayed message be %name is blocked or has not been activated yet. or should it be If an account exists with these credentials, password reset instructions will be sent to the email associated with the account. Any thoughts ?

sophie.sk’s picture

StatusFileSize
new7.79 KB
new3.9 KB
new2.17 KB
new877 bytes

@aerozeppelin - The point of this issue is to *not* say "%name is blocked or has not been activated yet".

Attaching patch with typo amended for D7, and an updated version of the D8 patch, re-rolled against the latest 8.3.

kevinsiji’s picture

Tested. #75 patch for D7 works.

sophie.sk’s picture

Re-queued patches for the failing environments. Not sure why they're failing; they still apply cleanly to latest dev versions of 7 and 8 (with no changes that I can see), and they're failing on tests that haven't been touched in any of this work.

sophie.sk’s picture

Issue tags: +mssprintjan17
StatusFileSize
new9.02 KB
new2.07 KB
new3.9 KB

Finally figured out testbot for my D8 patch.

Still have no idea what the D7 testbot is doing! Resubmitting the same patch, passed all tests locally.

sophie.sk’s picture

Issue summary: View changes
Issue tags: +Novice

Updating issue summary to provide further tasks (for novices).

dpi’s picture

Although I am for this issue, Disclosure of usernames and user ids is not considered a weakness should be considered

markpavlitski’s picture

@dpi this issue also extends to email addresses.

aburrows’s picture

Had this on Drupalcamp London website - using https://www.drupal.org/project/username_enumeration_prevention solved it.

dpi’s picture

Title: Privacy issue with 'Request new password' form: anonymous can check if someone is registered » Can check whether an email or username is taken by using password reset form

@markpavlitski of course, I was only speaking for usernames :)

dpi’s picture

Title: Can check whether an email or username is taken by using password reset form » Password reset form reveals whether an email or username is in use

whoops, not that one

dpi’s picture

dpi’s picture

Try that again...

markpavlitski’s picture

Status: Needs work » Needs review

The tests are all green, so looks like it was a testbot issue. Marking for review.

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

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now 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.

sophie.sk’s picture

Rerolled the patch against 8.4.x. The same patch applies and the interdiff is empty, so not uploading a new one.

swilmes’s picture

Is there a version of this patch that works for 8.3.0-rc2?

sophie.sk’s picture

Swilmes, you could try the latest version (which applied against 8.4 dev), or else the one in #80 above (working 8.3 version).

pfructuoso’s picture

Assigned: Unassigned » pfructuoso
Status: Needs review » Needs work

Latest patch doesn´t apply to 8.4.x

pfructuoso’s picture

Issue summary: View changes
StatusFileSize
new185.99 KB
new167.22 KB
new8.93 KB

Updated #80 patch to apply in 8.4.x

Before applying patch

After applying patch

pfructuoso’s picture

Assigned: pfructuoso » Unassigned
Issue summary: View changes
Status: Needs work » Needs review
kporras07’s picture

I just tested this and it works exactly as expected. Marking RTBC

kporras07’s picture

Status: Needs review » Reviewed & tested by the community

oops

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
  1. Nice to see this being fixed - we shouldn't be disclosing email addresses via the password reset form.
  2. +++ b/core/modules/user/config/schema/user.schema.yml
    @@ -50,6 +50,9 @@ user.settings:
    +      type: string
    

    This needs to be

    type: label<code> so it is translatable.
    </li>
    
    <li>
    <code>
    +++ b/core/modules/user/src/Tests/UserPasswordResetTest.php
    @@ -81,10 +81,13 @@ public function testUserPasswordReset() {
    -    $this->assertText(t('@name is not recognized as a username or an email address.', ['@name' => $edit['name']]), 'Validation error message shown when trying to request password for invalid account.');
    -    $this->assertEqual(count($this->drupalGetMails(['id' => 'user_password_reset'])), 0, 'No email was sent when requesting a password for an invalid account.');
    +    $message = $this->config('user.settings')->get('password_reset_text');
    +    $count = count($this->drupalGetMails(['id' => 'user_password_reset']));
    +    $this->assertText($message, 'Validation error message shown when trying to request password for invalid account.');
    +    $this->assertEqual(count($this->drupalGetMails(['id' => 'user_password_reset'])), $count, 'No email was sent when requesting a password for an invalid account.');
    

    I don't get why we're creating a $count variable and changing $this->assertEqual(count($this->drupalGetMails(['id' => 'user_password_reset'])), 0, 'No email was sent when requesting a password for an invalid account.');.

    As far as I can see only the first assertion needs changing.

  3. +++ b/core/modules/user/src/Tests/UserPasswordResetTest.php
    @@ -144,7 +147,7 @@ public function testUserPasswordReset() {
    -    $this->assertTrue( count($this->drupalGetMails(['id' => 'user_password_reset'])) === $before + 1, 'Email sent when requesting password reset using email address.');
    +    $this->assertTrue(count($this->drupalGetMails(['id' => 'user_password_reset'])) === $before + 1, 'Email sent when requesting password reset using email address.');
    

    Unrelated change. Out-of-scope.

  4. +++ b/core/modules/user/user.install
    @@ -98,3 +98,13 @@ function user_update_8100() {
    +/**
    + * Add password reset notification text.
    + */
    +function user_update_8400() {
    +  $config_factory = \Drupal::configFactory();
    +  $config = $config_factory->getEditable('user.settings');
    +  $default_value = t('If an account exists with these credentials, password reset instructions will be sent to the email associated with the account.');
    +  $config->set('password_reset_text', $default_value)->save(TRUE);
    +}
    

    This needs testing. Shouldn't be using t() - in fact it would probably be best if read the default config file and got the value from there. Less text to maintain. No need to have a $config_factory variable.

  5. +++ b/core/modules/user/src/Form/UserPasswordForm.php
    @@ -136,14 +130,22 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
    +    $name = $form_state->getValue('name');
    

    Only need this when logging the unknown or inactive.

  6. +++ b/core/modules/user/src/Form/UserPasswordForm.php
    @@ -136,14 +130,22 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
    +    else {
    +      $this->logger('user')->notice('Password reset form was submitted with an unknown or inactive account: %name.', ['%name' => $name]);
    

    I ummed and ahhed about this - given that we log 404s this makes sense. I think we should consider a logging different message for inactive accounts. To make it easy for a site administrator to why they are getting lots of invalid password reset form usage. The log messages need to be tested.

  7. +++ b/core/modules/user/src/Tests/UserPasswordResetTest.php
    @@ -174,9 +177,10 @@ public function testUserPasswordReset() {
    +    $message = $this->config('user.settings')->get('password_reset_text');
    

    Only need to get this message once for the entire test.

harshit.kansal’s picture

"If an account exists with these credentials, password reset instructions will be sent to the email associated with the account."

Message is relevant to update for this issue as:

"Email sent when requesting password reset using email address." message can still indicates that account exists on drupal site.

benjifisher’s picture

Issue summary: View changes
Issue tags: +Baltimore2017

I think the changes listed in #101 are clear enough that this issue still deserves the Novice tag.

David_Rothstein’s picture

This really seems like bad usability to me, and all of it is in service of a level of privacy that I don't think most sites actually want or care about...

Is there a way we can keep the current behavior (helpful error message) in place by default, but make it easily configurable so that sites which do have extra privacy requirements can get rid of it and replace it with generic text?

Also:

+      '#description' => $this->t('The text that appears when a user successfully submits the password reset form. Due to privacy concerns, it should not contain any information about previously registered users.'),

I don't understand this help text. It doesn't say anything about supporting tokens a.k.a. variable substitution (the form doesn't even have that, does it?), so if it's just raw text with no variable substitution I don't see how it would even be possible for the administrator to include user-specific information in it even if they wanted to...

+password_reset_text: 'If an account exists with these credentials, password reset instructions will be sent to the email associated with the account.'

I'm pretty sure it should say "...to the email address associated with the account" instead.

cjgratacos’s picture

@alexpott I went ahead and did from point 2 to point 7 from comment #101. I also applied the recomendations by @David_Rothstein on #103. Still a few things are missing due to my lack of knowledge on how it is done in the Drupal Core.

Points that still need to be done:

  1. From #101 point #1 is a bit unclear, needs a bit of clarification.
  2. From #101 point #4 still needs testing, personally I couldn't include the ones I wrote because, after writing the test, I tried to follow Running PHPUnit tests to run them, and I wasn't successful on it. I would recommend updating that instruction page to be more up to date and beginner friendly. It took me a while to get the hang of it, yet I wasn't successful on running the test locally, still trying to make them run. Any help is greatly appreciated it.
  3. From #101 point #6 still needs testing. This is due to the reason mentioned above.
  4. Personally, I still find that we should be more informative on the description mentioned on #103.
pritishkumar’s picture

Status: Needs work » Needs review
gordon’s picture

StatusFileSize
new4.78 KB

Re-roll Patch for 7.56

yogeshmpawar’s picture

Status: Needs work » Needs review

Triggering Test bots for the patch #108.

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.

sophie.sk’s picture

StatusFileSize
new8.06 KB

And we're back on this one...!

Patch re-rolled for 8.4 but we'll need a new one for 8.5 no doubt.

mohit1604’s picture

Any updates ?

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.

robpowell’s picture

I am working on this as part of SprintWeekend2018.

robpowell’s picture

robpowell’s picture

StatusFileSize
new8.29 KB

This hasn't succeeded since #96 which was on 8.4. I am going to reroll just those changes for 8.6 and then try to reapply #101 and #104.

robpowell’s picture

Status: Needs work » Needs review
robpowell’s picture

Awesome will add #105 and so what we get.

robpowell’s picture

StatusFileSize
new8.28 KB
new5.63 KB

Here's the patch that incorporates #105. @cjgratacos, I updated the phpUnit document - with a huge help by @phenaproxima- let me know if those work for you.

robpowell’s picture

kristen pol’s picture

Assigned: Unassigned » kristen pol

I'm going to test this so assigning to myself for now.

kristen pol’s picture

Thanks for the update. I tested the patch in #120 locally and unfortunately it didn't work for me. Prior to the patch, I saw the Sorry, john.doe@example.com is not recognized as a user name or an e-mail address. message text. After the patch (and running drush updb), I get redirected to the login page and there is no message.

Steps I took:

  1. Tested without patch
  2. Went to reset password page
  3. Filled in a bogus email address
  4. Got the old error message as expected
  5. Applied the patch
  6. Ran: drush updb, drush cim, drush cr
  7. Tested again
  8. Get redirected to the login page
  9. No message shown

I'll test on simplytest.me before switching the status just to be sure.

kristen pol’s picture

StatusFileSize
new193.6 KB

This did work on simplytest.me so something must be up with my local.

kristen pol’s picture

Status: Needs review » Needs work

I reviewed the code and noticed a couple nitpicks but otherwise it seems fine.

  1. +++ b/core/modules/user/src/Tests/UserPasswordResetTest.php
    @@ -70,7 +70,10 @@ protected function setUp() {
    +    // Arrange constants to be used in the tests.
    

    Nitpick: "Arrange constants" is odd wording.

    Perhaps something like:

    Define constants to be used in the tests.

  2. +++ b/core/modules/user/src/Tests/UserPasswordResetTest.php
    @@ -70,7 +70,10 @@ protected function setUp() {
    +    // Verify that accessing the password reset form without having the session.
         // variables set results in an access denied message.
    

    Awkward wording. And, is there supposed to be a period after session? If it's session.variables then it should stay on one line.

  3. Perhaps something like:

    Verify access denied message is shown if password reset form is accessed without a session.

kristen pol’s picture

Assigned: kristen pol » Unassigned
mohit1604’s picture

StatusFileSize
new8.28 KB

Did changes as per #125 1st point , 2nd point still to be done.

alexpott’s picture

+++ b/core/modules/user/user.install
@@ -98,3 +98,12 @@ function user_update_8100() {
+/**
+ * Add password reset notification text.
+ */
+function user_update_8601() {

This can/should be a post update. See core/modules/system/system.post_update.php

Also this won't work. In order to do this you need to read the value from the user module's supplied configuration.

So we need a test too.

kristen pol’s picture

Issue tags: +Needs tests
udayraj123’s picture

A thought from a security tester's view -

https://hackernoon.com/username-or-password-is-incorrect-is-bullshit-899...

In the last paragraph, he's mentioned what would be the best practice according to him.

no2e’s picture

#130: Yes, a solution is needed for the registration form, too. We have a separate issue for this:

#2346389: Prevent registered email address enumeration via user registration form

kristen pol’s picture

I was wondering why it didn't work on my local (see #123) but worked on simplytest.me (see #124) so I tried again on my local and I understand what is happening now. Per #128, the update hook doesn't do the trick.

I was able to get it to work on my local only by doing a full installation with the patch in place. It did not work when I had a fresh install and then applied the patch and then ran drush updb / drush cim / drush cr.

diamondsea’s picture

An additional way of determining existing accounts is to track the timing of the page response, as there will typically be a difference in the response time (might only be milliseconds, but detectable). A defense against this is to include a random delay function such as

usleep(rand(100, 750)); // add random delay of .1-.75 seconds to response to prevent timing attacks

before returning the page.

This will add some wobble to the response time and prevent identifying existing accounts through this vector.

This will also slow down a brute force iteration of accounts, but at the expense of keeping server resources held a little longer.

The settings can be stored in the yml file so they can be overridden in settings.php if desired.

The same timing should be added in #2346389: Prevent registered email address enumeration via user registration form to prevent that from being iterated.

robpowell’s picture

@alexpott re: #128, can you elaborate more on why the current update won't work and what testing needs to be added? From @kristen_pol testing, I see it works for a full install but doesn't for an already existing site.

In order to do this you need to read the value from the user module's supplied configuration.

+++ b/core/modules/user/user.install
@@ -98,3 +98,12 @@ function user_update_8100() {
+function user_update_8601() {
+  $config = \Drupal::configFactory()->getEditable('user.settings');
+  $default_value = \Drupal::config('user.settings')->get('password_reset_text');
+  $config->set('password_reset_text', $default_value)->save(TRUE);

I read the above code as, get the password_reset_text config from the user module.

Mike Lewis’s picture

I just tested #127 in local here at Drupalcon Nashville. I would call it a partial success. The

Sorry, john.doe@example.com is not recognized as a user name or an e-mail address.

message is now gone, but it no longer says

Further instructions have been sent to your e-mail address.

even when I enter a good address.

webchuck’s picture

StatusFileSize
new8.35 KB

Attempting to fix #127 update issue. Hard coding the message into the hook_update_n() function.

kim.pepper’s picture

Status: Needs work » Needs review
StatusFileSize
new8.56 KB
new1.41 KB

Reroll #136 and move update hook to post update.

kim.pepper’s picture

StatusFileSize
new9.21 KB
new665 bytes

Fix for failing test.

lomasr’s picture

I tested the patch #139. I got no message after applying the patch. I think #135 still needs to be fixed.

acbramley’s picture

@lomasr did you run the post update hooks? Applying the patch and running the updates works for me.

acbramley’s picture

Status: Needs review » Needs work

Functionally this looks good but there's still a couple of items outstanding from #101:

  1. +++ b/core/modules/user/src/Form/UserPasswordForm.php
    @@ -136,13 +130,22 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
    +        $this->logger('user')->notice('Password reset instructions mailed to %name at %email.', ['%name' => $account->getUsername(), '%email' => $account->getEmail()]);
    ...
    +      $this->logger('user')->notice('Password reset form was submitted with an unknown or inactive account: %name.', ['%name' => $name]);
    

    Need tests for this.

  2. +++ b/core/modules/user/user.post_update.php
    @@ -20,3 +20,12 @@ function user_post_update_enforce_order_of_permissions() {
    +  $config = \Drupal::configFactory()->getEditable('user.settings');
    +  $default_value = 'If an account exists with these credentials, password reset instructions will be sent to the email address associated with the account.';
    +  $config->set('password_reset_text', $default_value)->save(TRUE);
    

    Need tests for this. We could also make it a one-liner.

    @alexpott also suggested reading the text directly from the config install file.

kim.pepper’s picture

Status: Needs work » Needs review
StatusFileSize
new10.04 KB
new1.57 KB

#142

  1. Not sure if this is the right approach for testing log messages in a web test, but gave it a go.
acbramley’s picture

+++ b/core/modules/user/src/Tests/UserPasswordResetTest.php
@@ -81,10 +84,12 @@ public function testUserPasswordReset() {
+    $this->assertErrorLogged(t("Password reset form was submitted with an unknown or inactive account: %name.", ['%name' => $this->account->getAccountName()]));

@@ -92,6 +97,10 @@ public function testUserPasswordReset() {
+    $this->assertErrorLogged(t("Password reset instructions mailed to %name at %email.", [

We shouldn't be using t() in tests.

zuhair_ak’s picture

Status: Needs work » Needs review
StatusFileSize
new9.99 KB
new1.75 KB

Replaced t function with sprintf as mentioned in #145

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.

lomasr’s picture

How about message like this "If this account is valid, you will receive password reset instructions in your inbox."

zaporylie’s picture

@lomasr - a similar message is already a part of the proposal, i.e. "If an account exists with these credentials, password reset instructions will be sent to the email address associated with the account."

lomasr’s picture

@zaporylie - Looks good, I just suggested to have a shorter message with same meaning.

mjpa’s picture

Status: Needs review » Needs work
+        $this->messenger()->addStatus($this->t('Further instructions have been sent to your email address.'));

Surely that shouldn't be there?

izus’s picture

StatusFileSize
new10.03 KB

Hi,
tried to apply #146 but it needs a reroll.
here is the reroll of #146 with no addition nor modification

izus’s picture

Status: Needs work » Needs review
StatusFileSize
new9.92 KB
new761 bytes

Hi,

here is a patch and the interdiff adressing #152

Thanks

izus’s picture

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

Hi,
there was a patch change in a core commit yesterday so the patch didn't apply
the only difference between #154 and #156 is changing the path to UserPasswordResetTest.php file
Thanks

gaëlg’s picture

Issue tags: +DrupalEurope

I'll try to fix the last test error as part of DrupalEurope sprint.

mangy.fox’s picture

StatusFileSize
new9.22 KB

Patch from #139 no longer working on 8.6 due to a different test location, so rerolled.

elendev’s picture

Assigned: Unassigned » elendev

I'll take a look and do the manual testing

elendev’s picture

StatusFileSize
new27.05 KB
new34.25 KB
new42.27 KB
new56.89 KB

I've been able to reproduce the issue without the patch, with the Umami and Out-of-the-box drupal installation :
Drupal 8.7.x unrecognized e-mail

Drupal 8.7.x recognized e-mail

I've applied the patch 1521996-156.patch (the 1521996-159.patch is not working, the path to the test class is not correct, I suppose it's the patch for D8.6).

When I try to retrieve the password with the known e-mail and with the unknown e-mail I get the following results :
Drupal 8.7.x patch applied

Drupal 8.7.x patch applied

The patch seems to work fine but the message is not displayed correctly (translation issue ?). I tried with French and English.

elendev’s picture

Assigned: elendev » Unassigned
mjpa’s picture

@mangy.fox the patch in #159 does not address #152

mangy.fox’s picture

StatusFileSize
new9.09 KB

@mjpa We just noticed that too! Here's another reroll with it removed.

gaëlg’s picture

Issue tags: -Novice

I don't know what to do with the test error in #156 (on 8.7.x). There are calls to ->assertErrorLogged() but this method is not in the class nor in the parent classes.
I found this method in :
- UncaughtExceptionTest
- Drupal\simpletest\TestBase

But we cannot extend these classes because we already extend another one. Should this method be moved to a trait?
Because of this, I guess this issue is not a novice one anymore?

claudiu.cristea’s picture

Version: 8.7.x-dev » 8.6.x-dev
Issue summary: View changes
Issue tags: -refactor account workflow, -Needs issue summary update, -SprintWeekend2015, -Dublin2016, -mssprintjan17, -Baltimore2017, -SprintWeekend2018, -SprintWeekendBOS, -Needs tests +Security improvements, +Needs upgrade path tests, +Needs usability review, +Needs security review

Here we go...

  1. The patch from #164 doesn't apply.
  2. It's a bug, so it should be in 8.6.x.
  3. Updated the issue summary.
  4. Cleaned up the issue tags.
  5. Needs usability review.
  6. Needs security review.
  7. I'm not sure we have to let this message configurable. At least, I would not expose this configuration in UI, making it hard to be changed. Also, I'm not sure the default message is correct, I would keep the actual text: "Further instructions have been sent to your email address." Period.
  8. +++ b/core/modules/user/src/Form/UserPasswordForm.php
    index 11d0691..9027162 100644
    --- a/core/modules/user/src/Tests/Functional/UserPasswordResetTest.php
    
    --- a/core/modules/user/src/Tests/Functional/UserPasswordResetTest.php
    +++ b/core/modules/user/src/Tests/Functional/UserPasswordResetTest.php
    

    This test was converted to functional and javascript functional tests.

  9. +++ b/core/modules/user/user.post_update.php
    @@ -20,3 +20,12 @@ function user_post_update_enforce_order_of_permissions() {
    +function user_post_update_add_password_reset_text() {
    

    Needs an update path test.

claudiu.cristea’s picture

More on this...

  1. +++ b/core/modules/user/src/Form/UserPasswordForm.php
    @@ -116,17 +116,11 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
    +      // The account is valid and active.
    +      if ($account->isActive()) {
    

    What is the reason for testing if the account is active?

  2. +++ b/core/modules/user/src/Form/UserPasswordForm.php
    @@ -136,13 +130,21 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
    +    if ($account && $account->id()) {
    

    Instead of $account->id(), use !$account->isAnonymous().

    Also, what if the user is blocked? Should we send him also the E-mail?

EDIT: In fact we should move the whole logic in UserPasswordForm::submitForm() method. Now, that we don't show a validation error anymore, we should drop ::validateForm() entirely.

mangy.fox’s picture

StatusFileSize
new7.05 KB

Reworked #164 for 8.6.x, and checked that it also applies to 8.6.3.

I have also made some changes, as suggested by @claudiu.cristea

* Removed the password reset text field from the UI, so now it has to be updated manually in config or overridden
* Simplified the default message
* Moved the logic from validation to submit as there is no validation to fail
* Updated the new functional tests

As far as I am aware, it is the "isActive" check that prevents blocked users from receiving the password reset email.

I'm not really sure what an update path test looks like so that is still to do.

huzooka’s picture

Status: Needs work » Needs review

Triggering testbot

Anonymous’s picture

Wouldn't this better be addressed by implementing a notifications plugin which would allow users to modify all their notifications and would expose an API to the plugins? In this case the User module would "publish" 3 user editable (through plugin UI) messages for the password reset form through the API. Such a plugin would even allow an even greater modularity for Drupal.
Hope this makes sense. If not, I will be happy to elaborate.

ronald.garcia’s picture

StatusFileSize
new8.25 KB

Reroll #96.
8.4.x : PHP 7 : MySQL 5.6.35
password-reset-text-1521996-173.patch (8.25 KB)

alanhdev’s picture

StatusFileSize
new5.36 KB

Re-rolled the previous patch from #168 so it applies to 8.7.x

EDIT: Work in progress. That doesn't apply properly. I'll rework it.

alanhdev’s picture

StatusFileSize
new6.21 KB

Re-rolled patch from #168 to apply to 8.7.x

alanhdev’s picture

StatusFileSize
new7.09 KB

Fixed a failing test in #173.

sharif.elshobkshy’s picture

Hi guys.

For now, instead of applying a core patch, I'm trying (unsuccessfully) to override that validateForm function for UserPasswordForm.

  protected function alterRoutes(RouteCollection $collection) {
    // Pass Reset form.
    if ($route = $collection->get('user.pass')) {
      $route->setDefault('_form', '\Drupal\my_module\Form\NewUserPasswordForm');
    }
  }

namespace Drupal\my_module\Form;

use Drupal\user\Form\UserPasswordForm;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Render\Element\Email;
use Drupal\user\UserStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides a user password reset form.
 *
 * @internal
 */
class NewUserPasswordForm extends UserPasswordForm {

    /**
     * {@inheritdoc}
     */
    public function buildForm(array $form, FormStateInterface $form_state) {
      // I'M ABLE TO OVERRIDE THE BUILD FORM FUNCTION
    }

    /**
     * {@inheritdoc}
     */
    public function validateForm(array &$form, FormStateInterface $form_state) {
      // I'M NOT ABLE TO OVERRIDE THE VALIDATE FORM.
      // Always goes to the original validateForm
    }

}

As described above, I'm able to override the class and all public functions other than the validateForm.

Also, if I try to override the validate function in a hook_form_alter, like this:

 $form['#validate'] = ['myValidateFormCallback];

It stills goes to the original validateForm callback.

Can anyone help me with this particular issue? Why am I not able to override the validateForm callback?

Thanks in advance.

UPDATE: Nevermind. I discovered I'm not able to override the validateForm because of a contrib module hardcoding and forcing the callback (user_registrationpassword).
Thanks.

ashu1629’s picture

ashu1629’s picture

Assigned: Unassigned » ashu1629
ashu1629’s picture

StatusFileSize
new7.06 KB

Wrapping $message inside t() function in submit function. It is a change in patch https://www.drupal.org/files/issues/2018-11-14/password-reset-text-15219...

ashu1629’s picture

StatusFileSize
new5.5 KB

Hi guys,

The following patch does not use the configuration variable to set the message. This approach helps us in the following ways:
1. The message can be made translatable by passing it through t() function. Earlier we may need to pass the $message variable via t() which is not according to Drupal standards.
2. The message still cannot be changed using UI so security is maintained.
3. If anyone wants to change the message, he or she can do it using the translation interface easily. This right generally remains with the administrator, so security is also maintained.

cc @claudiucristea

ashu1629’s picture

Assigned: ashu1629 » Unassigned
Status: Needs work » Needs review
ashu1629’s picture

StatusFileSize
new5.46 KB
matteodem’s picture

StatusFileSize
new7.1 KB

This patch makes the configured message translatable.

darrenwh’s picture

StatusFileSize
new4.49 KB

Rerolling the patch for the latest version of core 8.8.0

darrenwh’s picture

Status: Needs work » Needs review
darrenwh’s picture

Removed my last contributed patch as it failed

darrenwh’s picture

Priority: Normal » Major

Bumping this up to a Major, the current functionality allows malicious visitors to find out if people have accounts on the site, if the site is of a sensitive nature it can have serious consequences.

klausi’s picture

Issue summary: View changes

I think this should not be major because an attacker can also find out on the registration form if a certain email address has an account.

So this patch only helps if you have no registration form on your drupal site. Otherwise attackers can just use the registration form instead.

Please also note that other sites such https://github.com/ also have this vulnerability. I can check any email address if it has an account on the registration form.

This also 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.

So although the decreased usability vs. better privacy is probably a tradeoff we accept I still think this should not be major.

cilefen’s picture

Priority: Major » Normal

Good points, and as far as I know, https://www.drupal.org/project/username_enumeration_prevention is the thing to use until this lands. On my *ehem* non-Drupal projects we tend not to allow enumeration this way if there is no registration feature.

alanhdev’s picture

There are valid scenarios where it is not desirable that a site discloses whether a user account is active. It's especially relevant if the email address is used to log in.

However, the password reset process is only one of the potential points of disclosure. For instance, even if a generic login failure message is used, user flood protection will reveal the validity of a user as it is based on the account ID and IP rather than the actual name entered in the form.

In order for it to be worthwhile pursuing, all potential points of disclosure/enumeration must be covered. Arguably, this is an issue that would be better handled by enhancing the username_enumeration_prevention module for those that need it due to the nature of the site rather than trying to patch core to fix something that the community largely agrees isn't broken.

no2e’s picture

@klausi (#191)

I think this should not be major because an attacker can also find out on the registration form if a certain email address has an account.

We have a separate issue for the registration form: https://www.drupal.org/project/drupal/issues/2346389

If 'major' is warranted for one, it should be given to both issues, I guess.

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

Drupal 8.6.x will not receive any further development aside from security fixes. Bug reports should be targeted against the 8.8.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.9.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.

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

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should 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.

spudley’s picture

@no2e yes, both issues should be major. Come on Drupal; you've had patches for this submitted over and over again for the best part of a decade. How is it not done already?

nginex’s picture

StatusFileSize
new4.64 KB

Reroll against 8.9.x

ravi.shankar’s picture

Assigned: Unassigned » ravi.shankar

Working on this.

ravi.shankar’s picture

Assigned: ravi.shankar » Unassigned
Status: Needs work » Needs review
StatusFileSize
new4.65 KB
new897 bytes

Here I have tried to fix failed tests.

jozzy_a’s picture

Alter patch 198/200 to work on 8.8.8

jozzy_a’s picture

jozzy_a’s picture

kristen pol’s picture

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

I'm unclear why the patches are being rolled against 8.*. It normally should be created against 9.1 and then it will be backported if it's eligible. Let me know if I'm missing a reason why 8.* is targeted.

jozzy_a’s picture

This security vulnerability also has an impact on the 8.8.x branch.
According to https://www.drupal.org/project/drupal/releases/8.8.8 there would be security coverage until 2nd December 2020. This final patch created on this ticket should really be ported over to 8.8.x as there is 4 month left of the security coverage.

I am happy to create an additional issue for this patch to be rolled against the latest 8.8.x release?

kristen pol’s picture

@jozzy_a Thanks. Normally the most recent version is targeted and then there are rerolls for previous versions as needed. Since the 8.8* patch is failing tests, IMO it would be good to focus on the 9.1 version first.

kristen pol’s picture

Issue tags: +Bug Smash Initiative

Not that we want more tags but working on this for the Bug Smash Initiative and forgot to tag it.

jozzy_a’s picture

quietone’s picture

Status: Needs work » Needs review
StatusFileSize
new2.12 KB
new3.62 KB

Rerolling for 9.1.x

quietone’s picture

Status: Needs work » Needs review
StatusFileSize
new1.7 KB
new4.5 KB

The test is failing in the form validation where "%name is not recognized as a username or an email address." which contains information that according to the issue summary should not be displayed. Removing that from the validation allows the test to pass. Let's see if any other test fails.

longwave’s picture

Status: Needs review » Needs work
Issue tags: -Needs upgrade path tests
+++ b/core/modules/user/src/Form/UserPasswordForm.php
@@ -170,13 +167,33 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
+    $this->messenger()
+      ->addStatus($this->t('If your account is locked, an email will be sent with instructions for resetting your password.'));

I don't think this message is correct. First I think it should be "if your account is unlocked...", and second: how do I know if my account is "locked" (or unlocked)? I think we need to come up with something a bit more user friendly.

Removing "needs upgrade path tests" as this is just a straight change, there is no upgrade path here.

quietone’s picture

Issue summary: View changes
Status: Needs work » Needs review
StatusFileSize
new3.57 KB
new1.67 KB
new4.43 KB
new4.34 KB

The IS suggests using the string, "Further instructions have been sent to your e-mail address.". How about that?

Actually, maybe this needs the UX review?

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.

vivek panicker’s picture

Assigned: Unassigned » vivek panicker
Status: Needs review » Needs work

Patch seems to fail to apply.
Need to create a new one.

vivek panicker’s picture

Assigned: vivek panicker » Unassigned
Status: Needs work » Needs review
StatusFileSize
new4.43 KB
new2.59 KB
vivek panicker’s picture

StatusFileSize
new4.43 KB
new1.32 KB

Change the string e-mail to email since otherwise translations were breaking in my project.

quietone’s picture

StatusFileSize
new3.08 KB
new4.55 KB
quietone’s picture

StatusFileSize
new704 bytes
new4.53 KB

On a brief self review I noticed that a comment that needs changing.

+++ b/core/modules/user/src/Form/UserPasswordForm.php
@@ -159,22 +159,39 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
+      // Mail one time login URL and instructions using current language.

s/using current language//

dww’s picture

I almost never use the UI for this, but a few days ago I had an occasion to do so. As the form submitted successfully, I said to myself: "that’s too bad, the UI confirms my email address for anonymous users." Then @quietone pinged about this in #bugsmash and I was happy to volunteer to review.

  1. The summary is clear and accurate. I only updated remaining tasks (since the tests have been fixed).
  2. The code is clean, well-documented, and correct. No CS violations.
  3. I'm not currently an active member of the Drupal Security Team, but I'm an alumnus of that team, and someone who's been dealing with computer security for over 25 years. This is the correct change for this problem. Drupal should *not* inform anonymous users about the results of trying to validate this reset form. It should always show the same thing to everyone, regardless of what they type. So I'm removing the "Needs security review" tag.
  4. Arguably, we could print something like "If your account is valid, further instructions have been sent to your e-mail address." That'd potentially mitigate the usability concerns that the site says it did something, the user keeps checking their inbox but doesn't see a message. However, in UI text (and issue comments), less is more. So I think what's in the patch is better.
  5. I'm not sure what a Usability review will uncover that hasn't already been considered. This is definitely a step backwards if you only consider usability. But in this case, security is more important. So I'm also removing the 'Needs usability review' tag.

I see nothing else to improve here, either in the issue, nor the patch. Therefore, RTBC.

Thanks everyone!
-Derek

dww’s picture

Status: Needs review » Reviewed & tested by the community

Sorry, I missed. ;)

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/modules/user/src/Form/UserPasswordForm.php
    @@ -159,22 +159,39 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
             $form_state->setValueForElement(['#parents' => ['account']], $account);
    

    This value is no longer used. However I think we should still use it. It makes us less likely to break things like http://codcontrib.hank.vps-private.net/node/31023764#line-483

  2. +++ b/core/modules/user/src/Form/UserPasswordForm.php
    @@ -159,22 +159,39 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
    -    $account = $form_state->getValue('account');
    -    // Mail one time login URL and instructions using current language.
    -    $mail = _user_mail_notify('password_reset', $account);
    -    if (!empty($mail)) {
    -      $this->logger('user')->notice('Password reset instructions mailed to %name at %email.', ['%name' => $account->getAccountName(), '%email' => $account->getEmail()]);
    -      $this->messenger()->addStatus($this->t('Further instructions have been sent to your email address.'));
    +    $name = trim($form_state->getValue('name'));
    +    // Try to load by email.
    +    $users = $this->userStorage->loadByProperties(['mail' => $name]);
    +    if (empty($users)) {
    +      // No success, try to load by name.
    +      $users = $this->userStorage->loadByProperties(['name' => $name]);
    +    }
    +    $account = reset($users);
    +
    +    if ($account && !$account->isAnonymous() && $account->isActive()) {
    +      // Mail one time login URL and instructions.
    +      $mail = _user_mail_notify('password_reset', $account);
    +      if (!empty($mail)) {
    +        $this->logger('user')
    +          ->notice('Password reset instructions mailed to %name at %email.', [
    +            '%name' => $account->getAccountName(),
    +            '%email' => $account->getEmail(),
    +          ]);
    +      }
    +    }
    +    else {
    +      $name = $form_state->getValue('name');
    +      $this->logger('user')
    +        ->notice('Password reset form was submitted with an unknown or inactive account: %name.', ['%name' => $name]);
         }
    +    $this->messenger()
    +      ->addStatus($this->t('Further instructions have been sent to your e-mail address.'));
    

    This code could be...

      public function submitForm(array &$form, FormStateInterface $form_state) {
        $account = $form_state->getValue('account');
        if ($account) {
          // Mail one time login URL and instructions using current language.
          $mail = _user_mail_notify('password_reset', $account);
          if (!empty($mail)) {
            $this->logger('user')->notice('Password reset instructions mailed to %name at %email.', [
              '%name' => $account->getAccountName(),
              '%email' => $account->getEmail()
            ]);
          }
        }
        else {
          $this->logger('user')
            ->notice('Password reset form was submitted with an unknown or inactive account: %name.', ['%name' => $form_state->getValue('name')]);
        }
    
        $this->messenger()->addStatus($this->t('Further instructions have been sent to your email address.'));
        $form_state->setRedirect('user.page');
      }
    

    less change and less repeated loading of entities.

  3. +++ b/core/modules/user/tests/src/Functional/UserPasswordResetTest.php
    @@ -465,9 +466,9 @@ public function assertValidPasswordReset($name) {
         // Make sure the error text is displayed and no email sent.
    

    It's no longer error text - right? I think that the comment should be changed to say that this text is deliberately the same as the success message for privacy.

quietone’s picture

Status: Needs work » Needs review
StatusFileSize
new3.57 KB
new4.29 KB

@alexpott, thank you for the review.

This patch fixes all the points in #223.

dww’s picture

Thanks, @alexpott, and sorry for not noticing #223.2. Agreed #224 is better.

The test fail is an unrelated JS random.

But instead of re-queing, maybe we can fix this, too:

+++ b/core/modules/user/tests/src/Functional/UserPasswordResetTest.php
@@ -31,7 +31,7 @@ class UserPasswordResetTest extends BrowserTestBase {
-   * @var \Drupal\language\LanguageManagerInterface
+   * @var \Drupal\language\ConfigurableLanguageManagerInterface

This seems like an out-of-scope change. Sorry I didn't notice that before, either.

Thanks,
-Derek

quietone’s picture

Status: Needs work » Needs review
StatusFileSize
new504 bytes
new4.02 KB
+++ b/core/modules/user/tests/src/Functional/UserPasswordResetTest.php
@@ -31,7 +31,7 @@ class UserPasswordResetTest extends BrowserTestBase {
-   * @var \Drupal\language\LanguageManagerInterface
+   * @var \Drupal\language\ConfigurableLanguageManagerInterface

Removing out of scope change, thanks to dww on Slack.

The failing test is in \Drupal\Tests\field_layout\FunctionalJavascript\FieldLayoutTest::testChangingFormatterAndRegion which I hope is an unrelated random test failure. I don't think I have even been able to run FunctionalJavascript tests locally so won't be much help if it is not.

dww’s picture

Status: Needs review » Reviewed & tested by the community

Thanks @quietone and @alexpott!

#223 is fully addressed, as is #226. Smaller, simpler patch with a tighter scope. Tests are passing as expected. All's well. Back to RTBC.

Cheers,
-Derek

mjpa’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new5.76 KB

1. The issue description mentions removing the "that email is blocked" message, because it still lets someone know that the user had an account. The patch in #227 doesn't remove that message. There's also the message stating the request was blocked as the account has had too many password reset attempts - this again lets someone know the email is used on an account. Unless I've missed something, both of those messages shouldn't be there?

2. I don't agree with the message "Further instructions have been sent to your email address." that gets shown regardless of it being a valid email or not, it suggests the email exists. We know its shown to everyone, but will it make someone trying to check if an email exists on an account assume an account does exist? Would something like "If a matching active account was found, further instructions will have been sent to your email address." be better?

Attached a patch addressing part 1 as I think that should be done. Patch does nothing for part 2 at it's just my opinion and looks to have been discussed previously and the message in the patch agreed on?

ressa’s picture

Thanks for weighing in @mjpa, perhaps you can include an interdiff to make it easier to spot the changes?

mjpa’s picture

StatusFileSize
new9.61 KB
new6 KB

Yep, not a problem. Realised I'd forgotten about the tests, so this patch includes updates to the tests that I think are correct but want another set of eyes on them as there seems to be 2 different ways that the existing tests check if an email was sent?

Interdiff is between #226 and this patch, other than changes to the tests, its the same as #229.

mjpa’s picture

Status: Needs work » Needs review
alexpott’s picture

+++ b/core/modules/user/src/Form/UserPasswordForm.php
@@ -169,13 +160,22 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
 
+    $this->messenger()->addStatus($this->t('Further instructions have been sent to your email address.'));

It feels a comment is worth it here to explain that the same message is used regardless of whether an email is sent to increase privacy.

quietone’s picture

StatusFileSize
new1.71 KB
new9.63 KB

Addressing #234, and #223, about adding a new comment in UserPasswordForm. Previously, I wrongly put it in the test.

quietone’s picture

Issue summary: View changes

229.1. Yes, that should be changed too.
229.2 There is still work to do to agree on the text of the message. There the suggestion is "If a matching active account was found, further instructions will have been sent to your email address.".

I did some searching and testing with gitlab and my email service etc and came to the conclusion that the best option is to use the suggestion from the OWASP Authentication Cheat Sheet Password recovery which is "If that email address is in our database, we will send you an email to reset your password."

dpagini’s picture

I recently came across this issue and haven't read all the way back up here... This seems like it's getting pretty close lately to having some consensus.
What about agreeing on a default, and providing the message as a config value so sites can change it? Or maybe the default can be what's there today, and we can at least give sites a way to comply with OWASP recommendation?
Just some thoughts I had while reviewing...

quietone’s picture

StatusFileSize
new428 bytes
new9.62 KB
quietone’s picture

Issue summary: View changes
StatusFileSize
new1.5 KB
new9.69 KB

This patch implements the OWASP Authentication Cheat Sheet Password recovery suggestion of "If that email address is in our database, we will send you an email to reset your password."

quietone’s picture

Issue summary: View changes
StatusFileSize
new6.9 KB

Update after screenshot in the IS

larowlan’s picture

+++ b/core/modules/user/src/Form/UserPasswordForm.php
@@ -169,12 +160,23 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
+    // Make sure the status text is displayed and no email sent. This message

Should this would be 'Make sure the status text is displayed andeven if no email is sent'?

Other than that, this looks great to me

quietone’s picture

StatusFileSize
new1019 bytes
new9.69 KB

Yes, I agree. Although I added 'was', as well.

    // 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.
larowlan’s picture

Status: Needs review » Reviewed & tested by the community

This looks good to me

mjpa’s picture

Looks good to me too

dww’s picture

StatusFileSize
new9.77 KB
new1.33 KB

Thanks for all the improvements and progress in here!

Re: #229.1: Good catch!
Re: #229.2: Yeah, I said something similar in #221.4. Addressed by #239.
Re: #234: 👍 Agreed.
Re: #236: 👍Sounds good.
Re: #237: If sites want to change this, they can alter the form. I think we should have a good default and not have a confusing config knob for this.
Re: #238: Thanks for the link, glad to see that other issue is now fixed, too. 😉
Re: #241: 👍 Agreed.
Re: #242: In light of #229.1, I applied the patch and carefully reviewed everything in validateForm() and submitForm(). Everything now looks great, with one possible super tiny nit:

+++ b/core/modules/user/src/Form/UserPasswordForm.php
@@ -169,12 +160,23 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
+        $this->logger('user')->notice('Password reset instructions mailed to %name at %email.', [
+          '%name' => $account->getAccountName(),
+          '%email' => $account->getEmail(),
+        ]);
...
+      $this->logger('user')
+        ->notice('Password reset form was submitted with an unknown or inactive account: %name.', ['%name' => $form_state->getValue('name')]);

It's a little jarring that these two similar logger() calls are wrapped differently. Maybe worth being more consistent? Attached here in case @quietone and others think it's an improvement, but leaving #242 as the RTBC patch...

Thanks again!
-Derek

p.s. Looking at UserPasswordResetTest I see there's nothing that checks what ends up in the logs. That's nothing new. But I'm wondering if it's worth a follow-up to add some test assertions about logging activity in this whole process?

quietone’s picture

@dww, sorry about the jarring code. :-)

Yes, lets add a followup for tests. Good on you for checking on that. I have searched the user.module issue and I don't see a similar issue so it doesn't look like a new one would be a duplicate.

longwave’s picture

+++ b/core/modules/user/src/Form/UserPasswordForm.php
@@ -169,12 +160,26 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
+    $this->messenger()->addStatus($this->t('If that email address is in our database, we will send you an email to reset your password.'));

This is probably bikeshedding but I don't think we use the "we" form elsewhere in messages. Also, sending the email doesn't reset the password by itself; further steps must be taken.

How about "If that email address is in our database, an email will be sent with instructions to reset your password."

quietone’s picture

Issue summary: View changes
StatusFileSize
new1.8 KB
new9.76 KB

OK, let's try that one.

alexpott’s picture

Ticked the credit boxes for people who have made comments that have helped move the issue along or changed the patch in a significant way.

As @penyaskito points out this is half the problem - the other half is #2346389: Prevent registered email address enumeration via user registration form - however given sites with a focus on privacy often don't allow registration I think it is okay to proceed here.

alexpott’s picture

Status: Reviewed & tested by the community » Needs review
+++ b/core/modules/user/src/Form/UserPasswordForm.php
@@ -169,12 +160,26 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
+    // 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 that email address is in our database, an email will be sent with instructions to reset your password.'));

So the thing we lose here is the usability of knowing when you've mistyped your email address. That's understandable given the aim of the issue. I think we can fix this by repeating the user input in this string. This was recommended a long long time ago in this issue... see #2 from @pdrake.

I think we should consider making the message:

$this->messenger()->addStatus($this->t('If @identifier is in our database, an email will be sent with instructions to reset your password.', ['@identifier' => $form_state->getValue('name')));

Because then at least the user has the chance to think "oh drats" I mistyped that.

spokje’s picture

Assigned: Unassigned » spokje
Status: Needs review » Needs work

Creating patch addressing #250

spokje’s picture

Assigned: spokje » Unassigned
Status: Needs work » Needs review
StatusFileSize
new11.56 KB
new4.16 KB

Let's see what TestBot thinks about this patch addressing #250

quietone’s picture

Add another suggested text to the IS and a new screen shot

dww’s picture

StatusFileSize
new11.09 KB
new1.76 KB

Thanks again for the progress here. Almost RTBC! ;)

If @identifier is in our database, an email will be sent with instructions to reset your password.

I'm not sure about "is in our database" for this text. That also seems out of step with most Drupal UI text. I originally proposed:

If your account is valid, further instructions have been sent to your e-mail address.

I agree with #250 that putting what the user typed into the message will help the UX concerns, so how about this?

If @identifier is a valid account, an email will be sent with instructions to reset your password.

Or something? 😉

Meanwhile, some more tiny nits:

  1. +++ b/core/modules/user/src/Form/UserPasswordForm.php
    @@ -117,7 +117,10 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    -    $form['actions']['submit'] = ['#type' => 'submit', '#value' => $this->t('Submit')];
    +    $form['actions']['submit'] = [
    +      '#type' => 'submit',
    +      '#value' => $this->t('Submit'),
    +    ];
    

    This change is out of scope.

  2. +++ b/core/modules/user/tests/src/Functional/UserPasswordResetTest.php
    @@ -379,18 +379,25 @@ public function testUserResetPasswordUserFloodControl() {
    +    // Ensure no further emails was sent.
    

    "emails was" isn't correct English grammar. Both need to be plural or both singular. "Ensure no further email was sent."

  3. +++ b/core/modules/user/tests/src/Functional/UserPasswordResetTest.php
    @@ -448,15 +461,16 @@ public function testUserResetPasswordUserFloodControlIsCleared() {
    +    // Ensure a further email was sent.
    

    "a further email was sent" is a little clumsy. How about:

    "Ensure another email was sent."

    ?

These 3 are fixed with the attached patch. I didn't want to unilaterally change the text, yet, so leaving that part for further consensus before making that change. If folks are happy with the old text, this patch is probably RTBC.

Thanks again,
-Derek

dww’s picture

StatusFileSize
new11.21 KB
new2.46 KB

Here's a patch for my proposed new wording:

If @identifier is a valid account, an email will be sent with instructions to reset your password.

If folks RTBC, they should be clear if they prefer #254 or #255.

Thanks!
-Derek

p.s. Also fleshed out code comments a bit in assertNoValidPasswordReset() to hopefully be more clear.

quietone’s picture

Issue summary: View changes

@dww, thanks for another suggestion with patch.

It seems that the patch itself is considered RTBC, except for the wording of the user facing text. To resolve this how we split this into two issues one to put the structure in place (this issue) and another to improve the message. I am suggesting this because this is a security related issue and we should get something in ASAP and make improvements later.

Now, that still leaves the problem of what to use as the message right now in this patch. For that I would suggest we stick with the OWASP suggestion. And then in a followup discuss how to word that for our community.

Would that work for everyone?

alexpott’s picture

Status: Needs review » Needs work

I made a mistake when I suggested using the @identifier - this should be %identifier. It should be really really clear what is the user supplied content.

I've also been pondering whether we should add validation that $form_state->getValue('name') is either a valid email address or a valid name in the form validation. See \Drupal\user\Plugin\Validation\Constraint\UserNameConstraintValidator::validate() and \Drupal\Component\Utility\EmailValidator::isValid(). This would prevent people from posting to this form and doing a reflected content sort of attack while still allowing a user to check their input if they've entered something that could be a username or an email address.

spokje’s picture

+1 for @quietone proposal in #256 to make the wording a separate, follow-up issue.

I can see this issue being open for another 8 years whilst we discuss wordings and eventually the suggested voice in your head to have the wording spoken out in. (I'm personally tied between Morgan Freeman and Samuel L. Jackson...)

I think the "general consensus" is that, besides the wording, the user-entered user-name/user-email-address should at least be in this message to prevent:

Because then at least the user has the chance to think "oh drats" I mistyped that.

.

spokje’s picture

StatusFileSize
new11.18 KB
new850 bytes

Attached patch addressed:

I made a mistake when I suggested using the @identifier - this should be %identifier. It should be really really clear what is the user supplied content.

Keeping at Needs work because this is not addressed yet:

I've also been pondering whether we should add validation that $form_state->getValue('name') is either a valid email address or a valid name in the form validation. See \Drupal\user\Plugin\Validation\Constraint\UserNameConstraintValidator::validate() and \Drupal\Component\Utility\EmailValidator::isValid(). This would prevent people from posting to this form and doing a reflected content sort of attack while still allowing a user to check their input if they've entered something that could be a username or an email address.

@alexpott in #257

dww’s picture

Status: Needs work » Needs review
StatusFileSize
new12.61 KB
new1.97 KB

Re: #256 - Not up to me to decide, but a) we're very close here, b) we've already identified problems with the default OWASP suggestion, c) the whole point of this issue is changing the user-facing text, d) core still has its usability gates and all that. So I think we're better off doing it all at once.

Re: #257: 👍 for %identifier. Since the form takes both email addresses and usernames as input, and since all valid email addresses are valid usernames, the most we can do is check for a valid username. Valid email is more restrictive, but we have to allow input that's not valid email.

Re: #258 I don't think that's fair. We're not debating wordings for the sake of delay, we're trying to get this to the point that a core committer will actually commit it.

So here's validation for the username and a test. This would user be easier if we could simply use user_validate_name()here. ;) That's not (yet?) deprecated, so it *should* be okay. So that's this patch. Much smaller change.

dww’s picture

StatusFileSize
new15.22 KB
new3.11 KB

Here, we "properly" re-implement user_validate_name() with DI and deprecation warnings and all that jazz. The lengths we go to avoid procedural code...

This is incomplete, since:

a) I didn't open a CR for a nid in the deprecation message
b) I didn't add a deprecation test for calling the constructor without the new service

I don't think any of this is worth the hassle. If folks don't like user_validate_name(), we can do all this when that's deprecated. I much prefer #260. But if we need it, here's most of the code.

Thanks,
-Derek

larowlan’s picture

I've also been pondering whether we should add validation that $form_state->getValue('name') is either a valid email address or a valid name in the form validation.

That feels out of scope here to me, its not clear if @alexpott meant in this issue, or as a follow-up - can you clarify @alexpott?

dww’s picture

Re: #262: Thanks for clarifying! If that's all for a follow-up, then #259 should be RTBC.

Also, so it doesn't get lost/forgotten, I opened #3189294: Add test coverage of logging performed by password reset form for the followup I proposed in the p.s. @ #245.

Cheers,
-Derek

alexpott’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/user/src/Form/UserPasswordForm.php
    @@ -133,7 +150,20 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
    -    $name = trim($form_state->getValue('name'));
    

    Let's not remove this trim... it's fine and helpful. It means we need a slightly different text but that's okay.

  2. +++ b/core/modules/user/src/Form/UserPasswordForm.php
    @@ -133,7 +150,20 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
    +    // First, see if the input is possibly valid as a username. Any valid email
    +    // address will be considered a valid username, and since this form accepts
    

    This is not true... the username has a length limit shorter than email address therefore we also need to check whether the thing is a valid email address address. We might want to do that before doing the username validation or add it to the if ($violations...

  3. +++ b/core/modules/user/src/Form/UserPasswordForm.php
    @@ -133,7 +150,20 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
    +    $definition = BaseFieldDefinition::create('string')
    +      ->addConstraint('UserName', []);
    +    $data = $this->typedDataManager->create($definition);
    +    $data->setValue($name);
    +    $violations = $data->validate();
    

    Love this. Something like this is exactly what I was hoping to see :)

quietone’s picture

Status: Needs work » Needs review

I was thinking the same as larowlan in #262.

@alexpott, can you clarify, please?

alexpott’s picture

@larowlan / @quietone we're reflecting user input in a form for anonymous users I think we should be double careful about what we put into the message to make it hard to construct a message that would be misleading. Using user name validation and email validation limits what a user can inject in the message. I'm not concerned about XSS here - auto-escape will take care of that. It's more the user engineering that possible with reflected content attacks.

dww’s picture

Assigned: Unassigned » dww
Status: Needs review » Needs work

Okay, I'll take a stab at #264.

quietone’s picture

@alexpott, thank you. I agree that if this is reflecting user input then the extra work needs to be done. What I guess I didn't make clear is that I think adding the user input to the message which then requires doing due diligence and adding validation is out of scope. It should be done in a followup. However, we have a patch that adds that and, if consensus is to continue with it, I am fine with that.

@dww, thanks. It is too late for me work on this.

larowlan’s picture

Ah, yep didn't consider social engineering now we output the input values, good call

dww’s picture

Issue summary: View changes
StatusFileSize
new17.24 KB
new16.87 KB
new2.42 KB
new6.04 KB
new25.15 KB
new18.25 KB

Of course, @alexpott is correct on what's valid. ;)

UserInterface::USERNAME_MAX_LENGTH = 60;
EMAIL_MAX_LENGTH = 254

1521996-268.test-only.patch is the code from #261 with the updated test for the super long email address case (with a tiny change to use the old error message text so we don't get a false negative on the invalid username assertions), plus drupalci.yml to only run this test.

1521996-268.patch is the full deal, should be green.

Interdiffs from #261 to both attached.

https://www.drupal.org/node/3189310 is the CR, so the @todo about that is gone.

I hope we don't also need deprecation tests for this.

New screenshots in the summary. Updated API changes. I didn't completely edit the remaining tasks about agreeing on the wording, since the current is basically my proposal and I'd rather someone else confirm we're agreed. ;)

RTBC?

Thanks!
-Derek

dww’s picture

Assigned: dww » Unassigned
Issue summary: View changes
Status: Needs work » Needs review

Sorry, d.o got confused by the x-post w/ @quietone + @larowlan :/

dww’s picture

StatusFileSize
new17.24 KB
new16.87 KB
new473 bytes
+++ b/core/drupalci.yml
@@ -15,32 +15,9 @@ build:
+        testgroups: '--class "Drupal\Tests\user\Functional\UserPasswordResetTest'

Argh, I missed the closing ". ;)

Fixed that, and re-uploading #268 so the RTBC bot isn't confused.

longwave’s picture

+++ b/core/modules/user/src/Form/UserPasswordForm.php
@@ -133,7 +166,23 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
+    // If we passed validation, trim() the name before trying to use it.
+    $name = trim($name);

I don't get why we trim after validating. If we are going to trim, isn't it friendlier to the user to do so before validating? e.g. if they are copying and pasting from somewhere and accidentally add a leading or trailing space, we remove it for them, as we can accept it anyway.

alexpott’s picture

#274 +1 we should definitely trim before validating.

spokje’s picture

StatusFileSize
new938 bytes
new16.63 KB

Trim-before-validation-patch attached addressing #274 and #275.

dww’s picture

Status: Needs work » Needs review
dww’s picture

StatusFileSize
new16.8 KB
new1.72 KB

(sorry, comment #fail)

Yeah, if you trim() first, the test we added for an invalid username won't trigger. We need to violate the length or something else.

spokje’s picture

Thanks @dww for the new patch.

I was looking at forcing an invalid username by applying two spaces in the middle:

$edit = ['name' => 'multiple  spaces'];

but weirdly/interestingly that returns the validation-error message:

'multiple spaces' is not a valid username or email address. , so without the multiple spaces in the username.

Is this something for a follow-up or even this issue to fix, since we're not showing the exact user-input in the validation error message?

Because then at least the user has the chance to think "oh drats" I mistyped that.

alexpott in #250

mjpa’s picture

Re #280: Is that not just browsers collapsing multiple spaces to 1 single one on display rather than Drupal doing something to them?

dww’s picture

Indeed, #281 is the answer to #280. It's how HTML works with whitespace. We'd have to do something unholy like use <pre> inside there. Kinda whacky. I'm not sure any of it is worth any more effort.

So, any final concerns with #279? RTBC?

Thanks again,
-Derek

alexpott’s picture

Status: Needs review » Needs work

I think it is absolutely fine that the validation error does not have the input

+++ b/core/modules/user/src/Form/UserPasswordForm.php
@@ -133,7 +166,20 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
+      $form_state->setErrorByName('name', $this->t("'%identifier' is not a valid username or email address.", ['%identifier' => $name]));

As we're erroring we don't need the identifier in the input in the error message. And this would allow for exactly the content spoofing we're adding the validation to prevent :) so let's not addd the identifier here. It's important to add the identifier to the success message because then we're telling the user everything might be successful.

quietone’s picture

Oh, this is closer, Thanks everyone!

#283 I'll make a new patch but what is the text is to be used?

alexpott’s picture

@quietone how about The username or email address is invalid.

spokje’s picture

Status: Needs work » Needs review
StatusFileSize
new1.53 KB
new16.66 KB

Attached patch addresses #285

@quietone: I hope you'll forgive me, but I figured it's getting a tad late in New Zealand for patch-creating. Just trying to move this one along to RTBC (as we all are :) )

quietone’s picture

@Spokje, no need for forgiveness, it is very considerate of you to be aware of my timezone. And it is more important to continue the progress here. We are sooo close! :-)

spokje’s picture

@quietone:

it is very considerate of you to be aware of my timezone.

Just prepping for an upcoming DrupalCon Trivia night. At some point they're bound to ask d.o. UIDs and timezones... 😇

longwave’s picture

Status: Needs review » Reviewed & tested by the community

All concerns seem to have been addressed, this looks ready to go.

alexpott’s picture

Issue summary: View changes
Status: Reviewed & tested by the community » Needs work
Issue tags: +String change in 9.2.0, +9.2.0 release highlights

As we're changing strings here we can only do this in 9.2.0. Also as this is a small but impacts a lot of user's experiences I think we should have a change record that details the changes in UX made by the issue. Also I think this should be featured in the 9.2.0 as a privacy improvement. I've seen custom and contrib code to address this issue. Therefore we need a release note snippet. Once we have the new CR and the release note snippet then we're go to go - i think.

quietone’s picture

Update IS and added a release note snippet.

That leaves the change record. Does the previous comment mean add a second change record or expand on the existing one?

quietone’s picture

Issue summary: View changes
StatusFileSize
new4.02 KB
new5.88 KB

Better screenshots

quietone’s picture

Second CR added. It needs formatting help.

spokje’s picture

@quietone: I like the new CR. The only thing I would like to add is the before/after screenshots (as we have in this topic), but I'm not sure if that's even allowed in a UX CR?

alexpott’s picture

I've re-formatted the new CR a bit so it's obvious what is CR text and what is a message produced by Drupal. I don't think we need to add images to the CR. The way the message looks is very dependent on your frontend theme.

spokje’s picture

Status: Needs work » Needs review

@alexpott:

I don't think we need to add images to the CR. The way the message looks is very dependent on your frontend theme.

Makes full sense to me.

Since I think we now addressed all the remarks in #290 are we now, indeed, good to go and can this issue be RTBC again?

Once we have the new CR and the release note snippet then we're go to go - i think.

quietone’s picture

@Spokje, Yes I think this can go back to RTBC, but also don't think I should do it either.

sokru’s picture

StatusFileSize
new16.39 KB
new1.41 KB

Patch #286 failed to apply, so here's a reroll. Failed to create real interdiff, so attached is done with diff. Tested with reroll and will set to RTBC if tests pass.

sokru’s picture

Status: Needs review » Reviewed & tested by the community

Works fine and looks good to me.

dww’s picture

Glad to see this RTBC, thanks! Reroll in #298 looks good when comparing locally. +1 to the RTBC.

Added #3192684: Comment name validation reveals whether an email or username is in use as a related issue. I searched, but didn't find any existing issues about it. Would love feedback on how to best solve that one. ;)

Thanks again,
-Derek

alexpott’s picture

Issue summary: View changes
Status: Reviewed & tested by the community » Fixed

Committed 5f04357 and pushed to 9.2.x. Thanks!

If someone want to backport this to Drupal 7 then a new issue will need to be opened.

  • alexpott committed 5f04357 on 9.2.x
    Issue #1521996 by quietone, dww, Sophie.SK, Carsten Müller, Spokje, kim....
c-logemann’s picture

Thanks everybody for contributing to this long term issue.

no2e’s picture

Awesome, thanks everyone!

In case someone is interested, we have the same issue with the registration form:
#2346389: Prevent registered email address enumeration via user registration form

Status: Fixed » Closed (fixed)

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

izmeez’s picture

Now this is fixed in D9 is it time for the backport to Drupal 7, continuing with re-roll in comment #108 #1521996-108: Password reset form reveals whether an email or username is in use or do we need a new issue?

solideogloria’s picture

Issue summary: View changes
dww’s picture

@izmeez: Yes, a separate issue for the D7 backport is a good idea. Once an issue is "Closed (fixed)" a) it can't be reopened by regular users and b) the visibility on further comments is almost nil. So yes, if you care about trying to fix this for D7, a new child issue would be needed.

Thanks,
-Derek

izmeez’s picture

@dww Thanks for direction. Backport issue created with re-rolled patch #3200198: [D7] password reset form prevent revealing email or username in use.

ankitv18’s picture

StatusFileSize
new21.85 KB

Re-roll the patch for 8.9.x

wagu’s picture

Issue summary: View changes
StatusFileSize
new16.69 KB

Re-roll the patch for 9.1.x

joao sausen’s picture

This same issue happens at Drupal\user\Controller\UserAuthenticationController::resetPassword()

    throw new BadRequestHttpException('The user has not been activated or is blocked.');
    ...
    throw new BadRequestHttpException('Unrecognized username or email address.');
avpaderno’s picture

(I reverted the change to the IS done from wguesmi09, who removed all the text. I apologize for bumping this issue.)