lockout_modules = array(); //array to hold information on modules using this feature
}
public function run() {
add_action( 'itsec_scheduler_register_events', array( $this, 'register_events' ) );
add_action( 'itsec_scheduled_purge-lockouts', array( $this, 'purge_lockouts' ) );
//Check for host lockouts
add_action( 'init', array( $this, 'check_for_host_lockouts' ) );
// Ensure that locked out users are prevented from checking logins.
add_filter( 'authenticate', array( $this, 'check_authenticate_lockout' ), 30 );
// Updated temp whitelist to ensure that admin users are automatically added.
add_action( 'init', array( $this, 'update_temp_whitelist' ), 0 );
//Register all plugin modules
add_action( 'plugins_loaded', array( $this, 'register_modules' ) );
//Set an error message on improper logout
add_action( 'login_head', array( $this, 'set_lockout_error' ) );
add_action( 'ithemes_sync_register_verbs', array( $this, 'register_sync_verbs' ) );
add_filter( 'itsec-filter-itsec-get-everything-verbs', array( $this, 'register_sync_get_everything_verbs' ) );
add_action( 'itsec-settings-page-init', array( $this, 'init_settings_page' ) );
add_action( 'itsec-logs-page-init', array( $this, 'init_settings_page' ) );
add_filter( 'itsec_notifications', array( $this, 'register_notification' ) );
add_filter( 'itsec_lockout_notification_strings', array( $this, 'notification_strings' ) );
add_filter( 'itsec_logs_prepare_lockout_entry_for_list_display', array( $this, 'filter_entry_for_list_display' ), 10, 3 );
}
public function init_settings_page() {
require_once( dirname( __FILE__ ) . '/sidebar-widget-active-lockouts.php' );
}
/**
* Check if a user has successfully logged-in, and prevent them from accessing the site if they
* still have a lockout in effect.
*
* @param \WP_User|\WP_Error|null $user
*
* @return WP_User|WP_Error|null
*/
public function check_authenticate_lockout( $user ) {
if ( ! ( $user instanceof WP_User ) ) {
return $user;
}
$this->check_lockout( $user->ID );
return $user;
}
/**
* On every page load, check if the current host is locked out.
*
* When a host becomes locked out, iThemes Security performs a quick ban. This will cause an IP block to be
* written to the site's server configuration file. This ip block might not immediately take effect, particularly
* on Nginx systems. So on every page load we check that if the current host is locked out or not.
*/
public function check_for_host_lockouts() {
$host = ITSEC_Lib::get_ip();
if ( $this->is_host_locked_out( $host ) || ITSEC_Lib::is_ip_blacklisted() ) {
$this->execute_lock();
}
}
/**
* Checks if the host or user is locked out and executes lockout
*
* @since 4.0
*
* @param mixed $user WordPress user object or false.
* @param mixed $username The username to check.
* @param string $type Lockout type asking for the check.
*
* @return void
*/
public function check_lockout( $user = false, $username = false, $type = '' ) {
global $wpdb;
$wpdb->hide_errors(); //Hide database errors in case the tables aren't there
$host = ITSEC_Lib::get_ip();
$username = sanitize_text_field( trim( $username ) );
$username_check = false;
$user_check = false;
$host_check = false;
if ( is_object( $user ) && is_a( $user, 'WP_User' ) ) {
$user_id = $user->ID;
} else if ( ! empty( $user ) ) {
$user = get_userdata( intval( $user ) );
$user_id = $user->ID;
} else {
$user = wp_get_current_user();
$user_id = $user->ID;
if ( $username !== false && $username != '' ) {
$username_check = $wpdb->get_results( $wpdb->prepare(
"SELECT `lockout_username`, `lockout_type` FROM `{$wpdb->base_prefix}itsec_lockouts` WHERE `lockout_active`=1 AND `lockout_expire_gmt` > %s AND `lockout_username`= %s;",
date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ), $username
) );
}
$host_check = $wpdb->get_results( $wpdb->prepare(
"SELECT `lockout_host`, `lockout_type` FROM `{$wpdb->base_prefix}itsec_lockouts` WHERE `lockout_active`=1 AND `lockout_expire_gmt` > %s AND `lockout_host`= %s;",
date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ), $host
) );
}
if ( $user_id !== 0 && $user_id !== null ) {
$user_check = $wpdb->get_results( $wpdb->prepare(
"SELECT `lockout_user`, `lockout_type` FROM `{$wpdb->base_prefix}itsec_lockouts` WHERE `lockout_active`=1 AND `lockout_expire_gmt` > %s AND `lockout_user`= %d;",
date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ), $user_id
) );
}
$error = $wpdb->last_error;
if ( strlen( trim( $error ) ) > 0 ) {
ITSEC_Lib::create_database_tables();
}
if ( $host_check ) {
$type = $type ? $type : $host_check[0]->lockout_type;
$this->execute_lock( array( 'type' => $type ) );
} elseif ( $user_check || $username_check ) {
if ( ! $type ) {
$type = $user_check ? $user_check[0]->lockout_type : $username_check[0]->lockout_type;
}
$lock_context = array( 'user_lock' => true, 'type' => $type );
if ( $user ) {
$lock_context['user'] = $user;
} elseif ( $username ) {
$lock_context['username'] = $username;
}
$this->execute_lock( $lock_context );
}
}
/**
* Check if a given username is locked out.
*
* @param string $username
*
* @return bool
*/
public function is_username_locked_out( $username ) {
/** @var wpdb $wpdb */
global $wpdb;
return (bool) $wpdb->get_var( $wpdb->prepare(
"SELECT `lockout_username` FROM `{$wpdb->base_prefix}itsec_lockouts` WHERE `lockout_active`=1 AND `lockout_expire_gmt` > %s AND `lockout_username` = %s;",
date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ), $username
) );
}
/**
* Check if a given user is locked out.
*
* @param string $user_id
*
* @return bool
*/
public function is_user_locked_out( $user_id ) {
/** @var wpdb $wpdb */
global $wpdb;
return (bool) $wpdb->get_var( $wpdb->prepare(
"SELECT `lockout_user` FROM `{$wpdb->base_prefix}itsec_lockouts` WHERE `lockout_active`=1 AND `lockout_expire_gmt` > %s AND `lockout_user` = %d;",
date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ), $user_id
) );
}
/**
* Check if a given host is locked out.
*
* @param string $host
*
* @return bool
*/
public function is_host_locked_out( $host ) {
/** @var wpdb $wpdb */
global $wpdb;
return (bool) $wpdb->get_var( $wpdb->prepare(
"SELECT `lockout_host` FROM `{$wpdb->base_prefix}itsec_lockouts` WHERE `lockout_active`=1 AND `lockout_expire_gmt` > %s AND `lockout_host` = %s;",
date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ), $host
) );
}
/**
* This persists a lockout to storage or performs a permanent ban if appropriate.
*
* Each module registers lockout settings that determine if a lockout event applies to the hostname, user, or
* username. In addition, these settings determine how many lockout events of a specific kind trigger an actual
* lockout by calling $this->lockout().
*
* @since 4.0
*
* @param string $module string name of the calling module
* @param string $username username of user
*
* @return void
*/
public function do_lockout( $module, $username = false ) {
global $wpdb;
if ( ! isset( $this->lockout_modules[$module] ) ) {
return;
}
// TODO: Ensure that this is not needed and remove it.
$wpdb->hide_errors(); //Hide database errors in case the tables aren't there
$lock_host = false;
$lock_user_id = false;
$lock_username = false;
$options = $this->lockout_modules[$module];
$lockout_event_data = array(
'temp_type' => $options['type'],
'temp_date' => date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time() ),
'temp_date_gmt' => date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ),
);
if ( isset( $options['host'] ) && $options['host'] > 0 ) {
$host = ITSEC_Lib::get_ip();
$host_lockout_event_data = $lockout_event_data;
$host_lockout_event_data['temp_host'] = $host;
$wpdb->insert( "{$wpdb->base_prefix}itsec_temp", $host_lockout_event_data );
$host_count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM `{$wpdb->base_prefix}itsec_temp` WHERE `temp_date_gmt` > %s AND `temp_host` = %s",
date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - ( $options['period'] * MINUTE_IN_SECONDS ) ),
$host
)
);
if ( $host_count >= $options['host'] ) {
$lock_host = $host;
}
}
if ( false !== $username && isset( $options['user'] ) && $options['user'] > 0 ) {
$username = sanitize_text_field( $username );
$user_id = username_exists( $username );
if ( false !== $user_id ) {
$user_lockout_event_data = $lockout_event_data;
$user_lockout_event_data['temp_user'] = $user_id;
$user_lockout_event_data['temp_username'] = $username;
$wpdb->insert( "{$wpdb->base_prefix}itsec_temp", $user_lockout_event_data );
$user_count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM `{$wpdb->base_prefix}itsec_temp` WHERE `temp_date_gmt` > %s AND (`temp_username` = %s OR `temp_user` = %d)",
date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - ( $options['period'] * MINUTE_IN_SECONDS ) ),
$username,
$user_id
)
);
if ( $user_count >= $options['user'] ) {
$lock_user_id = $user_id;
}
} else {
$user_lockout_event_data = $lockout_event_data;
$user_lockout_event_data['temp_username'] = $username;
$wpdb->insert( "{$wpdb->base_prefix}itsec_temp", $user_lockout_event_data );
$user_count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM `{$wpdb->base_prefix}itsec_temp` WHERE `temp_date_gmt` > %s AND `temp_username` = %s",
date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - ( $options['period'] * MINUTE_IN_SECONDS ) ),
$username
)
);
if ( $user_count >= $options['user'] ) {
$lock_username = $username;
}
}
}
$error = $wpdb->last_error;
// TODO: Confirm that this scenario can never happen and remove this
if ( strlen( trim( $error ) ) > 0 ) {
ITSEC_Lib::create_database_tables();
}
if ( false !== $lock_host || false !== $lock_user_id || false !== $lock_username ) {
$this->lockout( $module, $lock_host, $lock_user_id, $lock_username );
}
}
/**
* Executes lockout (locks user out)
*
* @param array $context
* @param bool $deprecated Deprecated argument. Previously whether this is a network lock.
*
* @return void
*/
public function execute_lock( $context = array(), $deprecated = false ) {
if ( func_num_args() > 1 ) {
_deprecated_argument( __METHOD__, '6.5.0', 'A network lockout should be specified in the $context parameter.' );
}
if ( is_array( $context ) ) {
$context = wp_parse_args( $context, array( 'user_lock' => false, 'network_lock' => false, 'type' => '' ) );
$user = $context['user_lock'];
$network = $context['network_lock'];
} else {
$user = $context;
$network = $deprecated;
}
if ( ITSEC_Lib::is_ip_whitelisted( ITSEC_Lib::get_ip() ) ) {
return;
}
if ( $network === true ) { //lockout triggered by iThemes Network
$message = ITSEC_Modules::get_setting( 'global', 'community_lockout_message' );
if ( ! $message ) {
$message = __( 'Your IP address has been flagged as a threat by the iThemes Security network.', 'better-wp-security' );
}
} elseif ( $user === true ) { //lockout the user
$message = ITSEC_Modules::get_setting( 'global', 'user_lockout_message' );
if ( ! $message ) {
$message = __( 'You have been locked out due to too many invalid login attempts.', 'better-wp-security' );
}
} else { //just lockout the host
$message = ITSEC_Modules::get_setting( 'global', 'lockout_message' );
if ( ! $message ) {
$message = __( 'Error.', 'better-wp-security' );
}
}
$formatted = false;
if ( $context['type'] ) {
/**
* Filter the lockout message displayed to the user.
*
* @param string $message
* @param string $type
* @param array $context
*/
$message = apply_filters( "itsec_{$context['type']}_lockout_message", $message, $context );
/**
* Filter whether to print the lockout error message with formatting or not.
*
* @param bool $formatted
* @param string $type
* @param array $context
*/
$formatted = apply_filters( "itsec_{$context['type']}_lockout_format_message", false, $context );
}
$current_user = wp_get_current_user();
if ( is_object( $current_user ) && isset( $current_user->ID ) ) {
wp_logout();
}
if ( $formatted ) {
wp_die( $message, '', array( 'response' => 403 ) );
} else {
@header( 'HTTP/1.0 403 Forbidden' );
@header( 'Cache-Control: no-cache, must-revalidate, max-age=0' );
@header( 'Expires: Thu, 22 Jun 1978 00:28:00 GMT' );
@header( 'Pragma: no-cache' );
add_filter( 'wp_die_handler', array( $this, 'apply_wp_die_handler' ) );
add_filter( 'wp_die_ajax_handler', array( $this, 'apply_wp_die_handler' ) );
add_filter( 'wp_die_xmlrpc_handler', array( $this, 'apply_wp_die_handler' ) );
wp_die( $message, '', array( 'response' => 403 ) );
}
}
/**
* Apply the Scalar wp die handler to print a message to the screen.
*
* @return string
*/
public function apply_wp_die_handler() {
return '_scalar_wp_die_handler';
}
/**
* Provides a description of lockout configuration for use in module settings.
*
* @since 4.0
*
* @return string the description of settings.
*/
public function get_lockout_description() {
$global_settings_url = add_query_arg( array( 'module' => 'global' ), ITSEC_Core::get_settings_page_url() ) . '#itsec-global-blacklist';
// If the user is currently viewing "all" then let them keep viewing all
if ( ! empty( $_GET['module_type'] ) && 'all' === $_GET['module_type'] ) {
$global_settings_url = add_query_arg( array( 'module_type', 'all' ), $global_settings_url );
}
$description = '
' . __( 'About Lockouts', 'better-wp-security' ) . '
';
$description .= '';
$description .= sprintf( __( 'Your lockout settings can be configured in Global Settings.', 'better-wp-security' ), esc_url( $global_settings_url ) );
$description .= '
';
$description .= __( 'Your current settings are configured as follows:', 'better-wp-security' );
$description .= '
- ';
$description .= sprintf( __( 'Permanently ban: %s', 'better-wp-security' ), ITSEC_Modules::get_setting( 'global', 'blacklist' ) === true ? __( 'yes', 'better-wp-security' ) : __( 'no', 'better-wp-security' ) );
$description .= '
- ';
$description .= sprintf( __( 'Number of lockouts before permanent ban: %s', 'better-wp-security' ), ITSEC_Modules::get_setting( 'global', 'blacklist_count' ) );
$description .= '
- ';
$description .= sprintf( __( 'How long lockouts will be remembered for ban: %s', 'better-wp-security' ), ITSEC_Modules::get_setting( 'global', 'blacklist_period' ) );
$description .= '
- ';
$description .= sprintf( __( 'Host lockout message: %s', 'better-wp-security' ), ITSEC_Modules::get_setting( 'global', 'lockout_message' ) );
$description .= '
- ';
$description .= sprintf( __( 'User lockout message: %s', 'better-wp-security' ), ITSEC_Modules::get_setting( 'global', 'user_lockout_message' ) );
$description .= '
- ';
$description .= sprintf( __( 'Is this computer white-listed: %s', 'better-wp-security' ), ITSEC_Lib::is_ip_whitelisted( ITSEC_Lib::get_ip() ) === true ? __( 'yes', 'better-wp-security' ) : __( 'no', 'better-wp-security' ) );
$description .= '
';
return $description;
}
/**
* Shows all lockouts currently in the database.
*
* @since 4.0
*
* @param string $type 'all', 'host', 'user' or 'username'.
* @param array $args Additional arguments.
*
* @return array all lockouts in the system
*/
public function get_lockouts( $type = 'all', $args = array() ) {
global $wpdb;
if ( is_bool( $args ) ) {
$args = array( 'current' => $args );
}
if ( func_num_args() === 3 ) {
$third = func_get_arg( 2 );
if ( $third && is_numeric( $third ) ) {
$args['limit'] = $third;
}
}
$args = wp_parse_args( $args, array(
'current' => true,
) );
$where = $limit = '';
$wheres = array();
switch ( $type ) {
case 'host':
$wheres[] = "`lockout_host` IS NOT NULL AND `lockout_host` != ''";
break;
case 'user':
$wheres[] = '`lockout_user` != 0';
break;
case 'username':
$wheres[] = "`lockout_username` IS NOT NULL AND `lockout_username` != ''";
break;
}
if ( $args['current'] ) {
$wheres[] = "`lockout_active` = 1 AND `lockout_expire_gmt` > '" . date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ) . "'";
}
if ( isset( $args['after'] ) ) {
$after = is_int( $args['after'] ) ? $args['after'] : strtotime( $args['after'] );
$after = date( 'Y-m-d H:i:s', $after );
$wheres[] = "`lockout_start_gmt` > '{$after}'";
}
if ( $wheres ) {
$where = ' WHERE ' . implode( ' AND ', $wheres );
}
if ( ! empty( $args['limit'] ) ) {
$limit = ' LIMIT ' . absint( $args['limit'] );
}
if ( isset( $args['return'] ) && 'count' === $args['return'] ) {
$select = 'SELECT COUNT(1) as COUNT';
$is_count = true;
} else {
$select = 'SELECT *';
$is_count = false;
}
$results = $wpdb->get_results( "{$select} FROM `" . $wpdb->base_prefix . "itsec_lockouts`" . $where . $limit . ';', ARRAY_A );
if ( $is_count && $results ) {
return $results[0]['COUNT'];
}
return $results;
}
/**
* Retrieve a list of the temporary whitelisted IP addresses.
*
* @return array A map of IP addresses to their expiration time.
*/
public function get_temp_whitelist() {
$whitelist = get_site_option( 'itsec_temp_whitelist_ip', false );
if ( ! is_array( $whitelist ) ) {
$whitelist = array();
} else if ( isset( $whitelist['ip'] ) ) {
// Update old format
$whitelist = array(
$whitelist['ip'] => $whitelist['exp'] - ITSEC_Core::get_time_offset(),
);
} else {
return $whitelist;
}
update_site_option( 'itsec_temp_whitelist_ip', $whitelist );
return $whitelist;
}
/**
* If the current user has permission to manage ITSEC, add them to the temporary whitelist.
*/
public function update_temp_whitelist() {
if ( ! ITSEC_Core::current_user_can_manage() ) {
// Only add IP's of users that can manage Security settings.
return;
}
$ip = ITSEC_Lib::get_ip();
$this->add_to_temp_whitelist( $ip );
}
/**
* Add an IP address to the temporary whitelist for 24 hours.
*
* This method will also remove any expired IPs from storage.
*
* @param string $ip
*/
public function add_to_temp_whitelist( $ip ) {
$whitelist = $this->get_temp_whitelist();
$expiration = ITSEC_Core::get_current_time_gmt() + DAY_IN_SECONDS;
$refresh_expiration = $expiration - HOUR_IN_SECONDS;
if ( isset( $whitelist[$ip] ) && $whitelist[$ip] > $refresh_expiration ) {
// An update is not needed yet.
return;
}
// Remove expired entries.
foreach ( $whitelist as $cached_ip => $cached_expiration ) {
if ( $cached_expiration < ITSEC_Core::get_current_time_gmt() ) {
unset( $whitelist[$cached_ip] );
}
}
$whitelist[$ip] = $expiration;
update_site_option( 'itsec_temp_whitelist_ip', $whitelist );
}
/**
* Remove a given IP address from the temporary whitelist.
*
* @param string $ip
*/
public function remove_from_temp_whitelist( $ip ) {
$whitelist = $this->get_temp_whitelist();
if ( ! isset( $whitelist[$ip] ) ) {
return;
}
unset( $whitelist[$ip] );
update_site_option( 'itsec_temp_whitelist_ip', $whitelist );
}
/**
* Completely clear the temporary whitelist of all IP addresses.
*/
public function clear_temp_whitelist() {
update_site_option( 'itsec_temp_whitelist_ip', array() );
}
/**
* Check if the current user is temporarily whitelisted.
*
* @return bool
*/
public function is_visitor_temp_whitelisted() {
$whitelist = $this->get_temp_whitelist();
$ip = ITSEC_Lib::get_ip();
if ( isset( $whitelist[$ip] ) && $whitelist[$ip] > ITSEC_Core::get_current_time() ) {
return true;
}
return false;
}
/**
* Store a record of the locked out user/host or permanently ban the host.
*
* Permanently banned hosts will be forwarded to the ban-users module via the itsec-new-blacklisted-ip hook and
* not persisted to the database.
*
* If configured, notifies the configured email addresses of the lockout.
*
* @since 4.0
*
* @param string $module The module triggering the lockout.
* @param string|bool $host Host to lock out or false if the host should not be locked out.
* @param int|bool $user_id User ID to lockout or false if the host should not be locked out.
* @param string|bool $username Username to lockout or false if the host should not be locked out.
*
* @return void
*/
private function lockout( $module, $host, $user_id, $username ) {
global $wpdb;
$lock = "lockout_$host$user_id$username";
// Acquire a lock to prevent a lockout being created more than once by a particularly fast attacker.
if ( ! ITSEC_Lib::get_lock( $lock, 180 ) ) {
return;
}
$module_details = $this->lockout_modules[$module];
$whitelisted = ITSEC_Lib::is_ip_whitelisted( $host );
$blacklisted = false;
$log_data = array(
'module' => $module,
'host' => $host,
'user_id' => $user_id,
'username' => $username,
'module_details' => $module_details,
'whitelisted' => $whitelisted,
'blacklisted' => false,
);
// Do a permanent ban if enabled and settings criteria are met.
if ( ITSEC_Modules::get_setting( 'global', 'blacklist' ) && false !== $host ) {
$blacklist_count = ITSEC_Modules::get_setting( 'global', 'blacklist_count' );
$blacklist_period = ITSEC_Modules::get_setting( 'global', 'blacklist_period', 7 );
$blacklist_seconds = $blacklist_period * DAY_IN_SECONDS;
$host_count = 1 + $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM `{$wpdb->base_prefix}itsec_lockouts` WHERE `lockout_expire_gmt` > %s AND `lockout_host`= %s",
date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - $blacklist_seconds ),
$host
)
);
if ( $host_count >= $blacklist_count ) {
$blacklisted = true;
$log_data['blacklisted'] = true;
if ( $whitelisted ) {
ITSEC_Log::add_notice( 'lockout', 'whitelisted-host-triggered-blacklist', array_merge( $log_data, compact( 'blacklist_period', 'blacklist_count', 'host_count' ) ) );
} else {
$this->blacklist_ip( $host );
ITSEC_Log::add_action( 'lockout', 'host-triggered-blacklist', array_merge( $log_data, compact( 'blacklist_period', 'blacklist_count', 'host_count' ) ) );
}
}
}
$host_expiration = false;
$user_expiration = false;
$lockouts_data = array(
'lockout_type' => $module_details['type'],
'lockout_start' => date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time() ),
'lockout_start_gmt' => date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() ),
);
if ( $whitelisted ) {
$lockouts_data['lockout_expire'] = date( 'Y-m-d H:i:s', 1 );
$lockouts_data['lockout_expire_gmt'] = date( 'Y-m-d H:i:s', 1 );
} else {
$exp_seconds = ITSEC_Modules::get_setting( 'global', 'lockout_period' ) * MINUTE_IN_SECONDS;
$lockouts_data['lockout_expire'] = date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time() + $exp_seconds );
$lockouts_data['lockout_expire_gmt'] = date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() + $exp_seconds );
}
if ( false !== $host && ! $blacklisted ) {
$host_expiration = $lockouts_data['lockout_expire'];
$this->add_lockout_to_db( 'host', $host, $whitelisted, $lockouts_data, $log_data );
}
if ( false !== $user_id ) {
$user_expiration = $lockouts_data['lockout_expire'];
$this->add_lockout_to_db( 'user', $user_id, $whitelisted, $lockouts_data, $log_data );
}
if ( false !== $username ) {
$user_expiration = $lockouts_data['lockout_expire'];
$this->add_lockout_to_db( 'username', $username, $whitelisted, $lockouts_data, $log_data );
}
if ( $whitelisted ) {
// No need to send an email notice when the host is whitelisted.
ITSEC_Lib::release_lock( $lock );
return;
}
$this->send_lockout_email( $host, $user_id, $username, $host_expiration, $user_expiration, $module_details['reason'] );
$lock_context = array(
'type' => $module_details['type'],
);
if ( false !== $user_id ) {
$lock_context['user'] = get_userdata( $user_id );
} else if ( false !== $username ) {
$lock_context['username'] = $username;
}
if ( false === $host ) {
$lock_context['user_lock'] = true;
}
ITSEC_Lib::release_lock( $lock );
$this->execute_lock( $lock_context );
}
/**
* Adds a record of a lockout event to the database and log the event.
*
* @param string $type The type of lockout: "host", "user", "username".
* @param string|int $id The value for the type: host's IP, user's ID, username.
* @param bool $whitelisted Whether or not the host triggering the event is whitelisted.
* @param array $lockout_data Array of base data to be inserted.
* @param array $log_data Array of data to be logged for the event.
*/
private function add_lockout_to_db( $type, $id, $whitelisted, $lockout_data, $log_data ) {
global $wpdb;
$lockout_data["lockout_$type"] = $id;
$wpdb->insert( "{$wpdb->base_prefix}itsec_lockouts", $lockout_data );
if ( $whitelisted ) {
ITSEC_Log::add_notice( 'lockout', "whitelisted-host-triggered-$type-lockout", array_merge( $log_data, $lockout_data ) );
} else {
if ( 'host' === $type ) {
$code = "host-lockout::{$log_data['host']}";
} else if ( 'user' === $type ) {
$code = "user-lockout::{$log_data['user_id']}";
} else if ( 'username' === $type ) {
$code = "username-lockout::{$log_data['username']}";
}
ITSEC_Log::add_action( 'lockout', $code, array_merge( $log_data, $lockout_data ) );
}
}
/**
* Inserts an IP address into the htaccess ban list.
*
* @since 4.0
*
* @param $ip
*
* @return boolean False if the IP is whitelisted, true otherwise.
*/
public function blacklist_ip( $ip ) {
$ip = sanitize_text_field( $ip );
if ( ITSEC_Lib::is_ip_blacklisted( $ip ) ) {
// Already blacklisted.
return true;
}
if ( ITSEC_Lib::is_ip_whitelisted( $ip ) ) {
// Cannot blacklist a whitelisted IP.
return false;
}
// The following action allows modules to handle the blacklist as needed. This is primarily useful for the Ban
// Users module.
do_action( 'itsec-new-blacklisted-ip', $ip );
return true;
}
/**
* Register the purge lockout event.
*
* @param ITSEC_Scheduler $scheduler
*/
public function register_events( $scheduler ) {
$scheduler->schedule( ITSEC_Scheduler::S_DAILY, 'purge-lockouts' );
}
/**
* Purges lockouts more than 7 days old from the database
*
* @return void
*/
public function purge_lockouts() {
global $wpdb;
$wpdb->query( "DELETE FROM `{$wpdb->base_prefix}itsec_lockouts` WHERE `lockout_expire_gmt` < '" . date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - ( ( ITSEC_Modules::get_setting( 'global', 'blacklist_period' ) + 1 ) * DAY_IN_SECONDS ) ) . "';" );
$wpdb->query( "DELETE FROM `{$wpdb->base_prefix}itsec_temp` WHERE `temp_date_gmt` < '" . date( 'Y-m-d H:i:s', ITSEC_Core::get_current_time_gmt() - DAY_IN_SECONDS ) . "';" );
}
/**
* Register verbs for Sync.
*
* @since 3.6.0
*
* @param Ithemes_Sync_API $api API object.
*/
public function register_sync_verbs( $api ) {
$api->register( 'itsec-get-lockouts', 'Ithemes_Sync_Verb_ITSEC_Get_Lockouts', dirname( __FILE__ ) . '/sync-verbs/itsec-get-lockouts.php' );
$api->register( 'itsec-release-lockout', 'Ithemes_Sync_Verb_ITSEC_Release_Lockout', dirname( __FILE__ ) . '/sync-verbs/itsec-release-lockout.php' );
$api->register( 'itsec-get-temp-whitelist', 'Ithemes_Sync_Verb_ITSEC_Get_Temp_Whitelist', dirname( __FILE__ ) . '/sync-verbs/itsec-get-temp-whitelist.php' );
$api->register( 'itsec-set-temp-whitelist', 'Ithemes_Sync_Verb_ITSEC_Set_Temp_Whitelist', dirname( __FILE__ ) . '/sync-verbs/itsec-set-temp-whitelist.php' );
}
/**
* Filter to add verbs to the response for the itsec-get-everything verb.
*
* @since 3.6.0
*
* @param array $verbs of verbs.
*
* @return array Array of verbs.
*/
public function register_sync_get_everything_verbs( $verbs ) {
$verbs['lockout'][] = 'itsec-get-lockouts';
$verbs['lockout'][] = 'itsec-get-temp-whitelist';
return $verbs;
}
/**
* Register modules that will use the lockout service.
*
* @return void
*/
public function register_modules() {
/**
* Filter the available lockout modules.
*
* @param array $lockout_modules Each lockout module should be an array containing 'type', 'reason' and
* 'period' options. The type is a unique string referring to the type of lockout.
* 'reason' is a human readable label describing the reason for the lockout.
* 'period' is the number of days to check for lockouts to decide if the host
* should be permanently banned. Additionally, the 'user' and 'host' options instruct
* security to wait for that many temporary lockout events to occur before executing
* the lockout.
*/
$this->lockout_modules = apply_filters( 'itsec_lockout_modules', $this->lockout_modules );
}
/**
* Process clearing lockouts on view log page
*
* @since 4.0
*
* @param int $id
*
* @return bool true on success or false
*/
public function release_lockout( $id = null ) {
global $wpdb;
if ( $id !== null && trim( $id ) !== '' ) {
$lockout = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$wpdb->base_prefix}itsec_lockouts` WHERE lockout_id = %d;", $id ), ARRAY_A );
if ( is_array( $lockout ) && count( $lockout ) >= 1 ) {
$success = $wpdb->update(
$wpdb->base_prefix . 'itsec_lockouts',
array(
'lockout_active' => 0,
),
array(
'lockout_id' => (int) $id,
)
);
return $success === false ? false : true;
}
}
return false;
}
/**
* Register the lockout notification.
*
* @param array $notifications
*
* @return array
*/
public function register_notification( $notifications ) {
$notifications['lockout'] = array(
'subject_editable' => true,
'recipient' => ITSEC_Notification_Center::R_USER_LIST_ADMIN_UPGRADE,
'schedule' => ITSEC_Notification_Center::S_NONE,
'optional' => true,
);
return $notifications;
}
/**
* Get the strings for the lockout notification.
*
* @return array
*/
public function notification_strings() {
return array(
'label' => esc_html__( 'Site Lockouts', 'better-wp-security' ),
'description' => esc_html__( 'Various modules send emails to notify you when a user or host is locked out of your website.', 'better-wp-security' ),
'subject' => esc_html__( 'Site Lockout Notification', 'better-wp-security' ),
);
}
/**
* Sends an email to notify site admins of lockouts
*
* @since 4.0
*
* @param string $host the host to lockout
* @param int $user_id the user id to lockout
* @param string $username the username to lockout
* @param string $host_expiration when the host login expires
* @param string $user_expiration when the user lockout expires
* @param string $reason the reason for the lockout to show to the user
*
* @return void
*/
private function send_lockout_email( $host, $user_id, $username, $host_expiration, $user_expiration, $reason ) {
$nc = ITSEC_Core::get_notification_center();
if ( ! $nc->is_notification_enabled( 'lockout' ) ) {
return;
}
$lockouts = array();
$show_remove_ip_ban_message = false;
$show_remove_lockout_message = false;
if ( false !== $user_id ) {
$user = get_userdata( $user_id );
$username = $user->user_login;
}
if ( false !== $username ) {
$show_remove_lockout_message = true;
$lockouts[] = array(
'type' => 'user',
'id' => $username,
'until' => $user_expiration,
'reason' => $reason,
);
}
if ( false !== $host ) {
if ( false === $host_expiration ) {
$host_expiration = __( 'Permanently', 'better-wp-security' );
$show_remove_ip_ban_message = true;
} else {
$show_remove_lockout_message = true;
}
$lockouts[] = array(
'type' => 'host',
'id' => '' . $host . '',
'until' => $host_expiration,
'reason' => $reason,
);
}
$mail = $nc->mail();
$mail->add_header( esc_html__( 'Site Lockout Notification', 'better-wp-security' ), esc_html__( 'Site Lockout Notification', 'better-wp-security' ) );
$mail->add_lockouts_table( $lockouts );
if ( $show_remove_lockout_message ) {
$mail->add_text( __( 'Release lockouts from the Active Lockouts section of the settings page.', 'better-wp-security' ) );
$mail->add_button( __( 'Visit Settings Page', 'better-wp-security' ), ITSEC_Mail::filter_admin_page_url( ITSEC_Core::get_settings_page_url() ) );
}
if ( $show_remove_ip_ban_message ) {
$mail->add_text( __( 'Release the permanent host ban from Ban Hosts list in the Banned Users section of the settings page.', 'better-wp-security' ) );
$mail->add_button( __( 'Visit Banned Users Settings', 'better-wp-security' ), ITSEC_Mail::filter_admin_page_url( ITSEC_Core::get_settings_module_url( 'ban-users' ) ) );
}
$mail->add_footer();
$subject = $mail->prepend_site_url_to_subject( $nc->get_subject( 'lockout' ) );
$subject = apply_filters( 'itsec_lockout_email_subject', $subject );
$mail->set_subject( $subject, false );
$nc->send( 'lockout', $mail );
}
/**
* Sets an error message when a user has been forcibly logged out due to lockout
*
* @return string
*/
public function set_lockout_error() {
//check to see if it's the logout screen
if ( isset( $_GET['itsec'] ) && $_GET['itsec'] == true ) {
return '' . ITSEC_Modules::get_setting( 'global', 'user_lockout_message' ) . '
' . PHP_EOL;
}
}
public function filter_entry_for_list_display( $entry, $code, $data ) {
$entry['module_display'] = esc_html__( 'Lockout', 'better-wp-security' );
if ( 'whitelisted-host-triggered-blacklist' === $code ) {
$entry['description'] = esc_html__( 'Whitelisted Host Triggered Blacklist', 'better-wp-security' );
} else if ( 'host-triggered-blacklist' === $code ) {
$entry['description'] = esc_html__( 'Host Triggered Blacklist', 'better-wp-security' );
} else if ( 'whitelisted-host-triggered-host-lockout' === $code ) {
$entry['description'] = esc_html__( 'Whitelisted Host Triggered Host Lockout', 'better-wp-security' );
} else if ( 'host-lockout' === $code ) {
if ( isset( $data[0] ) ) {
$entry['description'] = sprintf( wp_kses( __( 'Host Lockout: %s
', 'better-wp-security' ), array( 'code' => array() ) ), $data[0] );
} else {
$entry['description'] = esc_html__( 'Host Lockout', 'better-wp-security' );
}
} else if ( 'whitelisted-host-triggered-user-lockout' === $code ) {
$entry['description'] = esc_html__( 'Whitelisted Host Triggered User Lockout', 'better-wp-security' );
} else if ( 'user-lockout' === $code ) {
if ( isset( $data[0] ) ) {
$user = get_user_by( 'id', $data[0] );
}
if ( isset( $user ) && false !== $user ) {
$entry['description'] = sprintf( wp_kses( __( 'User Lockout: %s
', 'better-wp-security' ), array( 'code' => array() ) ), $user->user_login );
} else {
$entry['description'] = esc_html__( 'User Lockout', 'better-wp-security' );
}
} else if ( 'whitelisted-host-triggered-username-lockout' === $code ) {
$entry['description'] = esc_html__( 'Whitelisted Host Triggered Username Lockout', 'better-wp-security' );
} else if ( 'username-lockout' === $code ) {
if ( isset( $data[0] ) ) {
$entry['description'] = sprintf( wp_kses( __( 'Username Lockout: %s
', 'better-wp-security' ), array( 'code' => array() ) ), $data[0] );
} else {
$entry['description'] = esc_html__( 'Username Lockout', 'better-wp-security' );
}
}
return $entry;
}
}