login_security.module in Login Security 7
Same filename and directory in other branches
Login Security
Copyrighted by ilo@reversing.org Thanks to christefano for the module tips and strings
File
login_security.moduleView source
<?php
/**
* @file
* Login Security
*
* Copyrighted by ilo@reversing.org
* Thanks to christefano for the module tips and strings
*/
define('LOGIN_SECURITY_TRACK_TIME', 1);
define('LOGIN_SECURITY_USER_WRONG_COUNT', 0);
define('LOGIN_SECURITY_HOST_WRONG_COUNT', 0);
define('LOGIN_SECURITY_HOST_WRONG_COUNT_HARD', 0);
define('LOGIN_SECURITY_DISABLE_CORE_LOGIN_ERROR', 0);
define('LOGIN_SECURITY_NOTICE_ATTEMPTS_AVAILABLE', 0);
define('LOGIN_SECURITY_ACTIVITY_THRESHOLD', 0);
define('LOGIN_SECURITY_NOTICE_ATTEMPTS_MESSAGE', t("You have used @user_current_count out of @user_block_attempts login attempts. After all @user_block_attempts have been used, you will be unable to login."));
define('LOGIN_SECURITY_HOST_SOFT_BANNED', t("This host is not allowed to log in to @site. Please contact your site administrator."));
define('LOGIN_SECURITY_HOST_HARD_BANNED', t("The IP address @ip is banned at @site, and will not be able to access any of its content from now on. Please contact the site administrator."));
define('LOGIN_SECURITY_USER_BLOCKED', t("The user @username has been blocked due to failed login attempts."));
define('LOGIN_SECURITY_USER_BLOCKED_EMAIL_USER', '');
define('LOGIN_SECURITY_USER_BLOCKED_EMAIL_SUBJECT', t("Security action: The user @username has been blocked."));
define('LOGIN_SECURITY_USER_BLOCKED_EMAIL_BODY', t("The user @username (@edit_uri) has been blocked at @site due to the amount of failed login attempts. Please check the logs for more information."));
define('LOGIN_SECURITY_LOGIN_ACTIVITY_EMAIL_USER', '');
define('LOGIN_SECURITY_LOGIN_ACTIVITY_EMAIL_SUBJECT', t("Security information: Unexpected login activity has been detected at @site."));
define('LOGIN_SECURITY_LOGIN_ACTIVITY_EMAIL_BODY', t("The configured threshold of @activity_threshold logins has been reached with a total of @tracking_current_count invalid login attempts. You should review your log information about login attempts at @site."));
define('LOGIN_SECURITY_THRESHOLD_NOTIFIED', FALSE);
/**
* Implements hook_cron().
*/
function login_security_cron() {
// Remove expired events.
_login_security_remove_events();
}
/**
* Implements hook_menu().
*/
function login_security_menu() {
$items = array();
// Administer >> Site configuration >> Login Security settings.
$items['admin/config/people/login_security'] = array(
'title' => 'Login Security',
'description' => 'Configure security settings in the login form submission.',
'access arguments' => array(
'administer site configuration',
),
'page callback' => 'drupal_get_form',
'page arguments' => array(
'login_security_admin_settings',
),
'file' => 'login_security.admin.inc',
);
return $items;
}
/**
* Implements hook_user_login().
*/
function login_security_user_login(&$edit, $account) {
_login_security_remove_events($account->name, ip_address());
}
/**
* Implements hook_user_update().
*/
function login_security_user_update(&$edit, &$account, $category = NULL) {
// The update case can be launched by the user or by any administrator.
// On update, remove only the unser information tracked.
if ($account->status != 0) {
// Don't remove tracking events if account is being blocked.
_login_security_remove_events($account->name);
}
}
/**
* Implements hook_form_alter().
*/
function login_security_form_alter(&$form, &$form_state, $form_id) {
switch ($form_id) {
case 'user_login':
case 'user_login_block':
// Put login_security first or the capture of the previous login
// timestamp won't work and core's validation will update to the current
// login instance before login_security can read the old timestamp.
$validate = array(
'login_security_soft_block_validate',
'login_security_set_login_timestamp',
);
if (isset($form['#validate']) && is_array($form['#validate'])) {
$form['#validate'] = array_merge($validate, $form['#validate']);
}
else {
$form['#validate'] = $validate;
}
$form['#validate'][] = 'login_security_validate';
$form['#submit'][] = 'login_security_submit';
break;
}
}
/**
* Save login attempt and save login/access timestamps.
*
* Previous incarnations of this code put it in hook_submit or hook_user, but
* since Drupal core validation updates the login timestamp, we have to set the
* message before it gets updated with the current login instance.
*/
function login_security_set_login_timestamp($form, &$form_state) {
$account = db_select('users', 'u')
->fields('u', array(
'login',
'access',
))
->condition('name', $form_state['values']['name'])
->condition('status', 1)
->execute()
->fetchAssoc();
// Save entry in security log, Username and IP Address.
_login_security_add_event($form_state['values']['name'], ip_address());
if (empty($account)) {
return;
}
_login_security_login_timestamp($account['login']);
_login_security_access_timestamp($account['access']);
}
/**
* Returns account login timestamp.
*/
function _login_security_login_timestamp($login = NULL) {
static $account_login;
if (!isset($account_login) && is_numeric($login) && $login > 0) {
$account_login = $login;
}
return $account_login;
}
/**
* Returns account access timestamp.
*/
function _login_security_access_timestamp($access = NULL) {
static $account_access;
if (!isset($account_access) && is_numeric($access) && $access > 0) {
$account_access = $access;
}
return $account_access;
}
/**
* Temporarily deny validation to users with excess invalid login attempts.
*
* @url http://drupal.org/node/493164
*/
function login_security_soft_block_validate($form, &$form_state) {
$variables = _login_security_get_variables_by_name($form_state['values']['name']);
// Check for host login attempts: Soft.
if ($variables['@soft_block_attempts'] >= 1) {
if ($variables['@ip_current_count'] >= $variables['@soft_block_attempts']) {
form_set_error('submit', login_security_t(variable_get('login_security_host_soft_banned', LOGIN_SECURITY_HOST_SOFT_BANNED), $variables));
}
}
}
/**
* Implements hook_validate().
*
* This functions does more than just validating, but it's main intention is to
* break the login form flow.
*/
function login_security_validate($form, &$form_state) {
// Sanitize user input.
$name = $form_state['values']['name'];
// Null username should not be tracked.
if (!strlen($name)) {
return;
}
// Expire old tracked entries.
_login_security_remove_events();
// Populate variables to be used in any module message or login operation.
$variables = _login_security_get_variables_by_name($name);
// First, check if administrator should be notified of unexpected login
// activity.
// Only process if configured threshold > 1.
// see: http://drupal.org/node/583092.
if ($variables['@activity_threshold']) {
// Check if threshold has been reached.
if ($variables['@tracking_current_count'] > $variables['@activity_threshold']) {
// Check if admin has been already alerted.
if (!variable_get('login_security_threshold_notified', LOGIN_SECURITY_THRESHOLD_NOTIFIED)) {
// Mark alert status as notified and send the email.
watchdog('login_security', 'Ongoing attack detected: Suspicious activity detected in login form submissions. Too many invalid login attempts threshold reached: currently @tracking_current_count events are tracked, and threshold is configured for @activity_threshold attempts.', $variables, WATCHDOG_WARNING);
variable_set('login_security_threshold_notified', TRUE);
// Submit email only if required.
$login_activity_email_user = variable_get('login_security_login_activity_email_user', LOGIN_SECURITY_LOGIN_ACTIVITY_EMAIL_USER);
if ($login_activity_email_user !== '') {
$from = variable_get('site_mail', ini_get('sendmail_from'));
$admin_mail = db_query_range("SELECT mail FROM {users} WHERE name = :name", 0, 1, array(
':name' => $login_activity_email_user,
))
->fetchField();
$mail = drupal_mail('login_security', 'login_activity_notify', $admin_mail, language_default(), $variables, $from, TRUE);
}
}
}
elseif (variable_get('login_security_threshold_notified', TRUE) && $variables['@tracking_current_count'] < $variables['@activity_threshold'] / 3) {
// Reset alert if currently tracked events is < threshold / 3.
watchdog('login_security', 'Suspicious activity in login form submissions is no longer detected: currently @tracking_current_count events are being tracked, and threshold is configured for @activity_threshold maximum allowed attempts).', $variables, WATCHDOG_NOTICE);
variable_set('login_security_threshold_notified', FALSE);
}
}
// Check for host login attempts: Hard.
if ($variables['@hard_block_attempts'] >= 1) {
if ($variables['@ip_current_count'] >= $variables['@hard_block_attempts']) {
// Block the host ip_address().
login_user_block_ip($variables);
}
}
// Check for user login attempts.
if ($variables['@user_block_attempts'] >= 1) {
if ($variables['@user_current_count'] >= $variables['@user_block_attempts']) {
// Block the account $name.
login_user_block_user_name($variables);
}
}
// At this point, they're either logged in or not by Drupal core's abuse of
// the validation hook to login users completely.
global $user;
// Login failed.
$messages = drupal_get_messages('error', FALSE);
if (!empty($messages['error'])) {
$password_message = preg_grep("/<a href=\"\\/user\\/password\\?name={$name}\">Have you forgotten your password\\?<\\/a>/", $messages['error']);
$block_message = preg_grep("/The username <em class=\"placeholder\">{$name}<\\/em> has not been activated or is blocked./", $messages['error']);
if (!count($password_message) || !count($block_message)) {
if (variable_get('login_security_disable_core_login_error', LOGIN_SECURITY_DISABLE_CORE_LOGIN_ERROR)) {
// Resets the form error status so no form fields are highlighted in
// red.
$form_state['rebuild'] = TRUE;
form_clear_error();
// Removes "Sorry, unrecognized username or password. Have you
// forgotten your password?" and "The username $name has not been
// activated or is blocked.", and any other errors that might be
// helpful to an attacker it should not reset the attempts message
// because it is a warning, not an error.
drupal_get_messages('error', TRUE);
}
// Should the user be advised about the remaining login attempts?
$notice_user = variable_get('login_security_notice_attempts_available', LOGIN_SECURITY_NOTICE_ATTEMPTS_AVAILABLE);
if ($notice_user == TRUE && $variables['@user_block_attempts'] > 0 && $variables['@user_block_attempts'] >= $variables['@user_current_count']) {
$message_raw = variable_get('login_security_notice_attempts_message', LOGIN_SECURITY_NOTICE_ATTEMPTS_MESSAGE);
// Simple flag that can be changed using hook_alter (see below).
$display_block_attempts = TRUE;
// Allow other module to change the flag, or even the message displayed,
// with a custom logic.
drupal_alter('login_security_display_block_attempts', $message_raw, $display_block_attempts, $variables['@user_current_count']);
$message = array(
'message' => $message_raw,
'variables' => $variables,
);
// This loop is used instead of doing t() because t() can only
// translate static strings, not variables.
// Ignoring Coder because $variables is sanitized by
// login_security_t().
// See https://drupal.org/node/1743996#comment-6421246.
// @ignore security_2
$message = login_security_t($message['message'], $message['variables']);
if ($display_block_attempts) {
drupal_set_message($message, 'warning', TRUE);
}
}
}
}
}
/**
* Implements hook_sumbit().
*/
function login_security_submit($form, &$form_state) {
// The submit handler shouldn't be called unless the authentication succeeded.
if (user_is_logged_in()) {
$login = _login_security_login_timestamp();
if (variable_get('login_security_last_login_timestamp', 0) && $login > 0) {
drupal_set_message(t('Your last login was @stamp.', array(
'@stamp' => format_date($login, 'large'),
)), 'status');
}
$access = _login_security_access_timestamp();
if (variable_get('login_security_last_access_timestamp', 0) && $access > 0) {
drupal_set_message(t('Your last page access (site activity) was @stamp.', array(
'@stamp' => format_date($access, 'large'),
)), 'status');
}
}
}
/**
* Remove tracked events or expire old ones.
*
* @param string $name
* If specified, events for this user name will be removed.
*
* @param string $host
* If specified, IP Address of the name-ip pair to be removed.
*/
function _login_security_remove_events($name = NULL, $host = NULL) {
// Remove selected events.
if (!empty($name)) {
if (!empty($host)) {
$result = db_delete('login_security_track')
->condition('name', $name)
->condition('host', $host)
->execute();
}
else {
$result = db_delete('login_security_track')
->condition('name', $name)
->execute();
}
}
else {
// Calculate protection time window and remove expired events.
$time = REQUEST_TIME - variable_get('login_security_track_time', LOGIN_SECURITY_TRACK_TIME) * 3600;
_login_security_remove_all_events($time);
}
}
/**
* Remove all tracked events up to a date..
*
* @param int $time
* if specified, events up to this timestamp will be deleted. If not
* specified, all elements up to current timestamp will be deleted.
*/
function _login_security_remove_all_events($time = NULL) {
// Remove selected events.
if (empty($time)) {
$time = REQUEST_TIME;
}
$result = db_delete('login_security_track')
->condition('timestamp', $time, '<')
->execute();
}
/**
* Save the login attempt in the tracking database: user name nd ip address.
*
* @param string $name
* user name to be tracked.
*
* @param string $ip
* IP Address of the pair.
*/
function _login_security_add_event($name, $ip) {
// Each attempt is kept for future minning of advanced bruteforcing like
// multiple IP or X-Forwarded-for usage and automated track data cleanup.
$event = new stdClass();
$event->host = $ip;
$event->name = $name;
$event->timestamp = REQUEST_TIME;
drupal_write_record('login_security_track', $event);
}
/**
* Create a Deny entry for the IP address.
*
* If IP address is not especified then block current IP.
*/
function login_user_block_ip($variables) {
// There is no need to check if the host has been banned, we can't get here
// twice.
$block = new stdClass();
$block->ip = $variables['@ip'];
drupal_write_record('blocked_ips', $block);
watchdog('login_security', 'Banned IP address @ip due to security configuration.', $variables, WATCHDOG_NOTICE, l(t('view/delete blocked IPs'), "admin/config/people/ip-blocking"));
// Using form_set_error because it may disrupt current form submission.
form_set_error('void', login_security_t(variable_get('login_security_host_hard_banned', LOGIN_SECURITY_HOST_HARD_BANNED), $variables));
}
/**
* Block a user by user name. If no user id then block current user.
*/
function login_user_block_user_name($variables) {
// If the user exists.
if ($variables['@uid'] > 1) {
// Modifying the user table is not an option so it disables the user hooks.
// Need to do firing the hook so user_notifications can be used.
// db_query("UPDATE {users} SET status = 0 WHERE uid = %d", $uid);
$uid = $variables['@uid'];
$account = user_load($uid);
// Block account if is active.
if ($account->status == 1) {
user_save($account, array(
'status' => 0,
), NULL);
// Remove user from site now.
drupal_session_destroy_uid($uid);
// The watchdog alert is set to 'user' so it will show with other blocked
// user messages.
watchdog('user', 'Blocked user @username due to security configuration.', $variables, WATCHDOG_NOTICE, l(t('edit user'), "user/{$variables['@uid']}/edit", array(
'query' => array(
'destination' => 'admin/user/user',
),
)));
// Also notify the user that account has been blocked.
form_set_error('void', login_security_t(variable_get('login_security_user_blocked', LOGIN_SECURITY_USER_BLOCKED), $variables));
// Send admin email.
$user_blocked_email_user = variable_get('login_security_user_blocked_email_user', LOGIN_SECURITY_USER_BLOCKED_EMAIL_USER);
if ($user_blocked_email_user !== '') {
$from = variable_get('site_mail', ini_get('sendmail_from'));
$admin_mail = db_select('users', 'u')
->fields('u', array(
'mail',
))
->condition('name', $user_blocked_email_user)
->execute()
->fetchField();
return drupal_mail('login_security', 'block_user_notify', $admin_mail, language_default(), $variables, $from, TRUE);
}
}
}
}
/**
* Helper function to get the variable array for the messages.
*/
function _login_security_get_variables_by_name($name) {
$account = user_load_by_name($name);
// https://drupal.org/node/1744704
if (empty($account)) {
$account = user_load(0);
}
$ipaddress = ip_address();
global $base_url;
$variables = array(
'@date' => format_date(REQUEST_TIME),
'@ip' => $ipaddress,
'@username' => $account->name,
'@email' => $account->mail,
'@uid' => $account->uid,
'@site' => variable_get('site_name', 'drupal'),
'@uri' => $base_url,
'@edit_uri' => url('user/' . $account->uid . '/edit', array(
'absolute' => TRUE,
)),
'@hard_block_attempts' => variable_get('login_security_host_wrong_count_hard', LOGIN_SECURITY_HOST_WRONG_COUNT_HARD),
'@soft_block_attempts' => variable_get('login_security_host_wrong_count', LOGIN_SECURITY_USER_WRONG_COUNT),
'@user_block_attempts' => variable_get('login_security_user_wrong_count', LOGIN_SECURITY_USER_WRONG_COUNT),
'@user_ip_current_count' => db_select('login_security_track', 'lst')
->fields('lst', array(
'id',
))
->condition('name', $name)
->condition('host', $ipaddress)
->countQuery()
->execute()
->fetchField(),
'@ip_current_count' => db_select('login_security_track', 'lst')
->fields('lst', array(
'id',
))
->condition('host', $ipaddress)
->countQuery()
->execute()
->fetchField(),
'@user_current_count' => db_select('login_security_track', 'lst')
->fields('lst', array(
'id',
))
->condition('name', $name)
->countQuery()
->execute()
->fetchField(),
'@tracking_time' => variable_get('login_security_track_time', LOGIN_SECURITY_TRACK_TIME),
'@tracking_current_count' => db_select('login_security_track', 'lst')
->fields('lst', array(
'id',
))
->countQuery()
->execute()
->fetchField(),
'@activity_threshold' => variable_get('login_security_activity_threshold', LOGIN_SECURITY_ACTIVITY_THRESHOLD),
);
return $variables;
}
/**
* Implements hook_mail().
*/
function login_security_mail($key, &$message, $variables) {
switch ($key) {
case 'block_user_notify':
$message['subject'] = login_security_t(variable_get('login_security_user_blocked_email_subject', LOGIN_SECURITY_USER_BLOCKED_EMAIL_SUBJECT), $variables);
$message['body'][] = login_security_t(variable_get('login_security_user_blocked_email_body', LOGIN_SECURITY_USER_BLOCKED_EMAIL_BODY), $variables);
break;
case 'login_activity_notify':
$message['subject'] = login_security_t(variable_get('login_security_login_activity_email_subject', LOGIN_SECURITY_LOGIN_ACTIVITY_EMAIL_SUBJECT), $variables);
$message['body'][] = login_security_t(variable_get('login_security_login_activity_email_body', LOGIN_SECURITY_LOGIN_ACTIVITY_EMAIL_BODY), $variables);
break;
}
}
/**
* The performs drupal_placeholder() on variables in an array.
*
* This option is instead of doing t() because t() can only translate static
* strings, not variables.
*/
function login_security_t($message, $variables = array()) {
foreach ($variables as $key => $value) {
$variables[$key] = drupal_placeholder($value);
}
return strtr($message, $variables);
}
/**
* Implements hook_help().
*/
function login_security_help($path, $arg = NULL) {
switch ($path) {
case 'admin/settings/login_security':
return '<p>' . t('Make sure you have reviewed the <a href="!README">README file</a> for further information about how all these settings will affect your Drupal login form submissions.', array(
'!README' => 'http://drupalcode.org/project/login_security.git/blob/refs/heads/6.x-1.x:/README.txt',
)) . '</p>';
}
}
/**
* Implements hook_nagios().
*/
function login_security_nagios() {
$return = array(
'key' => 'login_security',
);
// Get the token variables.
$variables = _login_security_get_variables_by_name('anonymous');
// Check the threshold_notified flag for the module status.
$condition = variable_get('login_security_threshold_notified', 'bogus');
if ($condition !== 'bogus') {
// Attack is happening.
if ($condition) {
$status = NAGIOS_STATUS_CRITICAL;
$text = login_security_t(LOGIN_SECURITY_LOGIN_ACTIVITY_EMAIL_SUBJECT, $variables);
}
else {
$status = NAGIOS_STATUS_OK;
$text = '';
}
}
else {
$status = NAGIOS_STATUS_UNKNOWN;
$text = t("Please check if the drupal variable login_security_threshold_notified exists.");
}
// Build the return data.
$return['login_security'] = array(
'status' => $status,
'type' => 'state',
'text' => $text,
);
return $return;
}
Functions
Name | Description |
---|---|
login_security_cron | Implements hook_cron(). |
login_security_form_alter | Implements hook_form_alter(). |
login_security_help | Implements hook_help(). |
login_security_mail | Implements hook_mail(). |
login_security_menu | Implements hook_menu(). |
login_security_nagios | Implements hook_nagios(). |
login_security_set_login_timestamp | Save login attempt and save login/access timestamps. |
login_security_soft_block_validate | Temporarily deny validation to users with excess invalid login attempts. |
login_security_submit | Implements hook_sumbit(). |
login_security_t | The performs drupal_placeholder() on variables in an array. |
login_security_user_login | Implements hook_user_login(). |
login_security_user_update | Implements hook_user_update(). |
login_security_validate | Implements hook_validate(). |
login_user_block_ip | Create a Deny entry for the IP address. |
login_user_block_user_name | Block a user by user name. If no user id then block current user. |
_login_security_access_timestamp | Returns account access timestamp. |
_login_security_add_event | Save the login attempt in the tracking database: user name nd ip address. |
_login_security_get_variables_by_name | Helper function to get the variable array for the messages. |
_login_security_login_timestamp | Returns account login timestamp. |
_login_security_remove_all_events | Remove all tracked events up to a date.. |
_login_security_remove_events | Remove tracked events or expire old ones. |