You are here

login_security.module in Login Security 5

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', 0);
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_NOTICE_ATTEMPTS_AVAILABLE', 0);
define('LOGIN_SECURITY_NOTICE_ATTEMPTS_MESSAGE', "You have used %ip_current_count out of %soft_block_attempts login attempts. After all %soft_block_attempts have been used, you will be unable to login for %tracking_time hour(s).");
define('LOGIN_SECURITY_HOST_SOFT_BANNED', "This host is not allowed to log in to %site. Please contact your site administrator.");
define('LOGIN_SECURITY_HOST_HARD_BANNED', "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', "The user <em>%username</em> has been blocked due to failed login attempts.");
define('LOGIN_SECURITY_USER_BLOCKED_EMAIL', FALSE);
define('LOGIN_SECURITY_USER_BLOCKED_EMAIL_SUBJECT', "Security action: The user %username has been blocked.");
define('LOGIN_SECURITY_USER_BLOCKED_EMAIL_BODY', "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.");

/**
 * Implementation of hook_cron().
 */
function login_security_cron() {

  // calc expiring time of login security tracked entries
  $time = time() - variable_get('login_security_track_time', LOGIN_SECURITY_TRACK_TIME) * 3600;
  db_query("DELETE FROM {login_security_track} WHERE timestamp < '%d'", $time);
  return;
}

/**
 * Implementation of hook_user().
 */
function login_security_user($op, &$edit, &$account, $category = NULL) {
  global $user;
  static $login_security_last_login = FALSE;
  static $login_security_last_access = FALSE;
  switch ($op) {
    case 'load':
      if (!empty($user->uid) && $login_security_last_login === FALSE) {
        $login_security_last_login = $user->login;
      }
      if (!empty($user->uid) && $login_security_last_access === FALSE) {
        $login_security_last_access = $user->access;
      }
      break;
    case 'login':
      if (variable_get('login_security_last_login_timestamp', 0) && $login_security_last_login > 0) {
        drupal_set_message(t('Your last login was !stamp', array(
          '!stamp' => format_date($login_security_last_login, 'large'),
        )));
      }
      if (variable_get('login_security_last_access_timestamp', 0) && $login_security_last_access > 0) {
        drupal_set_message(t('Your last page access (site activity) was !stamp', array(
          '!stamp' => format_date($login_security_last_access, 'large'),
        )));
      }

      // Remove any notice message.. Damm.. I have to add more code to clean the message than to work for security :D
      if (variable_get('login_security_notice_attempts_available', LOGIN_SECURITY_NOTICE_ATTEMPTS_AVAILABLE) && isset($_SESSION['messages']['status'])) {
        foreach ($_SESSION['messages']['status'] as $mid => $mstr) {
          if (drupal_substr($mstr, 0, 23) == "<!-- login_security -->") {
            unset($_SESSION['messages']['status'][$mid]);
          }
        }
      }

      // clean the messages queue..
      if (!count($_SESSION['messages']['status'])) {
        unset($_SESSION['messages']['status']);
      }
      if (!count($_SESSION['messages'])) {
        unset($_SESSION['messages']);
      }

      // On success login remove any temporary protection for the IP address and the username
      db_query("DELETE FROM {login_security_track} WHERE name = '%s' and host = '%s'", $edit['name'], mip_address());
      break;
    case 'update':

      // The update case can be launched by the user or by any user administrator
      // On update, remove only the unser information tracked
      db_query("DELETE FROM {login_security_track} WHERE name = '%s'", $edit['name']);
      break;
  }
}

/**
 * Implementation of hook_form_alter().
 */
function login_security_form_alter($form_id, &$form) {
  switch ($form_id) {
    case 'user_login':
    case 'user_login_block':

      // Attach a new validatdor for the name field
      $form['name']['#validate']['login_security_validate'] = array();

      // Change to do soft-blocking here, see issue: http://drupal.org/node/493164
      // We alter the form here, and still show the message in the validation
      $variables = _login_security_get_variables_by_name(check_plain($form['name']['#value']));

      //drupal_set_message("<pre>".print_r($form,1)."</pre>");

      // Check for host login attempts: Soft
      if ($variables['%soft_block_attempts'] >= 1) {
        if ($variables['%ip_current_count'] >= $variables['%soft_block_attempts']) {

          //Alter current form, so user will not be able to submit it

          // this loop is instead of doing t() because t() can only translate static strings, not variables.
          foreach ($variables as $key => $value) {
            $variables[$key] = theme('placeholder', $value);
          }
          form_set_error('submit', strtr(variable_get('login_security_host_soft_banned', LOGIN_SECURITY_HOST_SOFT_BANNED), $variables));
          unset($form['submit']);
        }
      }
      break;
    case 'user_admin_settings':
      if (user_access('administer users')) {
        $form['login_security'] = array(
          '#type' => 'fieldset',
          '#title' => t('Login Security settings'),
          '#weight' => 0,
          '#collapsible' => FALSE,
        );
        $form['login_security'][] = login_security_build_admin_form();
      }
      break;
  }
}

/**
 * Build a form body for the configuration settings.
 */
function login_security_build_admin_form() {
  $form = array();
  $form['login_security_track_time'] = array(
    '#type' => 'textfield',
    '#title' => t('Track time'),
    '#default_value' => variable_get('login_security_track_time', LOGIN_SECURITY_TRACK_TIME),
    '#size' => 3,
    '#maxlength' => 3,
    '#description' => t('Enter the time that each failed login attempt is kept for future computing.'),
    '#field_suffix' => '<kbd>' . t('Hours') . '</kbd>',
  );
  $form['login_security_user_wrong_count'] = array(
    '#type' => 'textfield',
    '#title' => t('Maximum number of login failures before blocking a user'),
    '#default_value' => variable_get('login_security_user_wrong_count', LOGIN_SECURITY_USER_WRONG_COUNT),
    '#size' => 3,
    '#maxlength' => 3,
    '#description' => t('Enter the number of login failures a user is allowed. After that amount is reached, the user will be blocked, no matter the host attempting to log in. Use this option carefully on public sites, as an attacker may block your site users.'),
    '#field_suffix' => '<kbd>' . t('Failed attempts') . '</kbd>',
  );
  $form['login_security_host_wrong_count'] = array(
    '#type' => 'textfield',
    '#title' => t('Maximum number of login failures before soft blocking a host'),
    '#default_value' => variable_get('login_security_host_wrong_count', LOGIN_SECURITY_HOST_WRONG_COUNT),
    '#size' => 3,
    '#maxlength' => 3,
    '#description' => t('Enter the number of login failures a host is allowed. After that amount is reached, the host will not be able to log in but can still browse the site contents as an anonymous user.'),
    '#field_suffix' => '<kbd>' . t('Failed attempts') . '</kbd>',
  );
  $form['login_security_host_wrong_count_hard'] = array(
    '#type' => 'textfield',
    '#title' => t('Maximum number of login failures before blocking a host'),
    '#default_value' => variable_get('login_security_host_wrong_count_hard', LOGIN_SECURITY_HOST_WRONG_COUNT_HARD),
    '#size' => 3,
    '#maxlength' => 3,
    '#description' => t('Enter the number of login failures a host is allowed. After that number is reached, the host will be blocked, no matter the username attempting to log in.'),
    '#field_suffix' => '<kbd>' . t('Failed attempts') . '</kbd>',
  );
  $form['login_security']['Notifications'] = array(
    '#type' => 'fieldset',
    '#title' => t('Edit notifications'),
    '#weight' => 0,
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    '#description' => t("Allowed placeholders for notifications include the following: %date, %ip, %username, %email, %uid, %site, %uri, %edit_uri, %hard_block_attempts, %soft_block_attempts, %user_block_attempts, %user_ip_current_count, %ip_current_count, %user_current_count, %tracking_time"),
  );
  $form['login_security']['Notifications']['login_security_notice_attempts_available'] = array(
    '#type' => 'checkbox',
    '#title' => t('Notify the user after any failed login attempt'),
    '#default_value' => variable_get('login_security_notice_attempts_available', LOGIN_SECURITY_NOTICE_ATTEMPTS_AVAILABLE),
    '#description' => t('Security tip: If you enable this option, try to not disclose as much of your login policies as possible in the message shown on any failed login attempt.'),
  );
  $form['login_security']['Notifications']['login_security_notice_attempts_message'] = array(
    '#type' => 'textarea',
    '#title' => t('Message to be shown on each failed login attempt'),
    '#rows' => 2,
    '#default_value' => variable_get('login_security_notice_attempts_message', LOGIN_SECURITY_NOTICE_ATTEMPTS_MESSAGE),
    '#description' => t('Enter the message string to be shown if the login fails after the form is submitted. You can use any of the placeholders here.'),
  );
  $form['login_security']['Notifications']['login_security_host_soft_banned'] = array(
    '#type' => 'textarea',
    '#title' => t('Message for banned host (Soft IP ban)'),
    '#rows' => 2,
    '#default_value' => variable_get('login_security_host_soft_banned', LOGIN_SECURITY_HOST_SOFT_BANNED),
    '#description' => t('Enter the soft IP ban message to be shown when a host attempts to log in too many times.'),
  );
  $form['login_security']['Notifications']['login_security_host_hard_banned'] = array(
    '#type' => 'textarea',
    '#rows' => 2,
    '#title' => t('Message for banned host (Hard IP ban)'),
    '#default_value' => variable_get('login_security_host_hard_banned', LOGIN_SECURITY_HOST_HARD_BANNED),
    '#description' => t('Enter the hard IP ban message to be shown when a host attempts to log in too many times.'),
  );
  $form['login_security']['Notifications']['login_security_user_blocked'] = array(
    '#type' => 'textarea',
    '#rows' => 2,
    '#title' => t('Message when user is blocked by uid'),
    '#default_value' => variable_get('login_security_user_blocked', LOGIN_SECURITY_USER_BLOCKED),
    '#description' => t('Enter the message to be shown when a user gets blocked due to enough failed login attempts.'),
  );
  $form['login_security']['Notifications']['login_security_user_blocked_email'] = array(
    '#type' => 'checkbox',
    '#title' => t('Send email message to the admin (uid 1) when a user is blocked'),
    '#default_value' => variable_get('login_security_user_blocked_email', LOGIN_SECURITY_USER_BLOCKED_EMAIL),
  );
  $form['login_security']['Notifications']['login_security_user_blocked_email_subject'] = array(
    '#type' => 'textfield',
    '#title' => t('Email subject'),
    '#default_value' => variable_get('login_security_user_blocked_email_subject', LOGIN_SECURITY_USER_BLOCKED_EMAIL_SUBJECT),
  );
  $form['login_security']['Notifications']['login_security_user_blocked_email_body'] = array(
    '#type' => 'textarea',
    '#title' => t('Email body'),
    '#default_value' => variable_get('login_security_user_blocked_email_body', LOGIN_SECURITY_USER_BLOCKED_EMAIL_BODY),
    '#description' => t('Enter the message to be sent to the administrator informing a user has been blocked.'),
  );
  $form['login_messages'] = array(
    '#type' => 'fieldset',
    '#title' => t('Login messages'),
  );
  $form['login_messages']['login_security_last_login_timestamp'] = array(
    '#type' => 'checkbox',
    '#title' => t('Display last login timestamp'),
    '#description' => t('The last login timestamp will be displayed as a status message when users login.'),
    '#default_value' => variable_get('login_security_last_login_timestamp', 0),
  );
  $form['login_messages']['login_security_last_access_timestamp'] = array(
    '#type' => 'checkbox',
    '#title' => t('Display last access timestamp'),
    '#description' => t('The last access timestamp will be displayed as a status message when users login.'),
    '#default_value' => variable_get('login_security_last_access_timestamp', 0),
  );
  return $form;
}

/**
 * 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_item) {

  // Sanitize user input
  $name = $form_item['#value'];

  // Null username should not be tracked
  if (!strlen($name)) {
    return;
  }

  // Save entry in security log, Username and IP Address
  login_security_save_pair($name, mip_address());

  // Populate variables to be used in any module message or login operation
  $variables = _login_security_get_variables_by_name($name);

  // Check for host login attempts: Hard
  if ($variables['%hard_block_attempts'] >= 1) {
    if ($variables['%ip_current_count'] > $variables['%hard_block_attempts']) {

      // block the host mip_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);
    }
  }

  // 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) {
    drupal_set_message("<!-- login_security -->" . t(variable_get('login_security_notice_attempts_message', LOGIN_SECURITY_NOTICE_ATTEMPTS_MESSAGE), $variables));
  }
}

/**
 * Save the login attempt in the tracking database: user name and ip address.
 *
 * @param $name
 *   user name to be tracked.
 *
 * @param $ip
 *   IP Address of the pair.
 */
function login_security_save_pair($name, $ip) {

  // Each fail attempt is kept for future minning of advanced bruteforcing
  // like multiple IP or X-Forwarded-for usage and automated track data cleanup
  db_query("INSERT INTO {login_security_track} (name, host, timestamp) VALUES ('%s', '%s', %d)", $name, $ip, time());
}

/**
 * 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) {
  db_query("INSERT INTO {access} (mask, type, status) VALUES ('%s', '%s', %d)", $variables['%ip'], 'host', 0);
  watchdog('security', t('Banned IP address %ip because of login security configuration', $variables));
  form_set_error('void', 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 ($variables['%uid'] > 1) {
    form_set_error('void', t(variable_get('login_security_user_blocked', LOGIN_SECURITY_USER_BLOCKED), $variables));

    // 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,
    ));
    user_save($account, array(
      'status' => 0,
    ), NULL);

    // remove user from site now.
    sess_destroy_uid($uid);
    watchdog('security', t('Blocked user %name: with id %uid due to security configuration', $variables));

    // Send admin email
    if (variable_get('login_security_user_blocked_email', LOGIN_SECURITY_USER_BLOCKED_EMAIL)) {
      $from = variable_get('site_mail', ini_get('sendmail_from'));
      $headers = array(
        'X-Mailer' => 'Drupal Login Security module http://drupal.org/project/login_security',
      );
      $admin_mail = db_result(db_query("SELECT mail FROM {users} WHERE uid = 1"));
      $subject = strtr(variable_get('login_security_user_blocked_email_subject', LOGIN_SECURITY_USER_BLOCKED_EMAIL_SUBJECT), $variables);
      $body = strtr(variable_get('login_security_user_blocked_email_mody', LOGIN_SECURITY_USER_BLOCKED_EMAIL_BODY), $variables);
      return drupal_mail('login_security', $admin_mail, $subject, $body, $from, $headers);
    }
  }
}

/**
 * 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 = mip_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', NULL, NULL, 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),
  );
  return $variables;
}

/**
 * Helper function to get the IP Address viewing the page.
 */
function mip_address() {
  static $ip_address = NULL;
  if (!isset($ip_address)) {
    $ip_address = $_SERVER['REMOTE_ADDR'];
    if (variable_get('reverse_proxy', 0) && array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)) {

      // If there are several arguments, we need to check the most
      // recently added one, ie the last one.
      $ip_address = array_pop(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']));
    }
  }
  return $ip_address;
}

Functions

Namesort descending Description
login_security_build_admin_form Build a form body for the configuration settings.
login_security_cron Implementation of hook_cron().
login_security_form_alter Implementation of hook_form_alter().
login_security_save_pair Save the login attempt in the tracking database: user name and ip address.
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.
mip_address Helper function to get the IP Address viewing the page.
_login_security_get_variables_by_name Helper function to get the variable array for the messages.

Constants