diff --git a/modules/user/user.pages.inc b/modules/user/user.pages.inc
index 2a1b291b13..1544c64319 100644
--- a/modules/user/user.pages.inc
+++ b/modules/user/user.pages.inc
@@ -66,6 +66,18 @@ function user_pass() {
  * @see user_pass_submit()
  */
 function user_pass_validate($form, &$form_state) {
+  $user_pass_reset_ip_window = variable_get('user_pass_reset_ip_window', 3600);
+  // Do not allow any password reset from the current user's IP if the limit
+  // has been reached. Default is 50 attempts allowed in one hour. This is
+  // independent of the per-user limit to catch attempts from one IP to request
+  // resets for many different user accounts. We have a reasonably high limit
+  // since there may be only one apparent IP for all users at an institution.
+  if (!flood_is_allowed('pass_reset_ip', variable_get('user_pass_reset_ip_limit', 50), $user_pass_reset_ip_window)) {
+    form_set_error('name', t('Sorry, too many password reset attempts from your IP address. This IP address is temporarily blocked. Try again later or <a href="@url">request a new password</a>.', array('@url' => url('user/password'))));
+    return;
+  }
+  // Always register an per-IP event.
+  flood_register_event('pass_reset_ip', $user_pass_reset_ip_window);
   $name = trim($form_state['values']['name']);
   // Try to load by email.
   $users = user_load_multiple(array(), array('mail' => $name, 'status' => '1'));
@@ -76,6 +88,27 @@ function user_pass_validate($form, &$form_state) {
     $account = reset($users);
   }
   if (isset($account->uid)) {
+    if (variable_get('user_pass_reset_identifier_uid_only', FALSE)) {
+      // Register flood events based on the uid only, so they apply for any
+      // IP address. This is the most secure option.
+      $identifier = $account->uid;
+    }
+    else {
+      // The default identifier is a combination of uid and IP address. This
+      // is less secure but more resistant to denial-of-service attacks that
+      // could lock out all users with public user names.
+      $identifier = $account->uid . '-' . ip_address();
+    }
+    $user_pass_reset_user_window = variable_get('user_pass_reset_user_window', 21600);
+    $user_pass_reset_user_limit = variable_get('user_pass_reset_user_limit', 5);
+    // Don't allow password reset if the limit for this user has been reached.
+    // Default is to allow 5 passwords resets every 6 hours.
+    if (!flood_is_allowed('pass_reset_user', $user_pass_reset_user_limit, $user_pass_reset_user_window, $identifier)) {
+      form_set_error('name', format_plural($user_pass_reset_user_limit, 'Sorry, there has been more than one password reset attempt for this account. It is temporarily blocked. Try again later or <a href="@url">login with your password</a>.', 'Sorry, there have been more than @count password reset attempts for this account. It is temporarily blocked. Try again later or <a href="@url">login with your password</a>.', array('@url' => url('user/login'))));
+      return;
+    }
+    // Register a per-user event.
+    flood_register_event('pass_reset_user', $user_pass_reset_user_window, $identifier);
     form_set_value(array('#parents' => array('account')), $account, $form_state);
   }
   else {
diff --git a/modules/user/user.test b/modules/user/user.test
index fb82c93c22..ea2c56af0f 100644
--- a/modules/user/user.test
+++ b/modules/user/user.test
@@ -492,6 +492,8 @@ class UserPasswordResetTestCase extends DrupalWebTestCase {
     $this->drupalPost('user/password', $edit, t('E-mail new password'));
     // Confirm the password reset.
     $this->assertText(t('Further instructions have been sent to your e-mail address.'), 'Password reset instructions mailed message displayed.');
+    // Ensure that flood control was not triggered.
+    $this->assertNoText(t('is temporarily blocked. Try again later'), 'Flood control was not triggered by single password reset.');
 
     // Create an image field to enable an Ajax request on the user profile page.
     $field = array(
@@ -537,6 +539,67 @@ class UserPasswordResetTestCase extends DrupalWebTestCase {
     $this->assertText(t('The changes have been saved.'), 'Forgotten password changed.');
   }
 
+  /**
+   * Test user-based flood control on password reset.
+   */
+  function testPasswordResetFloodControlPerUser() {
+    // Set a very low limit for testing.
+    variable_set('user_pass_reset_user_limit', 2);
+
+    // Create a user.
+    $account = $this->drupalCreateUser();
+    $this->drupalLogin($account);
+    $this->drupalLogout();
+
+    $edit = array('name' => $account->name);
+
+    // Try 2 requests that should not trigger flood control.
+    for ($i = 0; $i < 2; $i++) {
+      $this->drupalPost('user/password', $edit, t('E-mail new password'));
+      // Confirm the password reset.
+      $this->assertText(t('Further instructions have been sent to your e-mail address.'), 'Password reset instructions mailed message displayed.');
+      // Ensure that flood control was not triggered.
+      $this->assertNoText(t('is temporarily blocked. Try again later'), 'Flood control was not triggered by password reset.');
+    }
+
+    // The next request should trigger flood control
+    $this->drupalPost('user/password', $edit, t('E-mail new password'));
+    // Confirm the password reset was blocked.
+    $this->assertNoText(t('Further instructions have been sent to your e-mail address.'), 'Password reset instructions mailed message not displayed for excessive password resets.');
+    // Ensure that flood control was triggered.
+    $this->assertText(t('Sorry, there have been more than 2 password reset attempts for this account. It is temporarily blocked.'), 'Flood control was triggered by excessive password resets for one user.');
+  }
+
+  /**
+   * Test IP-based flood control on password reset.
+   */
+  function testPasswordResetFloodControlPerIp() {
+    // Set a very low limit for testing.
+    variable_set('user_pass_reset_ip_limit', 2);
+
+    // Try 2 requests that should not trigger flood control.
+    for ($i = 0; $i < 2; $i++) {
+      $name = $this->randomName();
+      $edit = array('name' => $name);
+      $this->drupalPost('user/password', $edit, t('E-mail new password'));
+      // Confirm the password reset was not blocked. Note that @name is used
+      // instead of %name as assertText() works with plain text not HTML.
+      $this->assertText(t('Sorry, @name is not recognized as a user name or an e-mail address.', array('@name' => $name)), 'User name not recognized message displayed.');
+      // Ensure that flood control was not triggered.
+      $this->assertNoText(t('is temporarily blocked. Try again later'), 'Flood control was not triggered by password reset.');
+    }
+
+    // The next request should trigger flood control
+    $name = $this->randomName();
+    $edit = array('name' => $name);
+    $this->drupalPost('user/password', $edit, t('E-mail new password'));
+    // Confirm the password reset was blocked early. Note that @name is used
+    // instead of %name as assertText() works with plain text not HTML.
+    $this->assertNoText(t('Sorry, @name is not recognized as a user name or an e-mail address.', array('@name' => $name)), 'User name not recognized message not displayed.' . ' DEBUG: i = ' . $i);
+    // Ensure that flood control was triggered.
+    $this->assertText(t('Sorry, too many password reset attempts from your IP address. This IP address is temporarily blocked.'), 'Flood control was triggered by excessive password resets from one IP.');
+  }
+
   /**
    * Test user password reset while logged in.
    */
