diff --git includes/common.inc includes/common.inc
index 77182ee..6181837 100644
--- includes/common.inc
+++ includes/common.inc
@@ -1279,6 +1279,19 @@ function flood_register_event($name) {
}
/**
+ * Make the flood control mechanism forget about an event for the current visitor (hostname/IP).
+ *
+ * @param $name
+ * The name of an event.
+ */
+function flood_clear_event($name) {
+ db_delete('flood')
+ ->condition('event', $name)
+ ->condition('hostname', ip_address())
+ ->execute();
+}
+
+/**
* Check if the current visitor (hostname/IP) is allowed to proceed with the specified event.
*
* The user is allowed to proceed if he did not trigger the specified event more
diff --git modules/user/user.module modules/user/user.module
index f55c140..07eb63d 100644
--- modules/user/user.module
+++ modules/user/user.module
@@ -1635,8 +1635,26 @@ function user_login_authenticate_validate($form, &$form_state) {
*/
function user_login_final_validate($form, &$form_state) {
if (empty($form_state['uid'])) {
- form_set_error('name', t('Sorry, unrecognized username or password. Have you forgotten your password?', array('@password' => url('user/password'))));
- watchdog('user', 'Login attempt failed for %user.', array('%user' => $form_state['values']['name']));
+ // Register a global failed login event.
+ flood_register_event('failed_login_attempt');
+
+ if (!empty($form_state['flood_control_uid'])) {
+ // Register a per-user failed login event.
+ flood_register_event('failed_login_attempt_' . $form_state['flood_control_uid']);
+ }
+
+ if (!empty($form_state['flood_control'])) {
+ form_set_error('name', t('Sorry, too many failed password attempts, you are temporarily prohibited from logging in. Please try again later.'));
+ }
+ else {
+ form_set_error('name', t('Sorry, unrecognized username or password. Have you forgotten your password?', array('@password' => url('user/password'))));
+ watchdog('user', 'Login attempt failed for %user.', array('%user' => $form_state['values']['name']));
+ }
+ }
+ else {
+ // Clear past failures for this IP address 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_' . $form_state['uid']);
}
}
@@ -1651,8 +1669,23 @@ function user_authenticate(&$form_state) {
$password = trim($form_state['values']['pass']);
// Name and pass keys are required.
if (!empty($form_state['values']['name']) && !empty($password)) {
+ // Don't allow any login from that IP if the hourly limit has been reached.
+ if (!flood_is_allowed('failed_login_attempt', variable_get('user_failed_login_hourly_limit', 50))) {
+ $form_state['flood_control'] = TRUE;
+ return;
+ }
+
$account = db_query("SELECT * FROM {users} WHERE name = :name AND status = 1", array(':name' => $form_state['values']['name']))->fetchObject();
if ($account) {
+ $form_state['flood_control_uid'] = $account->uid;
+
+ // Don't allow any login from that IP if the hourly limit for this user
+ // has been reached.
+ if (!flood_is_allowed('failed_login_attempt_' . $account->uid, variable_get('user_failed_login_per_user_hourly_limit', 3))) {
+ $form_state['flood_control'] = TRUE;
+ return;
+ }
+
// Allow alternate password hashing schemes.
require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc');
if (user_check_password($password, $account)) {
diff --git modules/user/user.test modules/user/user.test
index 9c37d9a..4400c92 100644
--- modules/user/user.test
+++ modules/user/user.test
@@ -159,6 +159,107 @@ class UserValidationTestCase extends DrupalWebTestCase {
}
}
+class UserLoginTestCase extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'User login',
+ 'description' => 'Ensure that login works as expected.',
+ 'group' => 'User',
+ );
+ }
+
+ /**
+ * Test the global login flood control.
+ */
+ function testGlobalLoginFloodControl() {
+ // Set the global login limit.
+ variable_set('user_failed_login_hourly_limit', 10);
+ // Get the per-user limit out of the picture.
+ variable_set('user_failed_login_per_user_hourly_limit', 4000);
+
+ $user1 = $this->drupalCreateUser(array());
+ $incorrect_user1 = clone $user1;
+ $incorrect_user1->pass_raw = $this->randomName();
+
+ // Try 2 failed logins.
+ for ($i = 0; $i < 2; $i++) {
+ $this->assertFailedLogin($incorrect_user1, FALSE);
+ }
+
+ // Try one successful login, it will not reset overall flood control mechanism.
+ $this->drupalLogin($user1);
+ $this->drupalLogout();
+
+ // Try 8 more failed logins, they should not trigger the flood control
+ // mechanism.
+ for ($i = 0; $i < 8; $i++) {
+ $this->assertFailedLogin($incorrect_user1, FALSE);
+ }
+
+ // The next login trial should result in a flood error message.
+ $this->assertFailedLogin($incorrect_user1, TRUE);
+
+ // A login with the correct password should also result in a flood error
+ // message.
+ $this->assertFailedLogin($user1, TRUE);
+ }
+
+ /**
+ * Test the per-user login flood control.
+ */
+ function testPerUserLoginFloodControl() {
+ // Get the global limit out of the picture.
+ variable_set('user_failed_login_hourly_limit', 4000);
+ // Set the per-user login limit.
+ variable_set('user_failed_login_per_user_hourly_limit', 3);
+
+ $user1 = $this->drupalCreateUser(array());
+ $incorrect_user1 = clone $user1;
+ $incorrect_user1->pass_raw = $this->randomName();
+
+ $user2 = $this->drupalCreateUser(array());
+ $incorrect_user2 = clone $user2;
+ $incorrect_user2->pass_raw = $this->randomName();
+
+ // Try 3 failed logins for user 1.
+ for ($i = 0; $i < 3; $i++) {
+ $this->assertFailedLogin($incorrect_user1, FALSE);
+ }
+
+ // Try one successful attempt for user 2, it should not trigger any
+ // flood control.
+ $this->drupalLogin($user2);
+ $this->drupalLogout();
+
+ // Try one more attempt for user 1, it should be rejected, even if the
+ // correct password has been used.
+ $this->assertFailedLogin($user1, TRUE);
+ }
+
+ /**
+ * Make an unsuccessful login attempt.
+ *
+ * @param $user
+ * The user.
+ * @param $flood
+ * Whether to expect or not that the flood control mechanism will be
+ * triggered.
+ */
+ function assertFailedLogin($user, $flood = FALSE) {
+ $edit = array(
+ 'name' => $user->name,
+ 'pass' => $user->pass_raw,
+ );
+ $this->drupalPost('user', $edit, t('Log in'));
+ if ($flood) {
+ $this->assertText(t('Sorry, too many failed password attempts, you are temporarily prohibited from logging in. Please try again later.'));
+ }
+ else {
+ $this->assertText(t('Sorry, unrecognized username or password. Have you forgotten your password?'));
+ }
+ }
+}
+
class UserCancelTestCase extends DrupalWebTestCase {
public static function getInfo() {
return array(