login_security.module in Login Security 6
Same filename and directory in other branches
Login Security
GPL published.. if you don't have a copy of the license, search for it, it's free Copyrighted by ilo@reversing.org Thanks to christefano for the module tips and strings
File
login_security.moduleView source
<?php
/**
* @file
* Login Security
*
* GPL published.. if you don't have a copy of the license, search for it, it's free
* 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 <em>%ip</em> 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 <em>%username</em> 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);
/**
* Implementation of hook_cron().
*/
function login_security_cron() {
// Remove expired events
_login_security_remove_events();
return;
}
/**
* Implementation of hook_menu().
*/
function login_security_menu() {
$items = array();
// Administer >> Site configuration >> Login Security settings
$items['admin/settings/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;
}
/**
* Implementation of hook_user().
*/
function login_security_user($op, &$edit, &$account, $category = NULL) {
switch ($op) {
case 'login':
// On success login remove any temporary protection for the IP address and the username
_login_security_remove_events($edit['name'], ip_address());
break;
case 'update':
// The update case can be launched by the user or by any administrator
// On update, remove only the unser information tracked.
if ($edit['status'] != 0) {
// Don't remove tracking events if account is being blocked
_login_security_remove_events($account->name);
}
break;
}
}
/**
* Implementation of 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';
break;
}
}
/**
* 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.
*
* Also we save the login attempt event here.
*/
function login_security_set_login_timestamp($form, &$form_state) {
$account = user_load(array(
'name' => $form_state['values']['name'],
'pass' => trim($form_state['values']['pass']),
'status' => 1,
));
if (variable_get('login_security_last_login_timestamp', 0) && $account->login > 0) {
drupal_set_message(t('Your last login was !stamp', array(
'!stamp' => format_date($account->login, 'large'),
)), 'status');
}
if (variable_get('login_security_last_access_timestamp', 0) && $account->access > 0) {
drupal_set_message(t('Your last page access (site activity) was !stamp', array(
'!stamp' => format_date($account->access, 'large'),
)), 'status');
}
// Save entry in security log, Username and IP Address
_login_security_add_event($form_state['values']['name'], ip_address());
}
/**
* 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));
}
}
}
/**
* Implementation of form validate. This functions does more than just validating, but it's main
* Intention is to break the login form flow.
*
* @param $form_item
* The status of the name field in the form field after being submitted by the user.
*
*/
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_result(db_query("SELECT mail FROM {users} WHERE name = '%s'", $login_activity_email_user));
$subject = login_security_t(variable_get('login_security_login_activity_email_subject', LOGIN_SECURITY_LOGIN_ACTIVITY_EMAIL_SUBJECT), $variables);
$body = login_security_t(variable_get('login_security_login_activity_email_body', LOGIN_SECURITY_LOGIN_ACTIVITY_EMAIL_BODY), $variables);
$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
if ($user->uid == 0) {
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_set_error(NULL, '', TRUE);
// removes "Sorry, unrecognized username or password. Have you forgotten your password?"
// 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
unset($_SESSION['messages']['error']);
}
// 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']) {
// this loop is instead of doing t() because t() can only translate static strings, not variables.
drupal_set_message(login_security_t(variable_get('login_security_notice_attempts_message', LOGIN_SECURITY_NOTICE_ATTEMPTS_MESSAGE), $variables), 'warning');
}
}
}
/**
* Remove tracked events or expire old ones.
*
* @param $name
* if specified, events for this user name will be removed.
*
* @param $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)) {
db_query("DELETE FROM {login_security_track} WHERE name = '%s' AND host = '%s'", $name, $host);
}
else {
db_query("DELETE FROM {login_security_track} WHERE name = '%s'", $name);
}
}
else {
// Calculate protection time window and remove expired events
$time = time() - variable_get('login_security_track_time', LOGIN_SECURITY_TRACK_TIME) * 3600;
_login_security_remove_all_events($time);
}
return;
}
/**
* Remove all tracked events up to a date..
*
* @param $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 = time();
}
db_query("DELETE FROM {login_security_track} WHERE timestamp < %d", $time);
return;
}
/**
* Save the login attempt in the tracking database: user name nd ip address.
*
* @param $name
* user name to be tracked.
*
* @param $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 = 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.
*
* @param $ip
* Optional. Add a deny rule in the access control to this IP Address.
*/
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->mask = $variables['%ip'];
$block->type = 'host';
$block->status = 0;
drupal_write_record('access', $block);
watchdog('login_security', 'Banned IP address %ip due to security configuration.', $variables, WATCHDOG_NOTICE, l(t('edit rule'), "admin/user/rules/edit/{$block->aid}", array(
'query' => array(
'destination' => 'admin/user/rules',
),
)));
//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.
*
* @param $name
* Optional. The unique string identifying the 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(array(
"uid" => $uid,
));
// Block account if is active.
if ($account->status == 1) {
user_save($account, array(
'status' => 0,
), NULL);
// remove user from site now.
sess_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_result(db_query("SELECT mail FROM {users} WHERE name = '%s'", $user_blocked_email_user));
$subject = login_security_t(variable_get('login_security_user_blocked_email_subject', LOGIN_SECURITY_USER_BLOCKED_EMAIL_SUBJECT), $variables);
$body = login_security_t(variable_get('login_security_user_blocked_email_body', LOGIN_SECURITY_USER_BLOCKED_EMAIL_BODY), $variables);
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(array(
"name" => $name,
));
$ipaddress = ip_address();
global $base_url;
$variables = array(
'%date' => format_date(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_result(db_query("SELECT COUNT(id) FROM {login_security_track} WHERE name = '%s' AND host = '%s'", $name, $ipaddress)),
'%ip_current_count' => db_result(db_query("SELECT COUNT(id) FROM {login_security_track} WHERE host = '%s'", $ipaddress)),
'%user_current_count' => db_result(db_query("SELECT COUNT(id) FROM {login_security_track} WHERE name = '%s'", $name)),
'%tracking_time' => variable_get('login_security_track_time', LOGIN_SECURITY_TRACK_TIME),
'%tracking_current_count' => db_result(db_query("SELECT COUNT(id) FROM {login_security_track}")),
'%activity_threshold' => variable_get('login_security_activity_threshold', LOGIN_SECURITY_ACTIVITY_THRESHOLD),
);
return $variables;
}
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;
}
}
/**
* 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] = theme('placeholder', $value);
}
return strtr($message, $variables);
}
/**
* Implementation of 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 !README file for further information about how all these settings will affect your Drupal login form submissions.', array(
'!README' => l(t('README'), 'http://drupalcode.org/project/login_security.git/blob/refs/heads/6.x-1.x:/README.txt'),
)) . '</p>';
}
}
/**
* Implement 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 | Implementation of hook_cron(). |
login_security_form_alter | Implementation of hook_form_alter(). |
login_security_help | Implementation of hook_help(). |
login_security_mail | |
login_security_menu | Implementation of hook_menu(). |
login_security_nagios | Implement hook_nagios(). |
login_security_set_login_timestamp | 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. |
login_security_soft_block_validate | Temporarily deny validation to users with excess invalid login attempts. |
login_security_t | This option is instead of doing t() because t() can only translate static strings, not variables. |
login_security_user | Implementation of hook_user(). |
login_security_validate | Implementation of form validate. This functions does more than just validating, but it's main Intention is to break the login form flow. |
login_user_block_ip | Create a Deny entry for the IP address. If IP address is not especified then block current IP. |
login_user_block_user_name | Block a user by user name. If no user id then block current user. |
_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_remove_all_events | Remove all tracked events up to a date.. |
_login_security_remove_events | Remove tracked events or expire old ones. |