Commit 51d6e296 authored by mcdruid's avatar mcdruid

Issue #2989985 by mcdruid, colorfulCoder, tatarbj, Fabianx, paulocs: User...

Issue #2989985 by mcdruid, colorfulCoder, tatarbj, Fabianx, paulocs: User module's flood controls should do better logging, plus add new hook_user_flood_control()
parent f27beadb
name = "User module flood control tests"
description = "Support module for user flood control testing."
package = Testing
version = VERSION
core = 7.x
hidden = TRUE
<?php
/**
* @file
* Dummy module implementing hook_user_flood_control.
*/
/**
* Implements hook_user_flood_control().
*/
function user_flood_test_user_flood_control($ip, $username = FALSE) {
if (!empty($username)) {
watchdog('user_flood_test', 'hook_user_flood_control was passed username %username and IP %ip.', array('%username' => $username, '%ip' => $ip));
}
else {
watchdog('user_flood_test', 'hook_user_flood_control was passed IP %ip.', array('%ip' => $ip));
}
}
......@@ -35,7 +35,7 @@ function user_form_test_current_password($form, &$form_state, $account) {
'#description' => t('A field that would require a correct password to change.'),
'#required' => TRUE,
);
$form['current_pass'] = array(
'#type' => 'password',
'#title' => t('Current password'),
......
......@@ -472,6 +472,36 @@ function hook_user_role_delete($role) {
->execute();
}
/**
* Respond to user flood control events.
*
* This hook allows you act when an unsuccessful user login has triggered
* flood control. This means that either an IP address or a specific user
* account has been temporarily blocked from logging in.
*
* @param $ip
* The IP address that triggered flood control.
* @param $username
* The username that has been temporarily blocked.
*
* @see user_login_final_validate()
*/
function hook_user_flood_control($ip, $username = FALSE) {
if (!empty($username)) {
// Do something with the blocked $username and $ip. For example, send an
// e-mail to the user and/or site administrator.
// Drupal core uses this hook to log the event:
watchdog('user', 'Flood control blocked login attempt for %user from %ip.', array('%user' => $username, '%ip' => $ip));
}
else {
// Do something with the blocked $ip. For example, add it to a block-list.
// Drupal core uses this hook to log the event:
watchdog('user', 'Flood control blocked login attempt from %ip.', array('%ip' => $ip));
}
}
/**
* @} End of "addtogroup hooks".
*/
......@@ -2225,11 +2225,17 @@ function user_login_final_validate($form, &$form_state) {
if (isset($form_state['flood_control_triggered'])) {
if ($form_state['flood_control_triggered'] == 'user') {
form_set_error('name', format_plural(variable_get('user_failed_login_user_limit', 5), 'Sorry, there has been more than one failed login attempt for this account. It is temporarily blocked. Try again later or <a href="@url">request a new password</a>.', 'Sorry, there have been more than @count failed login attempts for this account. It is temporarily blocked. Try again later or <a href="@url">request a new password</a>.', array('@url' => url('user/password'))));
module_invoke_all('user_flood_control', ip_address(), $form_state['values']['name']);
}
else {
// We did not find a uid, so the limit is IP-based.
form_set_error('name', t('Sorry, too many failed login 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'))));
module_invoke_all('user_flood_control', ip_address());
}
// We cannot call drupal_access_denied() here as that can result in an
// infinite loop if the login form is rendered on the 403 page (e.g. in a
// block). So add the 403 header and allow form processing to finish.
drupal_add_http_header('Status', '403 Forbidden');
}
else {
// Use $form_state['input']['name'] here to guarantee that we send
......@@ -2247,6 +2253,23 @@ function user_login_final_validate($form, &$form_state) {
}
}
/**
* Implements hook_user_flood_control().
*/
function user_user_flood_control($ip, $username = FALSE) {
if (variable_get('log_user_flood_control', TRUE)) {
if (!empty($username)) {
watchdog('user', 'Flood control blocked login attempt for %user from %ip.', array(
'%user' => $username,
'%ip' => $ip
));
}
else {
watchdog('user', 'Flood control blocked login attempt from %ip.', array('%ip' => $ip));
}
}
}
/**
* Try to validate the user's login credentials locally.
*
......
......@@ -322,7 +322,7 @@ class UserLoginTestCase extends DrupalWebTestCase {
}
function setUp() {
parent::setUp('user_session_test');
parent::setUp('user_session_test', 'user_flood_test');
}
/**
......@@ -453,12 +453,19 @@ class UserLoginTestCase extends DrupalWebTestCase {
$this->drupalPost('user', $edit, t('Log in'));
$this->assertNoFieldByXPath("//input[@name='pass' and @value!='']", NULL, 'Password value attribute is blank.');
if (isset($flood_trigger)) {
$this->assertResponse(403);
$user_log = db_query_range('SELECT message FROM {watchdog} WHERE type = :type ORDER BY wid DESC', 0, 1, array(':type' => 'user'))->fetchField();
$user_flood_test_log = db_query_range('SELECT message FROM {watchdog} WHERE type = :type ORDER BY wid DESC', 0, 1, array(':type' => 'user_flood_test'))->fetchField();
if ($flood_trigger == 'user') {
$this->assertRaw(format_plural(variable_get('user_failed_login_user_limit', 5), 'Sorry, there has been more than one failed login attempt for this account. It is temporarily blocked. Try again later or <a href="@url">request a new password</a>.', 'Sorry, there have been more than @count failed login attempts for this account. It is temporarily blocked. Try again later or <a href="@url">request a new password</a>.', array('@url' => url('user/password'))));
$this->assertRaw(t('Sorry, there have been more than @count failed login attempts for this account. It is temporarily blocked. Try again later or <a href="@url">request a new password</a>.', array('@url' => url('user/password'), '@count' => variable_get('user_failed_login_user_limit', 5))));
$this->assertEqual('Flood control blocked login attempt for %user from %ip.', $user_log, 'A watchdog message was logged for the login attempt blocked by flood control per user');
$this->assertEqual('hook_user_flood_control was passed username %username and IP %ip.', $user_flood_test_log, 'hook_user_flood_control was invoked by flood control per user');
}
else {
// No uid, so the limit is IP-based.
$this->assertRaw(t('Sorry, too many failed login 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'))));
$this->assertEqual('Flood control blocked login attempt from %ip.', $user_log, 'A watchdog message was logged for the login attempt blocked by flood control per IP');
$this->assertEqual('hook_user_flood_control was passed IP %ip.', $user_flood_test_log, 'hook_user_flood_control was invoked by flood control per IP');
}
}
else {
......
......@@ -659,3 +659,17 @@
'node_modules',
'bower_components',
);
/**
* Logging of user flood control events.
*
* Drupal's user module will place a temporary block on a given IP address or
* user account if there are excessive failed login attempts. By default these
* flood control events will be logged. This can be useful for identifying
* brute force login attacks. Set this variable to FALSE to disable logging, for
* example if you are using the dblog module and want to avoid database writes.
*
* @see user_login_final_validate()
* @see user_user_flood_control()
*/
# $conf['log_user_flood_control'] = FALSE;
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment