args( $args, array( 'current' => true, ) ); $where = $limit = $join = $order = ''; $wheres = $prepare = 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 ( ! empty( $args['search'] ) ) { $search = '%' . $wpdb->esc_like( $args['search'] ) . '%'; $prepare = array_merge( $prepare, array_pad( array(), 6, $search ) ); $u = $wpdb->users; $l = $wpdb->base_prefix . 'itsec_lockouts'; $join .= " LEFT JOIN `{$u}` ON ( `{$l}`.`lockout_user` = `{$u}`.`ID` )"; $wheres[] = "( `{$u}`.`user_login` LIKE %s OR `{$u}`.`user_email` LIKE %s OR `{$u}`.`user_nicename` LIKE %s OR `{$u}`.`display_name` LIKE %s OR `{$l}`.`lockout_username` LIKE %s or `{$l}`.`lockout_host` LIKE %s)"; } if ( $wheres ) { $where = ' WHERE ' . implode( ' AND ', $wheres ); } if ( ! empty( $args['limit'] ) ) { $limit = ' LIMIT ' . absint( $args['limit'] ); } if ( ! empty( $args['orderby'] ) ) { $columns = array( 'lockout_id', 'lockout_start', 'lockout_expire' ); $direction = isset( $args['order'] ) ? $args['order'] : 'DESC'; if ( ! in_array( $args['orderby'], $columns, true ) ) { _doing_it_wrong( __METHOD__, "Orderby must be one of 'lockout_id', 'lockout_start', or 'lockout_expire'.", 4109 ); return array(); } if ( ! in_array( $direction, array( 'ASC', 'DESC' ), true ) ) { _doing_it_wrong( __METHOD__, "Order must be one of 'ASC' or 'DESC'.", 4109 ); return array(); } $order = " ORDER BY `{$args['orderby']}` $direction"; } if ( isset( $args['return'] ) && 'count' === $args['return'] ) { $select = 'SELECT COUNT(1) as COUNT'; $is_count = true; } else { $select = "SELECT `{$wpdb->base_prefix}itsec_lockouts`.*"; $is_count = false; } $sql = "{$select} FROM `{$wpdb->base_prefix}itsec_lockouts` {$join}{$where}{$order}{$limit};"; if ( $prepare ) { $sql = $wpdb->prepare( $sql, $prepare ); } $results = $wpdb->get_results( $sql, ARRAY_A ); if ( $is_count && $results ) { return $results[0]['COUNT']; } foreach ( $results as $result ) { wp_cache_add( $result['lockout_id'], $result, 'itsec-lockouts' ); } return $results; } /** * Checks if temp host authorization is enabled. * * @return bool */ public function is_temp_authorization_enabled() { if ( defined( 'ITSEC_DISABLE_TEMP_WHITELIST' ) && ITSEC_DISABLE_TEMP_WHITELIST ) { return false; } return ITSEC_Modules::get_setting( 'global', 'automatic_temp_auth' ); } /** * 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(); } elseif ( 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() ); } /** * Inserts an IP address into the htaccess ban list. * * @since 4.0 * * @param string $ip The IP address to ban. * @param Lockout\Context|null $context The lockout context that caused the ban. * * @return boolean False if the IP is whitelisted, true otherwise. */ public function blacklist_ip( $ip, Lockout\Context $context = null ) { $ip = sanitize_text_field( $ip ); if ( ITSEC_Lib::is_ip_banned( $ip ) ) { // Already blacklisted. return true; } if ( ITSEC_Lib::is_ip_whitelisted( $ip ) ) { // Cannot blacklist a whitelisted IP. return false; } /** * Fires when a new IP has been banned. * * This is primarily used by the Ban Users module. * * @param string $ip The IP address. * @param Lockout\Context|null $context The lockout context that caused the ban. */ do_action( 'itsec_new_banned_ip', $ip, $context ); return true; } /** * Check if the current user is temporarily whitelisted. * * @return bool */ public function is_visitor_temp_whitelisted() { if ( ! $this->is_temp_authorization_enabled() ) { return false; } $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; } /** * Purges lockouts more than 7 days old from the database * * @return void */ public function purge_lockouts() { global $wpdb; $this->record_lockout_summary(); $now = ITSEC_Core::get_current_time_gmt(); $period = ITSEC_Modules::get_setting( 'global', 'blacklist_period' ) + 1; $older_than = $now - ( $period * DAY_IN_SECONDS ); $wpdb->query( $wpdb->prepare( "DELETE FROM `{$wpdb->base_prefix}itsec_lockouts` WHERE `lockout_expire_gmt` < %s", date( 'Y-m-d H:i:s', $older_than ) ) ); $wpdb->query( $wpdb->prepare( "DELETE FROM `{$wpdb->base_prefix}itsec_temp` WHERE `temp_date_gmt` < %s", date( 'Y-m-d H:i:s', $now - DAY_IN_SECONDS ) ) ); } /** * Records a summary of the times an IP has been locked out in a day. * * We store a timestamp of the last time this function has run to avoid * counting any lockouts twice. * * @return void */ private function record_lockout_summary() { global $wpdb; $last_seen = ITSEC_Modules::get_setting( 'core', 'last_seen_lockout' ); $lockouts = $wpdb->get_results( $wpdb->prepare( <<base_prefix}itsec_lockouts` WHERE `lockout_active` = 1 AND `lockout_host` IS NOT NULL AND `lockout_host` != '' AND `lockout_start_gmt` > %s GROUP BY `lockout_host`, DATE(`lockout_start_gmt`) SQL, date( 'Y-m-d H:i:s', $last_seen ) ), ARRAY_A ); ITSEC_Modules::set_setting( 'core', 'last_seen_lockout', ITSEC_Core::get_current_time_gmt() ); if ( is_array( $lockouts ) && $lockouts ) { $insert = "INSERT INTO {$wpdb->base_prefix}itsec_dashboard_lockouts (`ip`, `time`, `count`) VALUES "; $prepare = []; foreach ( $lockouts as $lockout ) { $insert .= '(%s, %s, %d),'; $prepare[] = $lockout['h']; $prepare[] = $lockout['d']; $prepare[] = $lockout['c']; } $insert[ strlen( $insert ) - 1 ] = ';'; $wpdb->query( $wpdb->prepare( $insert, $prepare ) ); } } /** * Gets a list of top blocked IPs in a given period. * * @param int $number Number of IPs to return. * @param int $after Find IPs locked out after this time. * @param int $before Find IPs locked out before this time. * * @return Result */ public function get_top_blocked_ips( int $number, int $after, int $before ): Result { global $wpdb; $results = $wpdb->get_results( $wpdb->prepare( <<base_prefix}itsec_dashboard_lockouts WHERE `time` > %s AND `time` < %s GROUP BY `ip` ORDER BY c DESC LIMIT %d SQL, date( 'Y-m-d H:i:s', $after ), date( 'Y-m-d H:i:s', $before ), $number ), ARRAY_A ); if ( $wpdb->last_error ) { return Result::error( new WP_Error( 'itsec.lockout.top-blocked-ips.db-error', $wpdb->last_error ) ); } return Result::success( array_map( function ( array $result ) { return [ 'ip' => $result['ip'], 'count' => (int) $result['c'], ]; }, $results ) ); } /** * 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 ); } /** * Get all the registered lockout modules. * * @return array */ public function get_lockout_modules() { return $this->lockout_modules; } /** * Get lockout details. * * @param int $id * @param string $return * * @return Lockout\Lockout|array|false * @throws Exception */ public function get_lockout( $id, $return = ARRAY_A ) { global $wpdb; $data = wp_cache_get( $id, 'itsec-lockouts' ); if ( ! $data ) { $results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$wpdb->base_prefix}itsec_lockouts` WHERE `lockout_id` = %d", $id ), ARRAY_A ); if ( ! is_array( $results ) || ! isset( $results[0] ) ) { return false; } $data = $results[0]; wp_cache_add( $id, $data, 'itsec-lockouts' ); } if ( $return === OBJECT ) { return $this->hydrate_lockout_entity( $id, $data ); } return $data; } /** * 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 = 0 ) { global $wpdb; if ( ! $id ) { return false; } return (bool) $wpdb->update( $wpdb->base_prefix . 'itsec_lockouts', array( 'lockout_active' => 0, ), array( 'lockout_id' => (int) $id, ) ); } /** * 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, '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' => __( 'Site Lockouts', 'better-wp-security' ), 'description' => __( 'Various modules send emails to notify you when a user or IP address is locked out of your website.', 'better-wp-security' ), 'subject' => __( 'Site Lockout Notification', 'better-wp-security' ), ); } /** * Sends an email to notify site admins of lockouts * * @since 4.0 * * @param Lockout\Context $context * @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( Lockout\Context $context, $host_expiration, $user_expiration, $reason ) { $nc = ITSEC_Core::get_notification_center(); if ( ! $nc || ! $nc->is_notification_enabled( 'lockout' ) ) { return; } $lockouts = array(); $show_remove_ip_ban_message = false; $show_remove_lockout_message = false; if ( ( $context instanceof Lockout\User_Context && $user_id = $context->get_user_id() ) || ( $context instanceof Lockout\Host_Context && $context->is_user_limit_triggered() && $user_id = $context->get_login_user_id() ) ) { $show_remove_lockout_message = true; $lockouts[] = array( 'type' => 'user', 'id' => get_userdata( $user_id )->user_login, 'until' => $user_expiration, 'reason' => $reason, ); } if ( ( $context instanceof Lockout\Username_Context && $username = $context->get_username() ) || ( $context instanceof Lockout\Host_Context && $context->is_user_limit_triggered() && $username = $context->get_login_username() ) ) { $lockouts[] = array( 'type' => 'username', 'id' => $username, 'until' => $user_expiration, 'reason' => $reason, ); } if ( $context instanceof Lockout\Host_Context ) { 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' => $context->get_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' ), false, sprintf( esc_html__( '%s lockout notification', 'better-wp-security' ), $mail->get_display_url() ), ); $mail->add_lockouts_table( $lockouts ); if ( $show_remove_lockout_message ) { $mail->add_text( __( 'Release lockouts from the Active Lockouts section of the Security -> Dashboard page.', 'better-wp-security' ) ); $mail->add_button( __( 'Visit Dashboard', 'better-wp-security' ), ITSEC_Mail::filter_admin_page_url( network_admin_url( 'admin.php?page=itsec-dashboard' ) ) ); } if ( $show_remove_ip_ban_message ) { $mail->add_text( __( 'Release the permanently banned IP from the Banned IPs dashboard card.', 'better-wp-security' ) ); $mail->add_button( __( 'Visit Dashboard', 'better-wp-security' ), ITSEC_Mail::filter_admin_page_url( network_admin_url( 'admin.php?page=itsec-dashboard' ) ) ); } $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 ); } 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__( 'Authorized IP Triggered Ban Conditions', 'better-wp-security' ); } elseif ( 'host-triggered-blacklist' === $code ) { $entry['description'] = esc_html__( 'IP Triggered Ban Conditions', 'better-wp-security' ); } elseif ( 'whitelisted-host-triggered-host-lockout' === $code ) { $entry['description'] = esc_html__( 'Authorized IP Triggered IP Lockout', 'better-wp-security' ); } elseif ( 'host-lockout' === $code ) { if ( isset( $data[0] ) ) { $entry['description'] = sprintf( wp_kses( __( 'IP Lockout: %s', 'better-wp-security' ), array( 'code' => array() ) ), $data[0] ); } else { $entry['description'] = esc_html__( 'IP Lockout', 'better-wp-security' ); } } elseif ( 'whitelisted-host-triggered-user-lockout' === $code ) { $entry['description'] = esc_html__( 'Authorized IP Triggered User Lockout', 'better-wp-security' ); } elseif ( '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' ); } } elseif ( 'whitelisted-host-triggered-username-lockout' === $code ) { $entry['description'] = esc_html__( 'Authorized IP Triggered Username Lockout', 'better-wp-security' ); } elseif ( '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; } }