You are here

login_security.module in Login Security 6

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.module
View 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

Namesort descending 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.

Constants