mirror of
https://gitlab.com/ArkHost/WP-Security-Pack.git
synced 2026-09-19 17:37:30 +02:00
317 lines
8.5 KiB
PHP
317 lines
8.5 KiB
PHP
<?php
|
|
/**
|
|
* IP access control for Security Pack.
|
|
*
|
|
* @package ArkHost_Security_Pack
|
|
*/
|
|
|
|
// Prevent direct access.
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* IP control class for whitelist/blacklist management.
|
|
*/
|
|
class ARKSP_IP_Control {
|
|
|
|
/**
|
|
* Cached whitelist IPs.
|
|
*
|
|
* @var array|null
|
|
*/
|
|
private $whitelist_cache = null;
|
|
|
|
/**
|
|
* Cached blacklist IPs.
|
|
*
|
|
* @var array|null
|
|
*/
|
|
private $blacklist_cache = null;
|
|
|
|
/**
|
|
* Constructor.
|
|
*/
|
|
public function __construct() {
|
|
// Run IP check immediately (constructor runs during init).
|
|
// This blocks blacklisted IPs from accessing the entire website.
|
|
$this->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,
|
|
)
|
|
);
|
|
}
|
|
}
|