check_ip_access(); } /** * Check IP access on every request. */ public function check_ip_access() { $ip = ARKSP_Helper::get_client_ip(); if ( ! $ip ) { return; } // Always allow whitelisted IPs. if ( $this->is_whitelisted( $ip ) ) { return; } // Block blacklisted IPs. if ( $this->is_blacklisted( $ip ) ) { ARKSP_Activity_Log::log( ARKSP_Activity_Log::EVENT_IP_BLOCKED, $ip, null, __( 'IP blacklisted', 'arkhost-security-pack' ) ); $this->block_access( __( 'Your IP address has been blocked.', 'arkhost-security-pack' ) ); } // Check auto-blocked IPs. if ( $this->is_auto_blocked( $ip ) ) { ARKSP_Activity_Log::log( ARKSP_Activity_Log::EVENT_IP_BLOCKED, $ip, null, __( 'IP auto-blocked', 'arkhost-security-pack' ) ); $this->block_access( __( 'Your IP address has been temporarily blocked due to suspicious activity.', 'arkhost-security-pack' ) ); } } /** * Check if IP is whitelisted. * * @param string $ip IP address to check. * @return bool */ public function is_whitelisted( $ip ) { $whitelist = $this->get_whitelist(); return ARKSP_Helper::ip_matches_rules( $ip, $whitelist ); } /** * Check if IP is blacklisted. * * @param string $ip IP address to check. * @return bool */ public function is_blacklisted( $ip ) { $blacklist = $this->get_blacklist(); return ARKSP_Helper::ip_matches_rules( $ip, $blacklist ); } /** * Check if IP is auto-blocked (temporary block from failed logins). * * @param string $ip IP address to check. * @return bool */ public function is_auto_blocked( $ip ) { global $wpdb; $max_attempts = (int) ARKSP_Plugin::get_setting( 'login_max_attempts', 5 ); $lockout_minutes = (int) ARKSP_Plugin::get_setting( 'login_lockout_duration', 15 ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time. $lockout = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}arksp_lockouts WHERE ip_address = %s", $ip ) ); if ( ! $lockout ) { return false; } // Check if IP has reached max attempts and is still within lockout window. if ( (int) $lockout->failed_attempts >= $max_attempts ) { // Calculate if lockout is still active based on updated_at + duration. $updated_time = strtotime( $lockout->updated_at ); $lockout_expires = $updated_time + ( $lockout_minutes * 60 ); $current_time = time(); if ( $current_time < $lockout_expires ) { return true; // Still locked out. } } // Also check lockout_until for honeypot/manual blocks (stored as Unix timestamp). if ( ! empty( $lockout->lockout_until ) && is_numeric( $lockout->lockout_until ) ) { if ( time() < (int) $lockout->lockout_until ) { return true; // Still locked out via lockout_until. } } return false; } /** * Get whitelist IPs. * * @return array */ public function get_whitelist() { if ( null === $this->whitelist_cache ) { $whitelist_text = ARKSP_Plugin::get_setting( 'ip_whitelist', '' ); $this->whitelist_cache = ARKSP_Helper::parse_ip_list( $whitelist_text ); } return $this->whitelist_cache; } /** * Get blacklist IPs. * * @return array */ public function get_blacklist() { if ( null === $this->blacklist_cache ) { $blacklist_text = ARKSP_Plugin::get_setting( 'ip_blacklist', '' ); $this->blacklist_cache = ARKSP_Helper::parse_ip_list( $blacklist_text ); } return $this->blacklist_cache; } /** * Clear internal caches (call after modifying whitelist/blacklist). */ public function clear_cache() { $this->whitelist_cache = null; $this->blacklist_cache = null; } /** * Add IP to auto-block list (temporary lockout). * * @param string $ip IP address. * @param int $duration Duration in minutes. * @param string $reason Reason for blocking. * @return bool */ public function auto_block_ip( $ip, $duration = 15, $reason = '' ) { global $wpdb; // Store as Unix timestamp for consistency with lockout_ip(). $lockout_until = time() + ( $duration * 60 ); // Check if already exists. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time. $existing = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}arksp_lockouts WHERE ip_address = %s", $ip ) ); if ( $existing ) { // Update existing record. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time. return false !== $wpdb->update( $wpdb->prefix . 'arksp_lockouts', array( 'lockout_until' => $lockout_until, 'updated_at' => current_time( 'mysql' ), ), array( 'ip_address' => $ip ), array( '%d', '%s' ), array( '%s' ) ); } // Insert new record. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time. return false !== $wpdb->insert( $wpdb->prefix . 'arksp_lockouts', array( 'ip_address' => $ip, 'failed_attempts' => 0, 'lockout_until' => $lockout_until, 'created_at' => current_time( 'mysql' ), 'updated_at' => current_time( 'mysql' ), ), array( '%s', '%d', '%d', '%s', '%s' ) ); } /** * Remove IP from auto-block list. * * @param string $ip IP address. * @return bool */ public function unblock_ip( $ip ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time. return false !== $wpdb->delete( $wpdb->prefix . 'arksp_lockouts', array( 'ip_address' => $ip ), array( '%s' ) ); } /** * Get all currently blocked IPs. * * @return array */ public function get_blocked_ips() { global $wpdb; $max_attempts = (int) ARKSP_Plugin::get_setting( 'login_max_attempts', 5 ); $lockout_minutes = (int) ARKSP_Plugin::get_setting( 'login_lockout_duration', 15 ); $current_time = time(); // Get all lockout records. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time. $lockouts = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}arksp_lockouts ORDER BY updated_at DESC" ); $blocked = array(); foreach ( $lockouts as $lockout ) { $is_blocked = false; $lockout_expires = 0; // Check login-based lockout (failed_attempts >= max and within time window). if ( (int) $lockout->failed_attempts >= $max_attempts ) { $updated_time = strtotime( $lockout->updated_at ); $lockout_expires = $updated_time + ( $lockout_minutes * 60 ); if ( $current_time < $lockout_expires ) { $is_blocked = true; } } // Check honeypot/manual lockout (lockout_until as Unix timestamp). if ( ! empty( $lockout->lockout_until ) && is_numeric( $lockout->lockout_until ) ) { $lockout_until_ts = (int) $lockout->lockout_until; if ( $current_time < $lockout_until_ts ) { $is_blocked = true; // Use the later expiry time. if ( $lockout_until_ts > $lockout_expires ) { $lockout_expires = $lockout_until_ts; } } } if ( $is_blocked ) { // Add computed lockout_until field for display. $lockout->lockout_until = gmdate( 'Y-m-d H:i:s', $lockout_expires ); $blocked[] = $lockout; } } return $blocked; } /** * Block access and exit. * * @param string $message Error message. */ private function block_access( $message ) { status_header( 403 ); nocache_headers(); if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { wp_send_json_error( array( 'message' => $message ), 403 ); } // Simple blocked page. wp_die( esc_html( $message ), esc_html__( 'Access Denied', 'arkhost-security-pack' ), array( 'response' => 403, 'back_link' => false, ) ); } }