This is a D7 back-port of #1681832: Password reset form has no flood protection. The last D7 patch there is #7.

Problem/Motivation

Currently, User X can request a new password an infinite amount of times. This can be confirmed by the logs at admin/reports/dblog.

Proposed resolution

Create flood event and enforce it.
Create/modify tests in order so that this patch can pass. Since the flood protection takes action after a while the user will not be able to login after too many tries, and this the way it should work. The tests should let this pass.

Remaining tasks

Review, ideally write tests.

User interface changes

New error messages.

API changes

None.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

drumm created an issue. See original summary.

drumm credited coltrane.

drumm credited k_zoltan.

drumm credited mihai_brb.

drumm’s picture

Status: Active » Needs review
FileSize
3.15 KB

Compared to #1681832-7: Password reset form has no flood protection, this is a complete rewrite. This is a copy of the current logic from user_login_authenticate_validate() and user_login_final_validate() adapted for user_pass_validate() (not user_pass_submit()).

mcdruid’s picture

This looks like a good start. Couple of todo's though:

1) Should we clear the flood event if the password reset's successful in the same way that user_login_final_validate() does? Looks like that might need to happen in user_pass_reset()?

2) Tests :)

mcdruid’s picture

It doesn't look like the D8 patch over in #1681832: Password reset form has no flood protection clears the flood event when a reset is successful either.

It looks like it could be tricky to do so as we need to know the $identifier that was used to register the event.

In the case of the login form, that's carried along by the form API, e.g.:

  elseif (isset($form_state['flood_control_user_identifier'])) {
    // Clear past failures for this user so as not to block a user who might
    // log in and out more than once in an hour.
    flood_clear_event('failed_login_attempt_user', $form_state['flood_control_user_identifier']);
  }

IIUC password resets rely entirely on the URL to carry state, so I don't see that the identifier used in user_pass_validate() is available in user_pass_reset() (unless we added it to the URL, or stored in the db).

The IP may well be different, so I don't think deriving it from the ip_address() of the current request would work.

So perhaps not very straightforwards.

Unless perhaps we say that the user-based flood event should only use the uid as the identifier (i.e. not have user_pass_reset_identifier_uid_only as an option, but instead make that mandatory), then we would know the identifier on the successful password reset.

Doesn't look like the IP-only identifier has flood events cleared on login either, so perhaps just using the uid would be one way to go here.

drumm’s picture

Do they need to be cleared?

For login, flood protection can completely lock you out. Presumably for a password reset, you reset your password, and don’t need another password reset link in the near future.

mcdruid’s picture

Yup, it's probably an edge case but I suppose someone might do multiple password resets if e.g. they're struggling with their password manager or something similar.

I don't think it's essential that the flood events are cleared.

If we were going to add clearing, we'd need to do so in the D8 patch first.

mcdruid’s picture

Added some basic tests for password reset flood control, similar to those in the D8 parent issue.

This revealed that flood control was being triggered before the limit was exceeded e.g. if user_pass_reset_ip_limit is set to 2, the second password reset request is blocked by flood control. So an off-by-one error pretty much.

This was because we were registering the flood events before calling flood_is_allowed(), so this patch changes that order around (see the interdiff).

This wasn't obvious because in user.module user_login_authenticate_validate() calls flood_is_allowed() before user_login_final_validate() calls flood_register_event() whereas we're doing both from the same function here.

I think this works as expected now that the check and the registration of the event have been swapped around.

mcdruid’s picture

Oh, just noticed the comments need a tweak as they still mention "to catch attempts from one IP to log in" etc.. which should be changed to refer to password resets.

I'll let the tests run first.

mcdruid’s picture

...and here's the tweak to the comment.

drumm’s picture

Status: Needs review » Reviewed & tested by the community

Looks good to me, and this is now running on www.drupal.org.

mcdruid’s picture

The parent issue was fixed / committed to D8.

We did implement clearing per-user flood events when a password's reset successfully, and that meant using only the uid as an identifier.

So here's an update to the D7 patch which does the same.

The test-only patch doesn't clear the flood events, so it should fail.

The last submitted patch, 15: 3074666-15_test_only.patch, failed testing. View results

drumm’s picture

Assigned: drumm » Unassigned

I'm not actively working on this, un-assigning.

In general, looks good!

mcdruid’s picture

Issue tags: +Already In D8, +Drupal 7 bugfix target

The D8 version of this was committed; I'd like to get this Security Improvement into the next D7 release if possible.

mcdruid’s picture

Status: Needs review » Reviewed & tested by the community

I think this one is ready (and per @drumm in #14 has been in place on d.o for over a year now).

Fabianx’s picture

RTBM, +1 from me for merge

mcdruid’s picture

  • mcdruid committed 88bc1a5 on 7.x
    Issue #3074666 by mcdruid, drumm, coltrane, Damien Tournoud, k_zoltan,...
mcdruid’s picture

Status: Reviewed & tested by the community » Fixed
Issue tags: -Drupal 7 bugfix target

Thank you!

Status: Fixed » Closed (fixed)

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