v1.0
@@ -1,4 +1,4 @@
|
|||||||
# WP Security Pack
|
# Security Pack
|
||||||
|
|
||||||
WordPress security without the bullshit. No upsells, no premium tier, no fake threat counters.
|
WordPress security without the bullshit. No upsells, no premium tier, no fake threat counters.
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, Referrer-Policy, Perm
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Upload to `/wp-content/plugins/wp-security-pack/`
|
1. Upload to `/wp-content/plugins/security-pack/`
|
||||||
2. Activate
|
2. Activate
|
||||||
3. Configure under Security menu
|
3. Configure under Security menu
|
||||||
|
|
||||||
@@ -2,5 +2,5 @@
|
|||||||
/**
|
/**
|
||||||
* Silence is golden.
|
* Silence is golden.
|
||||||
*
|
*
|
||||||
* @package WP_Security_Pack
|
* @package Security_Pack
|
||||||
*/
|
*/
|
||||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 89 KiB |
@@ -2,5 +2,5 @@
|
|||||||
/**
|
/**
|
||||||
* Silence is golden.
|
* Silence is golden.
|
||||||
*
|
*
|
||||||
* @package WP_Security_Pack
|
* @package Security_Pack
|
||||||
*/
|
*/
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?php
|
||||||
|
// Silence is golden.
|
||||||
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 312 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 227 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 734 KiB |
@@ -2,5 +2,5 @@
|
|||||||
/**
|
/**
|
||||||
* Silence is golden.
|
* Silence is golden.
|
||||||
*
|
*
|
||||||
* @package WP_Security_Pack
|
* @package Security_Pack
|
||||||
*/
|
*/
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Activity logging for Security Pack.
|
||||||
|
*
|
||||||
|
* @package Security_Pack
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Prevent direct access.
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity log class.
|
||||||
|
*/
|
||||||
|
class WPSP_Activity_Log {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event types.
|
||||||
|
*/
|
||||||
|
const EVENT_LOGIN_SUCCESS = 'login_success';
|
||||||
|
const EVENT_LOGIN_FAILED = 'login_failed';
|
||||||
|
const EVENT_LOCKOUT = 'lockout';
|
||||||
|
const EVENT_IP_BLOCKED = 'ip_blocked';
|
||||||
|
const EVENT_GEO_BLOCKED = 'geo_blocked';
|
||||||
|
const EVENT_LOCKOUT_LIFTED = 'lockout_lifted';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
// Hooks are set up by Login Protection class.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log an event.
|
||||||
|
*
|
||||||
|
* @param string $event_type Event type.
|
||||||
|
* @param string|null $ip_address IP address (auto-detected if null).
|
||||||
|
* @param string|null $username Username.
|
||||||
|
* @param string|null $details Additional details.
|
||||||
|
* @return int|false Insert ID or false on failure.
|
||||||
|
*/
|
||||||
|
public static function log( $event_type, $ip_address = null, $username = null, $details = null ) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
if ( null === $ip_address ) {
|
||||||
|
$ip_address = WPSP_Helper::get_client_ip();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user agent.
|
||||||
|
$user_agent = isset( $_SERVER['HTTP_USER_AGENT'] )
|
||||||
|
? substr( sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ), 0, 255 )
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// Get country code if geo-blocking is available.
|
||||||
|
$country_code = null;
|
||||||
|
$geo_blocking = wpsp()->get_component( 'geo_blocking' );
|
||||||
|
if ( $geo_blocking && $ip_address ) {
|
||||||
|
$country_code = $geo_blocking->get_country_code( $ip_address );
|
||||||
|
}
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||||
|
$result = $wpdb->insert(
|
||||||
|
$wpdb->prefix . 'wpsp_activity_log',
|
||||||
|
array(
|
||||||
|
'event_type' => $event_type,
|
||||||
|
'ip_address' => $ip_address ? $ip_address : '',
|
||||||
|
'username' => $username,
|
||||||
|
'user_agent' => $user_agent,
|
||||||
|
'country_code' => $country_code,
|
||||||
|
'details' => $details,
|
||||||
|
'created_at' => current_time( 'mysql' ),
|
||||||
|
),
|
||||||
|
array( '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
|
||||||
|
);
|
||||||
|
|
||||||
|
return $result ? $wpdb->insert_id : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get recent logs.
|
||||||
|
*
|
||||||
|
* @param array $args Query arguments.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function get_logs( $args = array() ) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$defaults = array(
|
||||||
|
'limit' => 50,
|
||||||
|
'offset' => 0,
|
||||||
|
'event_type' => '',
|
||||||
|
'ip_address' => '',
|
||||||
|
);
|
||||||
|
|
||||||
|
$args = wp_parse_args( $args, $defaults );
|
||||||
|
$limit = absint( $args['limit'] );
|
||||||
|
$offset = absint( $args['offset'] );
|
||||||
|
|
||||||
|
// Build query based on filters. Always order by created_at DESC for security logs.
|
||||||
|
if ( ! empty( $args['event_type'] ) && ! empty( $args['ip_address'] ) ) {
|
||||||
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
|
||||||
|
return $wpdb->get_results(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}wpsp_activity_log WHERE event_type = %s AND ip_address = %s ORDER BY created_at DESC LIMIT %d OFFSET %d",
|
||||||
|
$args['event_type'],
|
||||||
|
$args['ip_address'],
|
||||||
|
$limit,
|
||||||
|
$offset
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} elseif ( ! empty( $args['event_type'] ) ) {
|
||||||
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
|
||||||
|
return $wpdb->get_results(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}wpsp_activity_log WHERE event_type = %s ORDER BY created_at DESC LIMIT %d OFFSET %d",
|
||||||
|
$args['event_type'],
|
||||||
|
$limit,
|
||||||
|
$offset
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} elseif ( ! empty( $args['ip_address'] ) ) {
|
||||||
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
|
||||||
|
return $wpdb->get_results(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}wpsp_activity_log WHERE ip_address = %s ORDER BY created_at DESC LIMIT %d OFFSET %d",
|
||||||
|
$args['ip_address'],
|
||||||
|
$limit,
|
||||||
|
$offset
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No filters.
|
||||||
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
|
||||||
|
return $wpdb->get_results(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT * FROM {$wpdb->prefix}wpsp_activity_log ORDER BY created_at DESC LIMIT %d OFFSET %d",
|
||||||
|
$limit,
|
||||||
|
$offset
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total log count.
|
||||||
|
*
|
||||||
|
* @param array $args Query arguments.
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public static function get_log_count( $args = array() ) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// Build query based on filters.
|
||||||
|
if ( ! empty( $args['event_type'] ) && ! empty( $args['ip_address'] ) ) {
|
||||||
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(*) FROM {$wpdb->prefix}wpsp_activity_log WHERE event_type = %s AND ip_address = %s",
|
||||||
|
$args['event_type'],
|
||||||
|
$args['ip_address']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} elseif ( ! empty( $args['event_type'] ) ) {
|
||||||
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(*) FROM {$wpdb->prefix}wpsp_activity_log WHERE event_type = %s",
|
||||||
|
$args['event_type']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} elseif ( ! empty( $args['ip_address'] ) ) {
|
||||||
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
|
||||||
|
return (int) $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(*) FROM {$wpdb->prefix}wpsp_activity_log WHERE ip_address = %s",
|
||||||
|
$args['ip_address']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No filters.
|
||||||
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
|
||||||
|
return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}wpsp_activity_log" );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get log statistics.
|
||||||
|
*
|
||||||
|
* @param int $days Number of days to look back.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function get_stats( $days = 30 ) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$cutoff = gmdate( 'Y-m-d H:i:s', strtotime( "-{$days} days" ) );
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security stats must be real-time.
|
||||||
|
$results = $wpdb->get_results(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT event_type, COUNT(*) as count FROM {$wpdb->prefix}wpsp_activity_log WHERE created_at >= %s GROUP BY event_type",
|
||||||
|
$cutoff
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$stats = array(
|
||||||
|
'login_success' => 0,
|
||||||
|
'login_failed' => 0,
|
||||||
|
'lockout' => 0,
|
||||||
|
'ip_blocked' => 0,
|
||||||
|
'geo_blocked' => 0,
|
||||||
|
'total' => 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $results as $row ) {
|
||||||
|
$stats[ $row->event_type ] = (int) $row->count;
|
||||||
|
$stats['total'] += (int) $row->count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup old logs (cron job).
|
||||||
|
*/
|
||||||
|
public static function cleanup_old_logs() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$retention_days = Security_Pack::get_setting( 'log_retention_days', 30 );
|
||||||
|
$cutoff = gmdate( 'Y-m-d H:i:s', strtotime( "-{$retention_days} days" ) );
|
||||||
|
|
||||||
|
// Delete old logs.
|
||||||
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Cleanup operation, caching not applicable.
|
||||||
|
$wpdb->query(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"DELETE FROM {$wpdb->prefix}wpsp_activity_log WHERE created_at < %s",
|
||||||
|
$cutoff
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete expired lockouts (lockout_until stores Unix timestamp as integer).
|
||||||
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Cleanup operation, caching not applicable.
|
||||||
|
$wpdb->query(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"DELETE FROM {$wpdb->prefix}wpsp_lockouts WHERE lockout_until IS NOT NULL AND lockout_until > 0 AND lockout_until < %d",
|
||||||
|
time()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all logs.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function clear_all_logs() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Truncate for admin action.
|
||||||
|
return false !== $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wpsp_activity_log" );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get event type label.
|
||||||
|
*
|
||||||
|
* @param string $event_type Event type.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function get_event_label( $event_type ) {
|
||||||
|
$labels = array(
|
||||||
|
self::EVENT_LOGIN_SUCCESS => __( 'Login Success', 'security-pack' ),
|
||||||
|
self::EVENT_LOGIN_FAILED => __( 'Login Failed', 'security-pack' ),
|
||||||
|
self::EVENT_LOCKOUT => __( 'Lockout', 'security-pack' ),
|
||||||
|
self::EVENT_IP_BLOCKED => __( 'IP Blocked', 'security-pack' ),
|
||||||
|
self::EVENT_GEO_BLOCKED => __( 'Geo Blocked', 'security-pack' ),
|
||||||
|
self::EVENT_LOCKOUT_LIFTED => __( 'Lockout Lifted', 'security-pack' ),
|
||||||
|
);
|
||||||
|
|
||||||
|
return isset( $labels[ $event_type ] ) ? $labels[ $event_type ] : $event_type;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Database management for WP Security Pack.
|
* Database management for Security Pack.
|
||||||
*
|
*
|
||||||
* @package WP_Security_Pack
|
* @package Security_Pack
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Prevent direct access.
|
// Prevent direct access.
|
||||||
@@ -23,23 +23,23 @@ class WPSP_DB {
|
|||||||
const DB_VERSION = '1.0.1';
|
const DB_VERSION = '1.0.1';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get activity log table name.
|
* Get activity log table name (escaped for safe use in queries).
|
||||||
*
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public static function get_log_table() {
|
public static function get_log_table() {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
return $wpdb->prefix . 'wpsp_activity_log';
|
return esc_sql( $wpdb->prefix . 'wpsp_activity_log' );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get lockouts table name.
|
* Get lockouts table name (escaped for safe use in queries).
|
||||||
*
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public static function get_lockout_table() {
|
public static function get_lockout_table() {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
return $wpdb->prefix . 'wpsp_lockouts';
|
return esc_sql( $wpdb->prefix . 'wpsp_lockouts' );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -132,7 +132,7 @@ class WPSP_DB {
|
|||||||
$existing = get_option( 'wpsp_settings', false );
|
$existing = get_option( 'wpsp_settings', false );
|
||||||
|
|
||||||
if ( false === $existing ) {
|
if ( false === $existing ) {
|
||||||
$defaults = WP_Security_Pack::get_default_settings();
|
$defaults = Security_Pack::get_default_settings();
|
||||||
update_option( 'wpsp_settings', $defaults );
|
update_option( 'wpsp_settings', $defaults );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,13 +144,10 @@ class WPSP_DB {
|
|||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
// Drop tables.
|
// Drop tables.
|
||||||
$log_table = self::get_log_table();
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange -- Uninstall cleanup.
|
||||||
$lockout_table = self::get_lockout_table();
|
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}wpsp_activity_log" );
|
||||||
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange -- Uninstall cleanup.
|
||||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}wpsp_lockouts" );
|
||||||
$wpdb->query( "DROP TABLE IF EXISTS {$log_table}" );
|
|
||||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
||||||
$wpdb->query( "DROP TABLE IF EXISTS {$lockout_table}" );
|
|
||||||
|
|
||||||
// Delete options.
|
// Delete options.
|
||||||
delete_option( 'wpsp_settings' );
|
delete_option( 'wpsp_settings' );
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* File integrity monitoring for WP Security Pack.
|
* File integrity monitoring for Security Pack.
|
||||||
*
|
*
|
||||||
* @package WP_Security_Pack
|
* @package Security_Pack
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Prevent direct access.
|
// Prevent direct access.
|
||||||
@@ -40,7 +40,7 @@ class WPSP_File_Integrity {
|
|||||||
* Constructor.
|
* Constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
if ( ! WP_Security_Pack::get_setting( 'file_integrity_enabled', true ) ) {
|
if ( ! Security_Pack::get_setting( 'file_integrity_enabled', true ) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,28 +256,28 @@ class WPSP_File_Integrity {
|
|||||||
* @param array $changes File changes.
|
* @param array $changes File changes.
|
||||||
*/
|
*/
|
||||||
private function send_alert( $changes ) {
|
private function send_alert( $changes ) {
|
||||||
if ( ! WP_Security_Pack::get_setting( 'email_alerts_enabled', false ) ) {
|
if ( ! Security_Pack::get_setting( 'email_alerts_enabled', false ) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$email = WP_Security_Pack::get_setting( 'email_alerts_address', get_option( 'admin_email' ) );
|
$email = Security_Pack::get_setting( 'email_alerts_address', get_option( 'admin_email' ) );
|
||||||
$site_name = get_bloginfo( 'name' );
|
$site_name = get_bloginfo( 'name' );
|
||||||
$site_url = home_url();
|
$site_url = home_url();
|
||||||
|
|
||||||
$subject = sprintf(
|
$subject = sprintf(
|
||||||
/* translators: %s: Site name */
|
/* translators: %s: Site name */
|
||||||
__( '[%s] File Integrity Alert - Core Files Changed', 'wp-security-pack' ),
|
__( '[%s] File Integrity Alert - Core Files Changed', 'security-pack' ),
|
||||||
$site_name
|
$site_name
|
||||||
);
|
);
|
||||||
|
|
||||||
$message = sprintf(
|
$message = sprintf(
|
||||||
/* translators: %s: Site URL */
|
/* translators: %s: Site URL */
|
||||||
__( "WP Security Pack has detected changes to WordPress core files on %s.\n\n", 'wp-security-pack' ),
|
__( "Security Pack has detected changes to WordPress core files on %s.\n\n", 'security-pack' ),
|
||||||
$site_url
|
$site_url
|
||||||
);
|
);
|
||||||
|
|
||||||
if ( ! empty( $changes['modified'] ) ) {
|
if ( ! empty( $changes['modified'] ) ) {
|
||||||
$message .= __( "Modified files:\n", 'wp-security-pack' );
|
$message .= __( "Modified files:\n", 'security-pack' );
|
||||||
foreach ( $changes['modified'] as $file ) {
|
foreach ( $changes['modified'] as $file ) {
|
||||||
$message .= '- ' . ( is_array( $file ) ? $file['file'] : $file ) . "\n";
|
$message .= '- ' . ( is_array( $file ) ? $file['file'] : $file ) . "\n";
|
||||||
}
|
}
|
||||||
@@ -285,7 +285,7 @@ class WPSP_File_Integrity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ( ! empty( $changes['added'] ) ) {
|
if ( ! empty( $changes['added'] ) ) {
|
||||||
$message .= __( "New files detected:\n", 'wp-security-pack' );
|
$message .= __( "New files detected:\n", 'security-pack' );
|
||||||
foreach ( $changes['added'] as $file ) {
|
foreach ( $changes['added'] as $file ) {
|
||||||
$message .= '- ' . $file . "\n";
|
$message .= '- ' . $file . "\n";
|
||||||
}
|
}
|
||||||
@@ -293,18 +293,18 @@ class WPSP_File_Integrity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ( ! empty( $changes['removed'] ) ) {
|
if ( ! empty( $changes['removed'] ) ) {
|
||||||
$message .= __( "Removed files:\n", 'wp-security-pack' );
|
$message .= __( "Removed files:\n", 'security-pack' );
|
||||||
foreach ( $changes['removed'] as $file ) {
|
foreach ( $changes['removed'] as $file ) {
|
||||||
$message .= '- ' . $file . "\n";
|
$message .= '- ' . $file . "\n";
|
||||||
}
|
}
|
||||||
$message .= "\n";
|
$message .= "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
$message .= __( "This could indicate:\n", 'wp-security-pack' );
|
$message .= __( "This could indicate:\n", 'security-pack' );
|
||||||
$message .= __( "- A recent WordPress update (normal)\n", 'wp-security-pack' );
|
$message .= __( "- A recent WordPress update (normal)\n", 'security-pack' );
|
||||||
$message .= __( "- Unauthorized modifications (investigate)\n", 'wp-security-pack' );
|
$message .= __( "- Unauthorized modifications (investigate)\n", 'security-pack' );
|
||||||
$message .= __( "- Plugin/theme conflicts (rare)\n\n", 'wp-security-pack' );
|
$message .= __( "- Plugin/theme conflicts (rare)\n\n", 'security-pack' );
|
||||||
$message .= __( "Review these changes in your WordPress admin panel.", 'wp-security-pack' );
|
$message .= __( "Review these changes in your WordPress admin panel.", 'security-pack' );
|
||||||
|
|
||||||
wp_mail( $email, $subject, $message );
|
wp_mail( $email, $subject, $message );
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Geo-blocking for WP Security Pack.
|
* Geo-blocking for Security Pack.
|
||||||
*
|
*
|
||||||
* @package WP_Security_Pack
|
* @package Security_Pack
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Prevent direct access.
|
// Prevent direct access.
|
||||||
@@ -40,7 +40,7 @@ class WPSP_Geo_Blocking {
|
|||||||
* Constructor.
|
* Constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
$this->db_path = WP_Security_Pack::get_setting( 'geo_database_path', '' );
|
$this->db_path = Security_Pack::get_setting( 'geo_database_path', '' );
|
||||||
|
|
||||||
if ( empty( $this->db_path ) ) {
|
if ( empty( $this->db_path ) ) {
|
||||||
// Default path in plugin directory.
|
// Default path in plugin directory.
|
||||||
@@ -49,7 +49,7 @@ class WPSP_Geo_Blocking {
|
|||||||
|
|
||||||
// Check geo-blocking immediately (constructor runs during init).
|
// Check geo-blocking immediately (constructor runs during init).
|
||||||
// This blocks access to the entire website for blocked countries.
|
// This blocks access to the entire website for blocked countries.
|
||||||
if ( WP_Security_Pack::get_setting( 'geo_blocking_enabled', false ) ) {
|
if ( Security_Pack::get_setting( 'geo_blocking_enabled', false ) ) {
|
||||||
$this->check_geo_access();
|
$this->check_geo_access();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ class WPSP_Geo_Blocking {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if country is blocked.
|
// Check if country is blocked.
|
||||||
$blocked_countries = WP_Security_Pack::get_setting( 'geo_blocked_countries', array() );
|
$blocked_countries = Security_Pack::get_setting( 'geo_blocked_countries', array() );
|
||||||
|
|
||||||
if ( ! empty( $blocked_countries ) && in_array( $country_code, $blocked_countries, true ) ) {
|
if ( ! empty( $blocked_countries ) && in_array( $country_code, $blocked_countries, true ) ) {
|
||||||
WPSP_Activity_Log::log(
|
WPSP_Activity_Log::log(
|
||||||
@@ -87,7 +87,7 @@ class WPSP_Geo_Blocking {
|
|||||||
null,
|
null,
|
||||||
sprintf(
|
sprintf(
|
||||||
/* translators: %s: Country code */
|
/* translators: %s: Country code */
|
||||||
__( 'Blocked country: %s', 'wp-security-pack' ),
|
__( 'Blocked country: %s', 'security-pack' ),
|
||||||
$country_code
|
$country_code
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -182,7 +182,7 @@ class WPSP_Geo_Blocking {
|
|||||||
|
|
||||||
$message = sprintf(
|
$message = sprintf(
|
||||||
/* translators: %s: Country name */
|
/* translators: %s: Country name */
|
||||||
__( 'Access from %s is not permitted.', 'wp-security-pack' ),
|
__( 'Access from %s is not permitted.', 'security-pack' ),
|
||||||
$country_name
|
$country_name
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ class WPSP_Geo_Blocking {
|
|||||||
|
|
||||||
wp_die(
|
wp_die(
|
||||||
esc_html( $message ),
|
esc_html( $message ),
|
||||||
esc_html__( 'Access Denied', 'wp-security-pack' ),
|
esc_html__( 'Access Denied', 'security-pack' ),
|
||||||
array(
|
array(
|
||||||
'response' => 403,
|
'response' => 403,
|
||||||
'back_link' => false,
|
'back_link' => false,
|
||||||
@@ -303,10 +303,10 @@ class WPSP_Geo_Blocking {
|
|||||||
|
|
||||||
if ( file_exists( $target_path ) ) {
|
if ( file_exists( $target_path ) ) {
|
||||||
// Update database path setting.
|
// Update database path setting.
|
||||||
WP_Security_Pack::update_setting( 'geo_database_path', $target_path );
|
Security_Pack::update_setting( 'geo_database_path', $target_path );
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new WP_Error( 'download_failed', __( 'Failed to download or extract database.', 'wp-security-pack' ) );
|
return new WP_Error( 'download_failed', __( 'Failed to download or extract database.', 'security-pack' ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Security hardening for WP Security Pack.
|
* Security hardening for Security Pack.
|
||||||
*
|
*
|
||||||
* @package WP_Security_Pack
|
* @package Security_Pack
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Prevent direct access.
|
// Prevent direct access.
|
||||||
@@ -20,19 +20,19 @@ class WPSP_Hardening {
|
|||||||
*/
|
*/
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
// Disable XML-RPC.
|
// Disable XML-RPC.
|
||||||
if ( WP_Security_Pack::get_setting( 'disable_xmlrpc', true ) ) {
|
if ( Security_Pack::get_setting( 'disable_xmlrpc', true ) ) {
|
||||||
add_filter( 'xmlrpc_enabled', '__return_false' );
|
add_filter( 'xmlrpc_enabled', '__return_false' );
|
||||||
add_filter( 'wp_headers', array( $this, 'remove_xmlrpc_headers' ) );
|
add_filter( 'wp_headers', array( $this, 'remove_xmlrpc_headers' ) );
|
||||||
add_action( 'wp', array( $this, 'block_xmlrpc_requests' ), 1 );
|
add_action( 'wp', array( $this, 'block_xmlrpc_requests' ), 1 );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable file editing.
|
// Disable file editing.
|
||||||
if ( WP_Security_Pack::get_setting( 'disable_file_editing', true ) ) {
|
if ( Security_Pack::get_setting( 'disable_file_editing', true ) ) {
|
||||||
$this->disable_file_editing();
|
$this->disable_file_editing();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove WordPress version.
|
// Remove WordPress version.
|
||||||
if ( WP_Security_Pack::get_setting( 'remove_wp_version', true ) ) {
|
if ( Security_Pack::get_setting( 'remove_wp_version', true ) ) {
|
||||||
add_filter( 'the_generator', '__return_empty_string' );
|
add_filter( 'the_generator', '__return_empty_string' );
|
||||||
remove_action( 'wp_head', 'wp_generator' );
|
remove_action( 'wp_head', 'wp_generator' );
|
||||||
add_filter( 'style_loader_src', array( $this, 'remove_version_strings' ), 10, 2 );
|
add_filter( 'style_loader_src', array( $this, 'remove_version_strings' ), 10, 2 );
|
||||||
@@ -40,17 +40,17 @@ class WPSP_Hardening {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add security headers.
|
// Add security headers.
|
||||||
if ( WP_Security_Pack::get_setting( 'add_security_headers', true ) ) {
|
if ( Security_Pack::get_setting( 'add_security_headers', true ) ) {
|
||||||
add_action( 'send_headers', array( $this, 'add_security_headers' ) );
|
add_action( 'send_headers', array( $this, 'add_security_headers' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restrict REST API.
|
// Restrict REST API.
|
||||||
if ( WP_Security_Pack::get_setting( 'restrict_rest_api', true ) ) {
|
if ( Security_Pack::get_setting( 'restrict_rest_api', true ) ) {
|
||||||
add_filter( 'rest_authentication_errors', array( $this, 'restrict_rest_api' ) );
|
add_filter( 'rest_authentication_errors', array( $this, 'restrict_rest_api' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable application passwords for non-admins.
|
// Disable application passwords for non-admins.
|
||||||
if ( WP_Security_Pack::get_setting( 'disable_application_passwords', false ) ) {
|
if ( Security_Pack::get_setting( 'disable_application_passwords', false ) ) {
|
||||||
add_filter( 'wp_is_application_passwords_available', '__return_false' );
|
add_filter( 'wp_is_application_passwords_available', '__return_false' );
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,14 +58,14 @@ class WPSP_Hardening {
|
|||||||
add_action( 'init', array( $this, 'remove_unnecessary_headers' ) );
|
add_action( 'init', array( $this, 'remove_unnecessary_headers' ) );
|
||||||
|
|
||||||
// Disable user enumeration.
|
// Disable user enumeration.
|
||||||
if ( WP_Security_Pack::get_setting( 'disable_user_enumeration', true ) ) {
|
if ( Security_Pack::get_setting( 'disable_user_enumeration', true ) ) {
|
||||||
add_action( 'init', array( $this, 'block_author_scanning' ) );
|
add_action( 'init', array( $this, 'block_author_scanning' ) );
|
||||||
add_filter( 'rest_endpoints', array( $this, 'restrict_users_endpoint' ) );
|
add_filter( 'rest_endpoints', array( $this, 'restrict_users_endpoint' ) );
|
||||||
add_filter( 'oembed_response_data', array( $this, 'remove_author_from_oembed' ) );
|
add_filter( 'oembed_response_data', array( $this, 'remove_author_from_oembed' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable pingbacks/trackbacks.
|
// Disable pingbacks/trackbacks.
|
||||||
if ( WP_Security_Pack::get_setting( 'disable_pingbacks', true ) ) {
|
if ( Security_Pack::get_setting( 'disable_pingbacks', true ) ) {
|
||||||
add_filter( 'xmlrpc_methods', array( $this, 'disable_pingback_methods' ) );
|
add_filter( 'xmlrpc_methods', array( $this, 'disable_pingback_methods' ) );
|
||||||
add_filter( 'wp_headers', array( $this, 'remove_pingback_header' ) );
|
add_filter( 'wp_headers', array( $this, 'remove_pingback_header' ) );
|
||||||
add_filter( 'pings_open', '__return_false', 20, 2 );
|
add_filter( 'pings_open', '__return_false', 20, 2 );
|
||||||
@@ -101,7 +101,7 @@ class WPSP_Hardening {
|
|||||||
*/
|
*/
|
||||||
private function disable_file_editing() {
|
private function disable_file_editing() {
|
||||||
if ( ! defined( 'DISALLOW_FILE_EDIT' ) ) {
|
if ( ! defined( 'DISALLOW_FILE_EDIT' ) ) {
|
||||||
define( 'DISALLOW_FILE_EDIT', true );
|
define( 'DISALLOW_FILE_EDIT', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- WordPress core constant.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ class WPSP_Hardening {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get header settings.
|
// Get header settings.
|
||||||
$headers = WP_Security_Pack::get_setting( 'security_headers', $this->get_default_headers() );
|
$headers = Security_Pack::get_setting( 'security_headers', $this->get_default_headers() );
|
||||||
|
|
||||||
// X-Content-Type-Options.
|
// X-Content-Type-Options.
|
||||||
if ( ! empty( $headers['x_content_type_options'] ) ) {
|
if ( ! empty( $headers['x_content_type_options'] ) ) {
|
||||||
@@ -204,7 +204,7 @@ class WPSP_Hardening {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get allowed REST routes.
|
// Get allowed REST routes.
|
||||||
$allowed_routes = WP_Security_Pack::get_setting( 'rest_api_allowed_routes', array() );
|
$allowed_routes = Security_Pack::get_setting( 'rest_api_allowed_routes', array() );
|
||||||
|
|
||||||
// Always allow some essential routes.
|
// Always allow some essential routes.
|
||||||
$essential_routes = array(
|
$essential_routes = array(
|
||||||
@@ -227,7 +227,7 @@ class WPSP_Hardening {
|
|||||||
// Block unauthenticated access.
|
// Block unauthenticated access.
|
||||||
return new WP_Error(
|
return new WP_Error(
|
||||||
'rest_not_logged_in',
|
'rest_not_logged_in',
|
||||||
__( 'You must be authenticated to access this endpoint.', 'wp-security-pack' ),
|
__( 'You must be authenticated to access this endpoint.', 'security-pack' ),
|
||||||
array( 'status' => 401 )
|
array( 'status' => 401 )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -246,7 +246,7 @@ class WPSP_Hardening {
|
|||||||
remove_action( 'wp_head', 'wp_shortlink_wp_head' );
|
remove_action( 'wp_head', 'wp_shortlink_wp_head' );
|
||||||
|
|
||||||
// Remove feed links.
|
// Remove feed links.
|
||||||
if ( WP_Security_Pack::get_setting( 'remove_feed_links', false ) ) {
|
if ( Security_Pack::get_setting( 'remove_feed_links', false ) ) {
|
||||||
remove_action( 'wp_head', 'feed_links', 2 );
|
remove_action( 'wp_head', 'feed_links', 2 );
|
||||||
remove_action( 'wp_head', 'feed_links_extra', 3 );
|
remove_action( 'wp_head', 'feed_links_extra', 3 );
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,555 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Helper functions for Security Pack.
|
||||||
|
*
|
||||||
|
* @package Security_Pack
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Prevent direct access.
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class with utility functions.
|
||||||
|
*/
|
||||||
|
class WPSP_Helper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the real client IP address.
|
||||||
|
*
|
||||||
|
* Handles proxies and CDNs like Cloudflare.
|
||||||
|
*
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public static function get_client_ip() {
|
||||||
|
$ip = null;
|
||||||
|
|
||||||
|
// Priority order for IP detection.
|
||||||
|
$headers = array(
|
||||||
|
'HTTP_CF_CONNECTING_IP', // Cloudflare.
|
||||||
|
'HTTP_X_REAL_IP', // Nginx reverse proxy.
|
||||||
|
'HTTP_X_FORWARDED_FOR', // Generic proxy.
|
||||||
|
'REMOTE_ADDR', // Direct connection.
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $headers as $header ) {
|
||||||
|
if ( ! empty( $_SERVER[ $header ] ) ) {
|
||||||
|
// X-Forwarded-For can contain multiple IPs, get the first one.
|
||||||
|
$ip = sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) );
|
||||||
|
if ( 'HTTP_X_FORWARDED_FOR' === $header && strpos( $ip, ',' ) !== false ) {
|
||||||
|
$ips = explode( ',', $ip );
|
||||||
|
$ip = trim( $ips[0] );
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate IP.
|
||||||
|
if ( $ip && filter_var( $ip, FILTER_VALIDATE_IP ) ) {
|
||||||
|
return $ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an IP is in a CIDR range.
|
||||||
|
*
|
||||||
|
* @param string $ip IP address to check.
|
||||||
|
* @param string $cidr CIDR notation (e.g., 192.168.1.0/24).
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function ip_in_cidr( $ip, $cidr ) {
|
||||||
|
// Handle exact IP match (no CIDR notation).
|
||||||
|
if ( strpos( $cidr, '/' ) === false ) {
|
||||||
|
return $ip === $cidr;
|
||||||
|
}
|
||||||
|
|
||||||
|
list( $network, $mask ) = explode( '/', $cidr );
|
||||||
|
|
||||||
|
// Detect IP version.
|
||||||
|
$ip_is_v6 = filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 );
|
||||||
|
$network_is_v6 = filter_var( $network, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 );
|
||||||
|
|
||||||
|
// Both must be same version.
|
||||||
|
if ( $ip_is_v6 !== $network_is_v6 ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $ip_is_v6 ) {
|
||||||
|
return self::ipv6_in_cidr( $ip, $network, (int) $mask );
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::ipv4_in_cidr( $ip, $network, (int) $mask );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if IPv4 is in CIDR range.
|
||||||
|
*
|
||||||
|
* @param string $ip IPv4 address.
|
||||||
|
* @param string $network Network address.
|
||||||
|
* @param int $mask CIDR mask.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private static function ipv4_in_cidr( $ip, $network, $mask ) {
|
||||||
|
$ip_long = ip2long( $ip );
|
||||||
|
$network_long = ip2long( $network );
|
||||||
|
|
||||||
|
if ( false === $ip_long || false === $network_long ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate network mask.
|
||||||
|
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
|
||||||
|
$network_mask = ~( ( 1 << ( 32 - $mask ) ) - 1 );
|
||||||
|
|
||||||
|
return ( $ip_long & $network_mask ) === ( $network_long & $network_mask );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if IPv6 is in CIDR range.
|
||||||
|
*
|
||||||
|
* @param string $ip IPv6 address.
|
||||||
|
* @param string $network Network address.
|
||||||
|
* @param int $mask CIDR mask.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private static function ipv6_in_cidr( $ip, $network, $mask ) {
|
||||||
|
$ip_bin = inet_pton( $ip );
|
||||||
|
$network_bin = inet_pton( $network );
|
||||||
|
|
||||||
|
if ( false === $ip_bin || false === $network_bin ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare full bytes.
|
||||||
|
$full_bytes = (int) floor( $mask / 8 );
|
||||||
|
for ( $i = 0; $i < $full_bytes; $i++ ) {
|
||||||
|
if ( $ip_bin[ $i ] !== $network_bin[ $i ] ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare remaining bits.
|
||||||
|
$remaining_bits = $mask % 8;
|
||||||
|
if ( $remaining_bits > 0 && $full_bytes < 16 ) {
|
||||||
|
$bit_mask = ( ( 1 << $remaining_bits ) - 1 ) << ( 8 - $remaining_bits );
|
||||||
|
if ( ( ord( $ip_bin[ $full_bytes ] ) & $bit_mask ) !== ( ord( $network_bin[ $full_bytes ] ) & $bit_mask ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an IP matches any rule in a list.
|
||||||
|
*
|
||||||
|
* @param string $ip IP address to check.
|
||||||
|
* @param array $rules Array of IP addresses or CIDR ranges.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function ip_matches_rules( $ip, $rules ) {
|
||||||
|
if ( empty( $rules ) || ! is_array( $rules ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( $rules as $rule ) {
|
||||||
|
$rule = trim( $rule );
|
||||||
|
if ( empty( $rule ) || strpos( $rule, '#' ) === 0 ) {
|
||||||
|
continue; // Skip empty lines and comments.
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( self::ip_in_cidr( $ip, $rule ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse IP list from textarea.
|
||||||
|
*
|
||||||
|
* @param string $text Textarea content with IPs/CIDRs.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function parse_ip_list( $text ) {
|
||||||
|
if ( empty( $text ) ) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = explode( "\n", $text );
|
||||||
|
$ips = array();
|
||||||
|
|
||||||
|
foreach ( $lines as $line ) {
|
||||||
|
$line = trim( $line );
|
||||||
|
|
||||||
|
// Skip empty lines and comments.
|
||||||
|
if ( empty( $line ) || strpos( $line, '#' ) === 0 ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove inline comments.
|
||||||
|
if ( strpos( $line, '#' ) !== false ) {
|
||||||
|
$line = trim( substr( $line, 0, strpos( $line, '#' ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate IP or CIDR.
|
||||||
|
if ( self::is_valid_ip_or_cidr( $line ) ) {
|
||||||
|
$ips[] = $line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ips;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string is a valid IP or CIDR.
|
||||||
|
*
|
||||||
|
* @param string $value Value to check.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function is_valid_ip_or_cidr( $value ) {
|
||||||
|
// Plain IP.
|
||||||
|
if ( filter_var( $value, FILTER_VALIDATE_IP ) ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CIDR notation.
|
||||||
|
if ( strpos( $value, '/' ) !== false ) {
|
||||||
|
list( $ip, $mask ) = explode( '/', $value );
|
||||||
|
|
||||||
|
if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mask = (int) $mask;
|
||||||
|
$is_ipv6 = filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 );
|
||||||
|
$max_mask = $is_ipv6 ? 128 : 32;
|
||||||
|
|
||||||
|
return $mask >= 0 && $mask <= $max_mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get country list for geo-blocking UI.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function get_countries() {
|
||||||
|
return array(
|
||||||
|
'AF' => __( 'Afghanistan', 'security-pack' ),
|
||||||
|
'AL' => __( 'Albania', 'security-pack' ),
|
||||||
|
'DZ' => __( 'Algeria', 'security-pack' ),
|
||||||
|
'AS' => __( 'American Samoa', 'security-pack' ),
|
||||||
|
'AD' => __( 'Andorra', 'security-pack' ),
|
||||||
|
'AO' => __( 'Angola', 'security-pack' ),
|
||||||
|
'AI' => __( 'Anguilla', 'security-pack' ),
|
||||||
|
'AQ' => __( 'Antarctica', 'security-pack' ),
|
||||||
|
'AG' => __( 'Antigua and Barbuda', 'security-pack' ),
|
||||||
|
'AR' => __( 'Argentina', 'security-pack' ),
|
||||||
|
'AM' => __( 'Armenia', 'security-pack' ),
|
||||||
|
'AW' => __( 'Aruba', 'security-pack' ),
|
||||||
|
'AU' => __( 'Australia', 'security-pack' ),
|
||||||
|
'AT' => __( 'Austria', 'security-pack' ),
|
||||||
|
'AZ' => __( 'Azerbaijan', 'security-pack' ),
|
||||||
|
'BS' => __( 'Bahamas', 'security-pack' ),
|
||||||
|
'BH' => __( 'Bahrain', 'security-pack' ),
|
||||||
|
'BD' => __( 'Bangladesh', 'security-pack' ),
|
||||||
|
'BB' => __( 'Barbados', 'security-pack' ),
|
||||||
|
'BY' => __( 'Belarus', 'security-pack' ),
|
||||||
|
'BE' => __( 'Belgium', 'security-pack' ),
|
||||||
|
'BZ' => __( 'Belize', 'security-pack' ),
|
||||||
|
'BJ' => __( 'Benin', 'security-pack' ),
|
||||||
|
'BM' => __( 'Bermuda', 'security-pack' ),
|
||||||
|
'BT' => __( 'Bhutan', 'security-pack' ),
|
||||||
|
'BO' => __( 'Bolivia', 'security-pack' ),
|
||||||
|
'BA' => __( 'Bosnia and Herzegovina', 'security-pack' ),
|
||||||
|
'BW' => __( 'Botswana', 'security-pack' ),
|
||||||
|
'BR' => __( 'Brazil', 'security-pack' ),
|
||||||
|
'BN' => __( 'Brunei', 'security-pack' ),
|
||||||
|
'BG' => __( 'Bulgaria', 'security-pack' ),
|
||||||
|
'BF' => __( 'Burkina Faso', 'security-pack' ),
|
||||||
|
'BI' => __( 'Burundi', 'security-pack' ),
|
||||||
|
'KH' => __( 'Cambodia', 'security-pack' ),
|
||||||
|
'CM' => __( 'Cameroon', 'security-pack' ),
|
||||||
|
'CA' => __( 'Canada', 'security-pack' ),
|
||||||
|
'CV' => __( 'Cape Verde', 'security-pack' ),
|
||||||
|
'KY' => __( 'Cayman Islands', 'security-pack' ),
|
||||||
|
'CF' => __( 'Central African Republic', 'security-pack' ),
|
||||||
|
'TD' => __( 'Chad', 'security-pack' ),
|
||||||
|
'CL' => __( 'Chile', 'security-pack' ),
|
||||||
|
'CN' => __( 'China', 'security-pack' ),
|
||||||
|
'CO' => __( 'Colombia', 'security-pack' ),
|
||||||
|
'KM' => __( 'Comoros', 'security-pack' ),
|
||||||
|
'CG' => __( 'Congo', 'security-pack' ),
|
||||||
|
'CD' => __( 'Congo (DRC)', 'security-pack' ),
|
||||||
|
'CR' => __( 'Costa Rica', 'security-pack' ),
|
||||||
|
'CI' => __( 'Ivory Coast', 'security-pack' ),
|
||||||
|
'HR' => __( 'Croatia', 'security-pack' ),
|
||||||
|
'CU' => __( 'Cuba', 'security-pack' ),
|
||||||
|
'CY' => __( 'Cyprus', 'security-pack' ),
|
||||||
|
'CZ' => __( 'Czech Republic', 'security-pack' ),
|
||||||
|
'DK' => __( 'Denmark', 'security-pack' ),
|
||||||
|
'DJ' => __( 'Djibouti', 'security-pack' ),
|
||||||
|
'DM' => __( 'Dominica', 'security-pack' ),
|
||||||
|
'DO' => __( 'Dominican Republic', 'security-pack' ),
|
||||||
|
'EC' => __( 'Ecuador', 'security-pack' ),
|
||||||
|
'EG' => __( 'Egypt', 'security-pack' ),
|
||||||
|
'SV' => __( 'El Salvador', 'security-pack' ),
|
||||||
|
'GQ' => __( 'Equatorial Guinea', 'security-pack' ),
|
||||||
|
'ER' => __( 'Eritrea', 'security-pack' ),
|
||||||
|
'EE' => __( 'Estonia', 'security-pack' ),
|
||||||
|
'ET' => __( 'Ethiopia', 'security-pack' ),
|
||||||
|
'FJ' => __( 'Fiji', 'security-pack' ),
|
||||||
|
'FI' => __( 'Finland', 'security-pack' ),
|
||||||
|
'FR' => __( 'France', 'security-pack' ),
|
||||||
|
'GA' => __( 'Gabon', 'security-pack' ),
|
||||||
|
'GM' => __( 'Gambia', 'security-pack' ),
|
||||||
|
'GE' => __( 'Georgia', 'security-pack' ),
|
||||||
|
'DE' => __( 'Germany', 'security-pack' ),
|
||||||
|
'GH' => __( 'Ghana', 'security-pack' ),
|
||||||
|
'GR' => __( 'Greece', 'security-pack' ),
|
||||||
|
'GL' => __( 'Greenland', 'security-pack' ),
|
||||||
|
'GD' => __( 'Grenada', 'security-pack' ),
|
||||||
|
'GU' => __( 'Guam', 'security-pack' ),
|
||||||
|
'GT' => __( 'Guatemala', 'security-pack' ),
|
||||||
|
'GN' => __( 'Guinea', 'security-pack' ),
|
||||||
|
'GW' => __( 'Guinea-Bissau', 'security-pack' ),
|
||||||
|
'GY' => __( 'Guyana', 'security-pack' ),
|
||||||
|
'HT' => __( 'Haiti', 'security-pack' ),
|
||||||
|
'HN' => __( 'Honduras', 'security-pack' ),
|
||||||
|
'HK' => __( 'Hong Kong', 'security-pack' ),
|
||||||
|
'HU' => __( 'Hungary', 'security-pack' ),
|
||||||
|
'IS' => __( 'Iceland', 'security-pack' ),
|
||||||
|
'IN' => __( 'India', 'security-pack' ),
|
||||||
|
'ID' => __( 'Indonesia', 'security-pack' ),
|
||||||
|
'IR' => __( 'Iran', 'security-pack' ),
|
||||||
|
'IQ' => __( 'Iraq', 'security-pack' ),
|
||||||
|
'IE' => __( 'Ireland', 'security-pack' ),
|
||||||
|
'IL' => __( 'Israel', 'security-pack' ),
|
||||||
|
'IT' => __( 'Italy', 'security-pack' ),
|
||||||
|
'JM' => __( 'Jamaica', 'security-pack' ),
|
||||||
|
'JP' => __( 'Japan', 'security-pack' ),
|
||||||
|
'JO' => __( 'Jordan', 'security-pack' ),
|
||||||
|
'KZ' => __( 'Kazakhstan', 'security-pack' ),
|
||||||
|
'KE' => __( 'Kenya', 'security-pack' ),
|
||||||
|
'KI' => __( 'Kiribati', 'security-pack' ),
|
||||||
|
'KP' => __( 'North Korea', 'security-pack' ),
|
||||||
|
'KR' => __( 'South Korea', 'security-pack' ),
|
||||||
|
'KW' => __( 'Kuwait', 'security-pack' ),
|
||||||
|
'KG' => __( 'Kyrgyzstan', 'security-pack' ),
|
||||||
|
'LA' => __( 'Laos', 'security-pack' ),
|
||||||
|
'LV' => __( 'Latvia', 'security-pack' ),
|
||||||
|
'LB' => __( 'Lebanon', 'security-pack' ),
|
||||||
|
'LS' => __( 'Lesotho', 'security-pack' ),
|
||||||
|
'LR' => __( 'Liberia', 'security-pack' ),
|
||||||
|
'LY' => __( 'Libya', 'security-pack' ),
|
||||||
|
'LI' => __( 'Liechtenstein', 'security-pack' ),
|
||||||
|
'LT' => __( 'Lithuania', 'security-pack' ),
|
||||||
|
'LU' => __( 'Luxembourg', 'security-pack' ),
|
||||||
|
'MO' => __( 'Macao', 'security-pack' ),
|
||||||
|
'MK' => __( 'North Macedonia', 'security-pack' ),
|
||||||
|
'MG' => __( 'Madagascar', 'security-pack' ),
|
||||||
|
'MW' => __( 'Malawi', 'security-pack' ),
|
||||||
|
'MY' => __( 'Malaysia', 'security-pack' ),
|
||||||
|
'MV' => __( 'Maldives', 'security-pack' ),
|
||||||
|
'ML' => __( 'Mali', 'security-pack' ),
|
||||||
|
'MT' => __( 'Malta', 'security-pack' ),
|
||||||
|
'MH' => __( 'Marshall Islands', 'security-pack' ),
|
||||||
|
'MR' => __( 'Mauritania', 'security-pack' ),
|
||||||
|
'MU' => __( 'Mauritius', 'security-pack' ),
|
||||||
|
'MX' => __( 'Mexico', 'security-pack' ),
|
||||||
|
'FM' => __( 'Micronesia', 'security-pack' ),
|
||||||
|
'MD' => __( 'Moldova', 'security-pack' ),
|
||||||
|
'MC' => __( 'Monaco', 'security-pack' ),
|
||||||
|
'MN' => __( 'Mongolia', 'security-pack' ),
|
||||||
|
'ME' => __( 'Montenegro', 'security-pack' ),
|
||||||
|
'MA' => __( 'Morocco', 'security-pack' ),
|
||||||
|
'MZ' => __( 'Mozambique', 'security-pack' ),
|
||||||
|
'MM' => __( 'Myanmar', 'security-pack' ),
|
||||||
|
'NA' => __( 'Namibia', 'security-pack' ),
|
||||||
|
'NR' => __( 'Nauru', 'security-pack' ),
|
||||||
|
'NP' => __( 'Nepal', 'security-pack' ),
|
||||||
|
'NL' => __( 'Netherlands', 'security-pack' ),
|
||||||
|
'NZ' => __( 'New Zealand', 'security-pack' ),
|
||||||
|
'NI' => __( 'Nicaragua', 'security-pack' ),
|
||||||
|
'NE' => __( 'Niger', 'security-pack' ),
|
||||||
|
'NG' => __( 'Nigeria', 'security-pack' ),
|
||||||
|
'NO' => __( 'Norway', 'security-pack' ),
|
||||||
|
'OM' => __( 'Oman', 'security-pack' ),
|
||||||
|
'PK' => __( 'Pakistan', 'security-pack' ),
|
||||||
|
'PW' => __( 'Palau', 'security-pack' ),
|
||||||
|
'PS' => __( 'Palestine', 'security-pack' ),
|
||||||
|
'PA' => __( 'Panama', 'security-pack' ),
|
||||||
|
'PG' => __( 'Papua New Guinea', 'security-pack' ),
|
||||||
|
'PY' => __( 'Paraguay', 'security-pack' ),
|
||||||
|
'PE' => __( 'Peru', 'security-pack' ),
|
||||||
|
'PH' => __( 'Philippines', 'security-pack' ),
|
||||||
|
'PL' => __( 'Poland', 'security-pack' ),
|
||||||
|
'PT' => __( 'Portugal', 'security-pack' ),
|
||||||
|
'PR' => __( 'Puerto Rico', 'security-pack' ),
|
||||||
|
'QA' => __( 'Qatar', 'security-pack' ),
|
||||||
|
'RO' => __( 'Romania', 'security-pack' ),
|
||||||
|
'RU' => __( 'Russia', 'security-pack' ),
|
||||||
|
'RW' => __( 'Rwanda', 'security-pack' ),
|
||||||
|
'SA' => __( 'Saudi Arabia', 'security-pack' ),
|
||||||
|
'SN' => __( 'Senegal', 'security-pack' ),
|
||||||
|
'RS' => __( 'Serbia', 'security-pack' ),
|
||||||
|
'SC' => __( 'Seychelles', 'security-pack' ),
|
||||||
|
'SL' => __( 'Sierra Leone', 'security-pack' ),
|
||||||
|
'SG' => __( 'Singapore', 'security-pack' ),
|
||||||
|
'SK' => __( 'Slovakia', 'security-pack' ),
|
||||||
|
'SI' => __( 'Slovenia', 'security-pack' ),
|
||||||
|
'SB' => __( 'Solomon Islands', 'security-pack' ),
|
||||||
|
'SO' => __( 'Somalia', 'security-pack' ),
|
||||||
|
'ZA' => __( 'South Africa', 'security-pack' ),
|
||||||
|
'SS' => __( 'South Sudan', 'security-pack' ),
|
||||||
|
'ES' => __( 'Spain', 'security-pack' ),
|
||||||
|
'LK' => __( 'Sri Lanka', 'security-pack' ),
|
||||||
|
'SD' => __( 'Sudan', 'security-pack' ),
|
||||||
|
'SR' => __( 'Suriname', 'security-pack' ),
|
||||||
|
'SZ' => __( 'Eswatini', 'security-pack' ),
|
||||||
|
'SE' => __( 'Sweden', 'security-pack' ),
|
||||||
|
'CH' => __( 'Switzerland', 'security-pack' ),
|
||||||
|
'SY' => __( 'Syria', 'security-pack' ),
|
||||||
|
'TW' => __( 'Taiwan', 'security-pack' ),
|
||||||
|
'TJ' => __( 'Tajikistan', 'security-pack' ),
|
||||||
|
'TZ' => __( 'Tanzania', 'security-pack' ),
|
||||||
|
'TH' => __( 'Thailand', 'security-pack' ),
|
||||||
|
'TL' => __( 'Timor-Leste', 'security-pack' ),
|
||||||
|
'TG' => __( 'Togo', 'security-pack' ),
|
||||||
|
'TO' => __( 'Tonga', 'security-pack' ),
|
||||||
|
'TT' => __( 'Trinidad and Tobago', 'security-pack' ),
|
||||||
|
'TN' => __( 'Tunisia', 'security-pack' ),
|
||||||
|
'TR' => __( 'Turkey', 'security-pack' ),
|
||||||
|
'TM' => __( 'Turkmenistan', 'security-pack' ),
|
||||||
|
'TV' => __( 'Tuvalu', 'security-pack' ),
|
||||||
|
'UG' => __( 'Uganda', 'security-pack' ),
|
||||||
|
'UA' => __( 'Ukraine', 'security-pack' ),
|
||||||
|
'AE' => __( 'United Arab Emirates', 'security-pack' ),
|
||||||
|
'GB' => __( 'United Kingdom', 'security-pack' ),
|
||||||
|
'US' => __( 'United States', 'security-pack' ),
|
||||||
|
'UY' => __( 'Uruguay', 'security-pack' ),
|
||||||
|
'UZ' => __( 'Uzbekistan', 'security-pack' ),
|
||||||
|
'VU' => __( 'Vanuatu', 'security-pack' ),
|
||||||
|
'VE' => __( 'Venezuela', 'security-pack' ),
|
||||||
|
'VN' => __( 'Vietnam', 'security-pack' ),
|
||||||
|
'YE' => __( 'Yemen', 'security-pack' ),
|
||||||
|
'ZM' => __( 'Zambia', 'security-pack' ),
|
||||||
|
'ZW' => __( 'Zimbabwe', 'security-pack' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize a custom login URL slug.
|
||||||
|
*
|
||||||
|
* @param string $slug The slug to sanitize.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function sanitize_login_slug( $slug ) {
|
||||||
|
$slug = sanitize_title( $slug );
|
||||||
|
$slug = preg_replace( '/[^a-z0-9\-]/', '', $slug );
|
||||||
|
|
||||||
|
// Prevent common reserved slugs.
|
||||||
|
$reserved = array( 'wp-admin', 'wp-login', 'admin', 'login', 'wp-content', 'wp-includes' );
|
||||||
|
if ( in_array( $slug, $reserved, true ) ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the server's public IP address.
|
||||||
|
*
|
||||||
|
* Uses external service to determine the server's outbound IP.
|
||||||
|
* Result is cached for 1 hour to avoid excessive external requests.
|
||||||
|
*
|
||||||
|
* @return string|null Server IP or null on failure.
|
||||||
|
*/
|
||||||
|
public static function get_server_ip() {
|
||||||
|
$cached = get_transient( 'wpsp_server_ip' );
|
||||||
|
if ( false !== $cached ) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try multiple services for reliability.
|
||||||
|
$services = array(
|
||||||
|
'https://api.ipify.org',
|
||||||
|
'https://ifconfig.me/ip',
|
||||||
|
'https://icanhazip.com',
|
||||||
|
);
|
||||||
|
|
||||||
|
$ip = null;
|
||||||
|
|
||||||
|
foreach ( $services as $service ) {
|
||||||
|
$response = wp_remote_get(
|
||||||
|
$service,
|
||||||
|
array(
|
||||||
|
'timeout' => 5,
|
||||||
|
'sslverify' => true,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) ) {
|
||||||
|
$body = trim( wp_remote_retrieve_body( $response ) );
|
||||||
|
if ( filter_var( $body, FILTER_VALIDATE_IP ) ) {
|
||||||
|
$ip = $body;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $ip ) {
|
||||||
|
// Cache for 1 hour.
|
||||||
|
set_transient( 'wpsp_server_ip', $ip, HOUR_IN_SECONDS );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get country flag emoji from country code.
|
||||||
|
*
|
||||||
|
* Converts ISO 3166-1 alpha-2 country codes to Unicode flag emojis.
|
||||||
|
*
|
||||||
|
* @param string $country_code Two-letter country code (e.g., "US", "GB").
|
||||||
|
* @return string Flag emoji or globe emoji for invalid codes.
|
||||||
|
*/
|
||||||
|
public static function get_country_flag( $country_code ) {
|
||||||
|
if ( empty( $country_code ) || strlen( $country_code ) !== 2 ) {
|
||||||
|
return '🌐'; // Globe emoji for invalid codes.
|
||||||
|
}
|
||||||
|
|
||||||
|
$country_code = strtoupper( $country_code );
|
||||||
|
|
||||||
|
// Convert country code to Unicode regional indicator symbols.
|
||||||
|
// Regional indicators are U+1F1E6 (A) through U+1F1FF (Z).
|
||||||
|
$first_letter = mb_chr( ord( $country_code[0] ) - ord( 'A' ) + 0x1F1E6, 'UTF-8' );
|
||||||
|
$second_letter = mb_chr( ord( $country_code[1] ) - ord( 'A' ) + 0x1F1E6, 'UTF-8' );
|
||||||
|
|
||||||
|
return $first_letter . $second_letter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get countries array with flags.
|
||||||
|
*
|
||||||
|
* @return array Country code => "Flag Name" format.
|
||||||
|
*/
|
||||||
|
public static function get_countries_with_flags() {
|
||||||
|
$countries = self::get_countries();
|
||||||
|
$countries_flags = array();
|
||||||
|
|
||||||
|
foreach ( $countries as $code => $name ) {
|
||||||
|
$flag = self::get_country_flag( $code );
|
||||||
|
$countries_flags[ $code ] = $flag . ' ' . $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $countries_flags;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* IP access control for WP Security Pack.
|
* IP access control for Security Pack.
|
||||||
*
|
*
|
||||||
* @package WP_Security_Pack
|
* @package Security_Pack
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Prevent direct access.
|
// Prevent direct access.
|
||||||
@@ -55,14 +55,14 @@ class WPSP_IP_Control {
|
|||||||
|
|
||||||
// Block blacklisted IPs.
|
// Block blacklisted IPs.
|
||||||
if ( $this->is_blacklisted( $ip ) ) {
|
if ( $this->is_blacklisted( $ip ) ) {
|
||||||
WPSP_Activity_Log::log( WPSP_Activity_Log::EVENT_IP_BLOCKED, $ip, null, __( 'IP blacklisted', 'wp-security-pack' ) );
|
WPSP_Activity_Log::log( WPSP_Activity_Log::EVENT_IP_BLOCKED, $ip, null, __( 'IP blacklisted', 'security-pack' ) );
|
||||||
$this->block_access( __( 'Your IP address has been blocked.', 'wp-security-pack' ) );
|
$this->block_access( __( 'Your IP address has been blocked.', 'security-pack' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check auto-blocked IPs.
|
// Check auto-blocked IPs.
|
||||||
if ( $this->is_auto_blocked( $ip ) ) {
|
if ( $this->is_auto_blocked( $ip ) ) {
|
||||||
WPSP_Activity_Log::log( WPSP_Activity_Log::EVENT_IP_BLOCKED, $ip, null, __( 'IP auto-blocked', 'wp-security-pack' ) );
|
WPSP_Activity_Log::log( WPSP_Activity_Log::EVENT_IP_BLOCKED, $ip, null, __( 'IP auto-blocked', 'security-pack' ) );
|
||||||
$this->block_access( __( 'Your IP address has been temporarily blocked due to suspicious activity.', 'wp-security-pack' ) );
|
$this->block_access( __( 'Your IP address has been temporarily blocked due to suspicious activity.', 'security-pack' ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,15 +97,13 @@ class WPSP_IP_Control {
|
|||||||
public function is_auto_blocked( $ip ) {
|
public function is_auto_blocked( $ip ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$table = WPSP_DB::get_lockout_table();
|
$max_attempts = (int) Security_Pack::get_setting( 'login_max_attempts', 5 );
|
||||||
$max_attempts = (int) WP_Security_Pack::get_setting( 'login_max_attempts', 5 );
|
$lockout_minutes = (int) Security_Pack::get_setting( 'login_lockout_duration', 15 );
|
||||||
$lockout_minutes = (int) WP_Security_Pack::get_setting( 'login_lockout_duration', 15 );
|
|
||||||
|
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
|
||||||
$lockout = $wpdb->get_row(
|
$lockout = $wpdb->get_row(
|
||||||
$wpdb->prepare(
|
$wpdb->prepare(
|
||||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
"SELECT * FROM {$wpdb->prefix}wpsp_lockouts WHERE ip_address = %s",
|
||||||
"SELECT * FROM {$table} WHERE ip_address = %s",
|
|
||||||
$ip
|
$ip
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -143,7 +141,7 @@ class WPSP_IP_Control {
|
|||||||
*/
|
*/
|
||||||
public function get_whitelist() {
|
public function get_whitelist() {
|
||||||
if ( null === $this->whitelist_cache ) {
|
if ( null === $this->whitelist_cache ) {
|
||||||
$whitelist_text = WP_Security_Pack::get_setting( 'ip_whitelist', '' );
|
$whitelist_text = Security_Pack::get_setting( 'ip_whitelist', '' );
|
||||||
$this->whitelist_cache = WPSP_Helper::parse_ip_list( $whitelist_text );
|
$this->whitelist_cache = WPSP_Helper::parse_ip_list( $whitelist_text );
|
||||||
}
|
}
|
||||||
return $this->whitelist_cache;
|
return $this->whitelist_cache;
|
||||||
@@ -156,7 +154,7 @@ class WPSP_IP_Control {
|
|||||||
*/
|
*/
|
||||||
public function get_blacklist() {
|
public function get_blacklist() {
|
||||||
if ( null === $this->blacklist_cache ) {
|
if ( null === $this->blacklist_cache ) {
|
||||||
$blacklist_text = WP_Security_Pack::get_setting( 'ip_blacklist', '' );
|
$blacklist_text = Security_Pack::get_setting( 'ip_blacklist', '' );
|
||||||
$this->blacklist_cache = WPSP_Helper::parse_ip_list( $blacklist_text );
|
$this->blacklist_cache = WPSP_Helper::parse_ip_list( $blacklist_text );
|
||||||
}
|
}
|
||||||
return $this->blacklist_cache;
|
return $this->blacklist_cache;
|
||||||
@@ -181,25 +179,23 @@ class WPSP_IP_Control {
|
|||||||
public function auto_block_ip( $ip, $duration = 15, $reason = '' ) {
|
public function auto_block_ip( $ip, $duration = 15, $reason = '' ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$table = WPSP_DB::get_lockout_table();
|
|
||||||
// Store as Unix timestamp for consistency with lockout_ip().
|
// Store as Unix timestamp for consistency with lockout_ip().
|
||||||
$lockout_until = time() + ( $duration * 60 );
|
$lockout_until = time() + ( $duration * 60 );
|
||||||
|
|
||||||
// Check if already exists.
|
// Check if already exists.
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
|
||||||
$existing = $wpdb->get_var(
|
$existing = $wpdb->get_var(
|
||||||
$wpdb->prepare(
|
$wpdb->prepare(
|
||||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
"SELECT id FROM {$wpdb->prefix}wpsp_lockouts WHERE ip_address = %s",
|
||||||
"SELECT id FROM {$table} WHERE ip_address = %s",
|
|
||||||
$ip
|
$ip
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if ( $existing ) {
|
if ( $existing ) {
|
||||||
// Update existing record.
|
// Update existing record.
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
|
||||||
return false !== $wpdb->update(
|
return false !== $wpdb->update(
|
||||||
$table,
|
$wpdb->prefix . 'wpsp_lockouts',
|
||||||
array(
|
array(
|
||||||
'lockout_until' => $lockout_until,
|
'lockout_until' => $lockout_until,
|
||||||
'updated_at' => current_time( 'mysql' ),
|
'updated_at' => current_time( 'mysql' ),
|
||||||
@@ -211,9 +207,9 @@ class WPSP_IP_Control {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Insert new record.
|
// Insert new record.
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
|
||||||
return false !== $wpdb->insert(
|
return false !== $wpdb->insert(
|
||||||
$table,
|
$wpdb->prefix . 'wpsp_lockouts',
|
||||||
array(
|
array(
|
||||||
'ip_address' => $ip,
|
'ip_address' => $ip,
|
||||||
'failed_attempts' => 0,
|
'failed_attempts' => 0,
|
||||||
@@ -234,11 +230,9 @@ class WPSP_IP_Control {
|
|||||||
public function unblock_ip( $ip ) {
|
public function unblock_ip( $ip ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$table = WPSP_DB::get_lockout_table();
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
|
||||||
|
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
|
||||||
return false !== $wpdb->delete(
|
return false !== $wpdb->delete(
|
||||||
$table,
|
$wpdb->prefix . 'wpsp_lockouts',
|
||||||
array( 'ip_address' => $ip ),
|
array( 'ip_address' => $ip ),
|
||||||
array( '%s' )
|
array( '%s' )
|
||||||
);
|
);
|
||||||
@@ -252,17 +246,13 @@ class WPSP_IP_Control {
|
|||||||
public function get_blocked_ips() {
|
public function get_blocked_ips() {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$table = WPSP_DB::get_lockout_table();
|
$max_attempts = (int) Security_Pack::get_setting( 'login_max_attempts', 5 );
|
||||||
$max_attempts = (int) WP_Security_Pack::get_setting( 'login_max_attempts', 5 );
|
$lockout_minutes = (int) Security_Pack::get_setting( 'login_lockout_duration', 15 );
|
||||||
$lockout_minutes = (int) WP_Security_Pack::get_setting( 'login_lockout_duration', 15 );
|
|
||||||
$current_time = time();
|
$current_time = time();
|
||||||
|
|
||||||
// Get all lockout records.
|
// Get all lockout records.
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
|
||||||
$lockouts = $wpdb->get_results(
|
$lockouts = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}wpsp_lockouts ORDER BY updated_at DESC" );
|
||||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
||||||
"SELECT * FROM {$table} ORDER BY updated_at DESC"
|
|
||||||
);
|
|
||||||
|
|
||||||
$blocked = array();
|
$blocked = array();
|
||||||
foreach ( $lockouts as $lockout ) {
|
foreach ( $lockouts as $lockout ) {
|
||||||
@@ -316,7 +306,7 @@ class WPSP_IP_Control {
|
|||||||
// Simple blocked page.
|
// Simple blocked page.
|
||||||
wp_die(
|
wp_die(
|
||||||
esc_html( $message ),
|
esc_html( $message ),
|
||||||
esc_html__( 'Access Denied', 'wp-security-pack' ),
|
esc_html__( 'Access Denied', 'security-pack' ),
|
||||||
array(
|
array(
|
||||||
'response' => 403,
|
'response' => 403,
|
||||||
'back_link' => false,
|
'back_link' => false,
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* IP2Location database reader for WP Security Pack.
|
* IP2Location database reader for Security Pack.
|
||||||
*
|
*
|
||||||
* Reads IP2Location BIN format databases.
|
* Reads IP2Location BIN format databases.
|
||||||
*
|
*
|
||||||
* @package WP_Security_Pack
|
* @package Security_Pack
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Prevent direct access.
|
// Prevent direct access.
|
||||||
@@ -288,11 +288,15 @@ class WPSP_IP2Location {
|
|||||||
/**
|
/**
|
||||||
* Read a byte from database.
|
* Read a byte from database.
|
||||||
*
|
*
|
||||||
|
* Note: Direct file operations are required for reading the binary IP2Location database format.
|
||||||
|
* WP_Filesystem is not suitable for binary file parsing.
|
||||||
|
*
|
||||||
* @param int $pos Position.
|
* @param int $pos Position.
|
||||||
* @return int
|
* @return int
|
||||||
*/
|
*/
|
||||||
private function read_byte( $pos ) {
|
private function read_byte( $pos ) {
|
||||||
fseek( $this->handle, $pos - 1, SEEK_SET );
|
fseek( $this->handle, $pos - 1, SEEK_SET );
|
||||||
|
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- Binary database parsing requires direct file access.
|
||||||
$data = fread( $this->handle, 1 );
|
$data = fread( $this->handle, 1 );
|
||||||
return unpack( 'C', $data )[1];
|
return unpack( 'C', $data )[1];
|
||||||
}
|
}
|
||||||
@@ -305,6 +309,7 @@ class WPSP_IP2Location {
|
|||||||
*/
|
*/
|
||||||
private function read_word( $pos ) {
|
private function read_word( $pos ) {
|
||||||
fseek( $this->handle, $pos - 1, SEEK_SET );
|
fseek( $this->handle, $pos - 1, SEEK_SET );
|
||||||
|
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- Binary database parsing requires direct file access.
|
||||||
$data = fread( $this->handle, 4 );
|
$data = fread( $this->handle, 4 );
|
||||||
return unpack( 'V', $data )[1];
|
return unpack( 'V', $data )[1];
|
||||||
}
|
}
|
||||||
@@ -317,7 +322,9 @@ class WPSP_IP2Location {
|
|||||||
*/
|
*/
|
||||||
private function read_string( $pos ) {
|
private function read_string( $pos ) {
|
||||||
fseek( $this->handle, $pos, SEEK_SET );
|
fseek( $this->handle, $pos, SEEK_SET );
|
||||||
|
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- Binary database parsing requires direct file access.
|
||||||
$length = unpack( 'C', fread( $this->handle, 1 ) )[1];
|
$length = unpack( 'C', fread( $this->handle, 1 ) )[1];
|
||||||
|
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- Binary database parsing requires direct file access.
|
||||||
return fread( $this->handle, $length );
|
return fread( $this->handle, $length );
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,6 +336,7 @@ class WPSP_IP2Location {
|
|||||||
*/
|
*/
|
||||||
private function read_ipv6( $pos ) {
|
private function read_ipv6( $pos ) {
|
||||||
fseek( $this->handle, $pos - 1, SEEK_SET );
|
fseek( $this->handle, $pos - 1, SEEK_SET );
|
||||||
|
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- Binary database parsing requires direct file access.
|
||||||
$data = fread( $this->handle, 16 );
|
$data = fread( $this->handle, 16 );
|
||||||
$int = unpack( 'V4', $data );
|
$int = unpack( 'V4', $data );
|
||||||
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Login protection for WP Security Pack.
|
* Login protection for Security Pack.
|
||||||
*
|
*
|
||||||
* @package WP_Security_Pack
|
* @package Security_Pack
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Prevent direct access.
|
// Prevent direct access.
|
||||||
@@ -48,7 +48,7 @@ class WPSP_Login_Protection {
|
|||||||
// Honeypot field.
|
// Honeypot field.
|
||||||
// NOTE: Must be registered BEFORE handle_custom_login_slug_early() which
|
// NOTE: Must be registered BEFORE handle_custom_login_slug_early() which
|
||||||
// may load wp-login.php and exit, preventing any later hook registrations.
|
// may load wp-login.php and exit, preventing any later hook registrations.
|
||||||
if ( WP_Security_Pack::get_setting( 'honeypot_enabled', true ) ) {
|
if ( Security_Pack::get_setting( 'honeypot_enabled', true ) ) {
|
||||||
add_action( 'login_form', array( $this, 'add_honeypot_field' ) );
|
add_action( 'login_form', array( $this, 'add_honeypot_field' ) );
|
||||||
add_action( 'register_form', array( $this, 'add_honeypot_field' ) );
|
add_action( 'register_form', array( $this, 'add_honeypot_field' ) );
|
||||||
add_filter( 'authenticate', array( $this, 'check_honeypot' ), 1, 3 );
|
add_filter( 'authenticate', array( $this, 'check_honeypot' ), 1, 3 );
|
||||||
@@ -58,7 +58,7 @@ class WPSP_Login_Protection {
|
|||||||
// Hide login error messages (prevents username enumeration).
|
// Hide login error messages (prevents username enumeration).
|
||||||
// NOTE: Must be registered BEFORE handle_custom_login_slug_early() which
|
// NOTE: Must be registered BEFORE handle_custom_login_slug_early() which
|
||||||
// may load wp-login.php and exit, preventing any later hook registrations.
|
// may load wp-login.php and exit, preventing any later hook registrations.
|
||||||
if ( WP_Security_Pack::get_setting( 'hide_login_errors', true ) ) {
|
if ( Security_Pack::get_setting( 'hide_login_errors', true ) ) {
|
||||||
// Replace specific auth errors at the source (runs after WP's authenticate at priority 20).
|
// Replace specific auth errors at the source (runs after WP's authenticate at priority 20).
|
||||||
add_filter( 'authenticate', array( $this, 'genericize_auth_error' ), PHP_INT_MAX, 3 );
|
add_filter( 'authenticate', array( $this, 'genericize_auth_error' ), PHP_INT_MAX, 3 );
|
||||||
// Also filter the rendered error string as a safety net.
|
// Also filter the rendered error string as a safety net.
|
||||||
@@ -68,8 +68,8 @@ class WPSP_Login_Protection {
|
|||||||
// Custom login URL - must run very early before WordPress processes the request.
|
// Custom login URL - must run very early before WordPress processes the request.
|
||||||
// WARNING: handle_custom_login_slug_early() may require wp-login.php and exit.
|
// WARNING: handle_custom_login_slug_early() may require wp-login.php and exit.
|
||||||
// All login-related hooks MUST be registered above this point.
|
// All login-related hooks MUST be registered above this point.
|
||||||
if ( WP_Security_Pack::get_setting( 'login_rename_enabled', false ) ) {
|
if ( Security_Pack::get_setting( 'login_rename_enabled', false ) ) {
|
||||||
$this->custom_login_slug = WP_Security_Pack::get_setting( 'login_custom_url', '' );
|
$this->custom_login_slug = Security_Pack::get_setting( 'login_custom_url', '' );
|
||||||
if ( ! empty( $this->custom_login_slug ) ) {
|
if ( ! empty( $this->custom_login_slug ) ) {
|
||||||
// Handle custom login slug immediately (constructor runs during plugins_loaded).
|
// Handle custom login slug immediately (constructor runs during plugins_loaded).
|
||||||
$this->handle_custom_login_slug_early();
|
$this->handle_custom_login_slug_early();
|
||||||
@@ -83,7 +83,7 @@ class WPSP_Login_Protection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Hide wp-admin for non-logged-in users.
|
// Hide wp-admin for non-logged-in users.
|
||||||
if ( WP_Security_Pack::get_setting( 'hide_wp_admin', false ) ) {
|
if ( Security_Pack::get_setting( 'hide_wp_admin', false ) ) {
|
||||||
// Must run early - WordPress redirects wp-admin to wp-login.php before init.
|
// Must run early - WordPress redirects wp-admin to wp-login.php before init.
|
||||||
$this->block_wp_admin_early();
|
$this->block_wp_admin_early();
|
||||||
add_action( 'init', array( $this, 'hide_wp_admin' ), 1 );
|
add_action( 'init', array( $this, 'hide_wp_admin' ), 1 );
|
||||||
@@ -118,11 +118,11 @@ class WPSP_Login_Protection {
|
|||||||
*/
|
*/
|
||||||
private function should_block_direct_wp_login() {
|
private function should_block_direct_wp_login() {
|
||||||
// Only applies when custom login URL is enabled.
|
// Only applies when custom login URL is enabled.
|
||||||
if ( ! WP_Security_Pack::get_setting( 'login_rename_enabled', false ) ) {
|
if ( ! Security_Pack::get_setting( 'login_rename_enabled', false ) ) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$custom_slug = WP_Security_Pack::get_setting( 'login_custom_url', '' );
|
$custom_slug = Security_Pack::get_setting( 'login_custom_url', '' );
|
||||||
if ( empty( $custom_slug ) ) {
|
if ( empty( $custom_slug ) ) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -142,7 +142,7 @@ class WPSP_Login_Protection {
|
|||||||
// Allow if user has the access cookie (visited custom login URL before).
|
// Allow if user has the access cookie (visited custom login URL before).
|
||||||
// This allows all WordPress flows (password reset, etc.) to work normally.
|
// This allows all WordPress flows (password reset, etc.) to work normally.
|
||||||
$expected_value = wp_hash( 'wpsp_login_' . $custom_slug );
|
$expected_value = wp_hash( 'wpsp_login_' . $custom_slug );
|
||||||
$cookie_value = isset( $_COOKIE['wpsp_login_access'] ) ? $_COOKIE['wpsp_login_access'] : '';
|
$cookie_value = isset( $_COOKIE['wpsp_login_access'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['wpsp_login_access'] ) ) : '';
|
||||||
|
|
||||||
if ( $cookie_value === $expected_value ) {
|
if ( $cookie_value === $expected_value ) {
|
||||||
return false;
|
return false;
|
||||||
@@ -158,7 +158,7 @@ class WPSP_Login_Protection {
|
|||||||
* @return bool True if access should be denied.
|
* @return bool True if access should be denied.
|
||||||
*/
|
*/
|
||||||
private function is_admin_access_restricted() {
|
private function is_admin_access_restricted() {
|
||||||
if ( ! WP_Security_Pack::get_setting( 'admin_access_restriction', false ) ) {
|
if ( ! Security_Pack::get_setting( 'admin_access_restriction', false ) ) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,8 +176,8 @@ class WPSP_Login_Protection {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$allowed_countries = WP_Security_Pack::get_setting( 'admin_allowed_countries', array() );
|
$allowed_countries = Security_Pack::get_setting( 'admin_allowed_countries', array() );
|
||||||
$allowed_ips = WP_Security_Pack::get_setting( 'admin_allowed_ips', '' );
|
$allowed_ips = Security_Pack::get_setting( 'admin_allowed_ips', '' );
|
||||||
$allowed_ip_list = WPSP_Helper::parse_ip_list( $allowed_ips );
|
$allowed_ip_list = WPSP_Helper::parse_ip_list( $allowed_ips );
|
||||||
|
|
||||||
$has_country_restriction = ! empty( $allowed_countries );
|
$has_country_restriction = ! empty( $allowed_countries );
|
||||||
@@ -223,15 +223,15 @@ class WPSP_Login_Protection {
|
|||||||
WPSP_Activity_Log::EVENT_IP_BLOCKED,
|
WPSP_Activity_Log::EVENT_IP_BLOCKED,
|
||||||
$ip,
|
$ip,
|
||||||
null,
|
null,
|
||||||
__( 'Admin access denied: country/IP not allowed', 'wp-security-pack' )
|
__( 'Admin access denied: country/IP not allowed', 'security-pack' )
|
||||||
);
|
);
|
||||||
|
|
||||||
status_header( 403 );
|
status_header( 403 );
|
||||||
nocache_headers();
|
nocache_headers();
|
||||||
|
|
||||||
wp_die(
|
wp_die(
|
||||||
esc_html__( 'Admin access is not permitted from your location.', 'wp-security-pack' ),
|
esc_html__( 'Admin access is not permitted from your location.', 'security-pack' ),
|
||||||
esc_html__( 'Access Denied', 'wp-security-pack' ),
|
esc_html__( 'Access Denied', 'security-pack' ),
|
||||||
array(
|
array(
|
||||||
'response' => 403,
|
'response' => 403,
|
||||||
'back_link' => false,
|
'back_link' => false,
|
||||||
@@ -245,7 +245,7 @@ class WPSP_Login_Protection {
|
|||||||
* @param string $username Username attempted.
|
* @param string $username Username attempted.
|
||||||
*/
|
*/
|
||||||
public function handle_failed_login( $username ) {
|
public function handle_failed_login( $username ) {
|
||||||
if ( ! WP_Security_Pack::get_setting( 'login_limit_enabled', true ) ) {
|
if ( ! Security_Pack::get_setting( 'login_limit_enabled', true ) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +258,7 @@ class WPSP_Login_Protection {
|
|||||||
// Check if IP is whitelisted.
|
// Check if IP is whitelisted.
|
||||||
$ip_control = wpsp()->get_component( 'ip_control' );
|
$ip_control = wpsp()->get_component( 'ip_control' );
|
||||||
if ( $ip_control && $ip_control->is_whitelisted( $ip ) ) {
|
if ( $ip_control && $ip_control->is_whitelisted( $ip ) ) {
|
||||||
WPSP_Activity_Log::log( WPSP_Activity_Log::EVENT_LOGIN_FAILED, $ip, $username, __( 'Failed login (whitelisted IP)', 'wp-security-pack' ) );
|
WPSP_Activity_Log::log( WPSP_Activity_Log::EVENT_LOGIN_FAILED, $ip, $username, __( 'Failed login (whitelisted IP)', 'security-pack' ) );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,7 +269,7 @@ class WPSP_Login_Protection {
|
|||||||
$attempts = $this->increment_failed_attempts( $ip );
|
$attempts = $this->increment_failed_attempts( $ip );
|
||||||
|
|
||||||
// Check if lockout threshold reached.
|
// Check if lockout threshold reached.
|
||||||
$max_attempts = WP_Security_Pack::get_setting( 'login_max_attempts', 5 );
|
$max_attempts = Security_Pack::get_setting( 'login_max_attempts', 5 );
|
||||||
|
|
||||||
if ( $attempts >= $max_attempts ) {
|
if ( $attempts >= $max_attempts ) {
|
||||||
$this->lockout_ip( $ip );
|
$this->lockout_ip( $ip );
|
||||||
@@ -295,7 +295,7 @@ class WPSP_Login_Protection {
|
|||||||
$this->clear_failed_attempts( $ip );
|
$this->clear_failed_attempts( $ip );
|
||||||
|
|
||||||
// Send admin login notification if enabled.
|
// Send admin login notification if enabled.
|
||||||
if ( WP_Security_Pack::get_setting( 'admin_login_notify', false ) && user_can( $user, 'manage_options' ) ) {
|
if ( Security_Pack::get_setting( 'admin_login_notify', false ) && user_can( $user, 'manage_options' ) ) {
|
||||||
$this->send_admin_login_notification( $user, $ip );
|
$this->send_admin_login_notification( $user, $ip );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -307,7 +307,7 @@ class WPSP_Login_Protection {
|
|||||||
* @param string $ip IP address.
|
* @param string $ip IP address.
|
||||||
*/
|
*/
|
||||||
private function send_admin_login_notification( $user, $ip ) {
|
private function send_admin_login_notification( $user, $ip ) {
|
||||||
$email = WP_Security_Pack::get_setting( 'email_alerts_address', get_option( 'admin_email' ) );
|
$email = Security_Pack::get_setting( 'email_alerts_address', get_option( 'admin_email' ) );
|
||||||
if ( empty( $email ) ) {
|
if ( empty( $email ) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -336,21 +336,24 @@ class WPSP_Login_Protection {
|
|||||||
$site_name = get_bloginfo( 'name' );
|
$site_name = get_bloginfo( 'name' );
|
||||||
$subject = sprintf(
|
$subject = sprintf(
|
||||||
/* translators: %s: Site name */
|
/* translators: %s: Site name */
|
||||||
__( '[%s] Admin Login from New IP', 'wp-security-pack' ),
|
__( '[%s] Admin Login from New IP', 'security-pack' ),
|
||||||
$site_name
|
$site_name
|
||||||
);
|
);
|
||||||
|
|
||||||
$message = sprintf(
|
$message = sprintf(
|
||||||
/* translators: 1: Username, 2: Site name */
|
/* translators: 1: Username, 2: Site name */
|
||||||
__( 'An administrator account logged in to %2$s from a new IP address.', 'wp-security-pack' ),
|
__( 'An administrator account logged in to %2$s from a new IP address.', 'security-pack' ),
|
||||||
$user->user_login,
|
$user->user_login,
|
||||||
$site_name
|
$site_name
|
||||||
) . "\n\n";
|
) . "\n\n";
|
||||||
|
|
||||||
$message .= sprintf( __( 'Username: %s', 'wp-security-pack' ), $user->user_login ) . "\n";
|
/* translators: %s: Username */
|
||||||
$message .= sprintf( __( 'IP Address: %s', 'wp-security-pack' ), $ip ) . "\n";
|
$message .= sprintf( __( 'Username: %s', 'security-pack' ), $user->user_login ) . "\n";
|
||||||
$message .= sprintf( __( 'Time: %s', 'wp-security-pack' ), current_time( 'mysql' ) ) . "\n\n";
|
/* translators: %s: IP address */
|
||||||
$message .= __( 'If this was not you, please secure your account immediately.', 'wp-security-pack' ) . "\n";
|
$message .= sprintf( __( 'IP Address: %s', 'security-pack' ), $ip ) . "\n";
|
||||||
|
/* translators: %s: Login time */
|
||||||
|
$message .= sprintf( __( 'Time: %s', 'security-pack' ), current_time( 'mysql' ) ) . "\n\n";
|
||||||
|
$message .= __( 'If this was not you, please secure your account immediately.', 'security-pack' ) . "\n";
|
||||||
|
|
||||||
wp_mail( $email, $subject, $message );
|
wp_mail( $email, $subject, $message );
|
||||||
}
|
}
|
||||||
@@ -363,7 +366,7 @@ class WPSP_Login_Protection {
|
|||||||
*/
|
*/
|
||||||
public function hide_login_errors( $error ) {
|
public function hide_login_errors( $error ) {
|
||||||
// Return generic message instead of revealing if username or password was wrong.
|
// Return generic message instead of revealing if username or password was wrong.
|
||||||
return __( 'Invalid username or password.', 'wp-security-pack' );
|
return __( 'Invalid username or password.', 'security-pack' );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -385,7 +388,7 @@ class WPSP_Login_Protection {
|
|||||||
if ( in_array( $user->get_error_code(), $specific_codes, true ) ) {
|
if ( in_array( $user->get_error_code(), $specific_codes, true ) ) {
|
||||||
return new WP_Error(
|
return new WP_Error(
|
||||||
'authentication_failed',
|
'authentication_failed',
|
||||||
__( 'Invalid username or password.', 'wp-security-pack' )
|
__( 'Invalid username or password.', 'security-pack' )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -402,7 +405,7 @@ class WPSP_Login_Protection {
|
|||||||
* @return WP_User|WP_Error|null
|
* @return WP_User|WP_Error|null
|
||||||
*/
|
*/
|
||||||
public function check_lockout( $user, $username, $password ) {
|
public function check_lockout( $user, $username, $password ) {
|
||||||
if ( ! WP_Security_Pack::get_setting( 'login_limit_enabled', true ) ) {
|
if ( ! Security_Pack::get_setting( 'login_limit_enabled', true ) ) {
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,8 +427,8 @@ class WPSP_Login_Protection {
|
|||||||
|
|
||||||
// Check if IP is locked out.
|
// Check if IP is locked out.
|
||||||
$lockout = $this->get_lockout( $ip );
|
$lockout = $this->get_lockout( $ip );
|
||||||
$max_attempts = (int) WP_Security_Pack::get_setting( 'login_max_attempts', 5 );
|
$max_attempts = (int) Security_Pack::get_setting( 'login_max_attempts', 5 );
|
||||||
$lockout_duration = (int) WP_Security_Pack::get_setting( 'login_lockout_duration', 15 );
|
$lockout_duration = (int) Security_Pack::get_setting( 'login_lockout_duration', 15 );
|
||||||
|
|
||||||
if ( $lockout && (int) $lockout->failed_attempts >= $max_attempts ) {
|
if ( $lockout && (int) $lockout->failed_attempts >= $max_attempts ) {
|
||||||
// Calculate lockout expiry from updated_at + duration.
|
// Calculate lockout expiry from updated_at + duration.
|
||||||
@@ -442,7 +445,7 @@ class WPSP_Login_Protection {
|
|||||||
'wpsp_locked_out',
|
'wpsp_locked_out',
|
||||||
sprintf(
|
sprintf(
|
||||||
/* translators: %s: Time remaining */
|
/* translators: %s: Time remaining */
|
||||||
__( 'Too many failed login attempts. Please try again in %s.', 'wp-security-pack' ),
|
__( 'Too many failed login attempts. Please try again in %s.', 'security-pack' ),
|
||||||
$remaining
|
$remaining
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -464,13 +467,10 @@ class WPSP_Login_Protection {
|
|||||||
private function increment_failed_attempts( $ip ) {
|
private function increment_failed_attempts( $ip ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$table = WPSP_DB::get_lockout_table();
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
|
||||||
|
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
|
||||||
$existing = $wpdb->get_row(
|
$existing = $wpdb->get_row(
|
||||||
$wpdb->prepare(
|
$wpdb->prepare(
|
||||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
"SELECT * FROM {$wpdb->prefix}wpsp_lockouts WHERE ip_address = %s",
|
||||||
"SELECT * FROM {$table} WHERE ip_address = %s",
|
|
||||||
$ip
|
$ip
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -478,9 +478,9 @@ class WPSP_Login_Protection {
|
|||||||
if ( $existing ) {
|
if ( $existing ) {
|
||||||
$new_count = $existing->failed_attempts + 1;
|
$new_count = $existing->failed_attempts + 1;
|
||||||
|
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
|
||||||
$wpdb->update(
|
$wpdb->update(
|
||||||
$table,
|
$wpdb->prefix . 'wpsp_lockouts',
|
||||||
array(
|
array(
|
||||||
'failed_attempts' => $new_count,
|
'failed_attempts' => $new_count,
|
||||||
'updated_at' => current_time( 'mysql' ),
|
'updated_at' => current_time( 'mysql' ),
|
||||||
@@ -494,9 +494,9 @@ class WPSP_Login_Protection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create new record.
|
// Create new record.
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
|
||||||
$wpdb->insert(
|
$wpdb->insert(
|
||||||
$table,
|
$wpdb->prefix . 'wpsp_lockouts',
|
||||||
array(
|
array(
|
||||||
'ip_address' => $ip,
|
'ip_address' => $ip,
|
||||||
'failed_attempts' => 1,
|
'failed_attempts' => 1,
|
||||||
@@ -516,11 +516,9 @@ class WPSP_Login_Protection {
|
|||||||
private function clear_failed_attempts( $ip ) {
|
private function clear_failed_attempts( $ip ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$table = WPSP_DB::get_lockout_table();
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
|
||||||
|
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
|
||||||
$wpdb->delete(
|
$wpdb->delete(
|
||||||
$table,
|
$wpdb->prefix . 'wpsp_lockouts',
|
||||||
array( 'ip_address' => $ip ),
|
array( 'ip_address' => $ip ),
|
||||||
array( '%s' )
|
array( '%s' )
|
||||||
);
|
);
|
||||||
@@ -534,14 +532,13 @@ class WPSP_Login_Protection {
|
|||||||
private function lockout_ip( $ip ) {
|
private function lockout_ip( $ip ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$table = WPSP_DB::get_lockout_table();
|
$duration = (int) Security_Pack::get_setting( 'login_lockout_duration', 15 );
|
||||||
$duration = (int) WP_Security_Pack::get_setting( 'login_lockout_duration', 15 );
|
|
||||||
// Store Unix timestamp to avoid timezone issues.
|
// Store Unix timestamp to avoid timezone issues.
|
||||||
$lockout_until = time() + ( $duration * 60 );
|
$lockout_until = time() + ( $duration * 60 );
|
||||||
|
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
|
||||||
$wpdb->update(
|
$wpdb->update(
|
||||||
$table,
|
$wpdb->prefix . 'wpsp_lockouts',
|
||||||
array(
|
array(
|
||||||
'lockout_until' => $lockout_until,
|
'lockout_until' => $lockout_until,
|
||||||
'updated_at' => current_time( 'mysql' ),
|
'updated_at' => current_time( 'mysql' ),
|
||||||
@@ -558,7 +555,7 @@ class WPSP_Login_Protection {
|
|||||||
null,
|
null,
|
||||||
sprintf(
|
sprintf(
|
||||||
/* translators: %d: Duration in minutes */
|
/* translators: %d: Duration in minutes */
|
||||||
__( 'Locked out for %d minutes', 'wp-security-pack' ),
|
__( 'Locked out for %d minutes', 'security-pack' ),
|
||||||
$duration
|
$duration
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -576,11 +573,11 @@ class WPSP_Login_Protection {
|
|||||||
* @param string $ip IP address.
|
* @param string $ip IP address.
|
||||||
*/
|
*/
|
||||||
private function maybe_auto_blacklist( $ip ) {
|
private function maybe_auto_blacklist( $ip ) {
|
||||||
if ( ! WP_Security_Pack::get_setting( 'auto_blacklist_enabled', false ) ) {
|
if ( ! Security_Pack::get_setting( 'auto_blacklist_enabled', false ) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$threshold = (int) WP_Security_Pack::get_setting( 'auto_blacklist_threshold', 3 );
|
$threshold = (int) Security_Pack::get_setting( 'auto_blacklist_threshold', 3 );
|
||||||
if ( $threshold < 1 ) {
|
if ( $threshold < 1 ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -598,7 +595,7 @@ class WPSP_Login_Protection {
|
|||||||
null,
|
null,
|
||||||
sprintf(
|
sprintf(
|
||||||
/* translators: %d: Number of lockouts */
|
/* translators: %d: Number of lockouts */
|
||||||
__( 'Auto-blacklisted after %d lockouts', 'wp-security-pack' ),
|
__( 'Auto-blacklisted after %d lockouts', 'security-pack' ),
|
||||||
$lockout_count
|
$lockout_count
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -617,13 +614,10 @@ class WPSP_Login_Protection {
|
|||||||
private function count_ip_lockouts( $ip ) {
|
private function count_ip_lockouts( $ip ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$table = WPSP_DB::get_log_table();
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
|
||||||
|
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
|
||||||
$count = $wpdb->get_var(
|
$count = $wpdb->get_var(
|
||||||
$wpdb->prepare(
|
$wpdb->prepare(
|
||||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
"SELECT COUNT(*) FROM {$wpdb->prefix}wpsp_activity_log WHERE ip_address = %s AND event_type = %s",
|
||||||
"SELECT COUNT(*) FROM {$table} WHERE ip_address = %s AND event_type = %s",
|
|
||||||
$ip,
|
$ip,
|
||||||
WPSP_Activity_Log::EVENT_LOCKOUT
|
WPSP_Activity_Log::EVENT_LOCKOUT
|
||||||
)
|
)
|
||||||
@@ -638,7 +632,7 @@ class WPSP_Login_Protection {
|
|||||||
* @param string $ip IP address.
|
* @param string $ip IP address.
|
||||||
*/
|
*/
|
||||||
private function add_ip_to_blacklist( $ip ) {
|
private function add_ip_to_blacklist( $ip ) {
|
||||||
$current_blacklist = WP_Security_Pack::get_setting( 'ip_blacklist', '' );
|
$current_blacklist = Security_Pack::get_setting( 'ip_blacklist', '' );
|
||||||
|
|
||||||
// Check if IP is already in the blacklist.
|
// Check if IP is already in the blacklist.
|
||||||
$blacklist_array = array_filter( array_map( 'trim', explode( "\n", $current_blacklist ) ) );
|
$blacklist_array = array_filter( array_map( 'trim', explode( "\n", $current_blacklist ) ) );
|
||||||
@@ -650,7 +644,7 @@ class WPSP_Login_Protection {
|
|||||||
$blacklist_array[] = $ip;
|
$blacklist_array[] = $ip;
|
||||||
$new_blacklist = implode( "\n", $blacklist_array );
|
$new_blacklist = implode( "\n", $blacklist_array );
|
||||||
|
|
||||||
WP_Security_Pack::update_setting( 'ip_blacklist', $new_blacklist );
|
Security_Pack::update_setting( 'ip_blacklist', $new_blacklist );
|
||||||
|
|
||||||
// Clear the IP control cache.
|
// Clear the IP control cache.
|
||||||
$ip_control = wpsp()->get_component( 'ip_control' );
|
$ip_control = wpsp()->get_component( 'ip_control' );
|
||||||
@@ -668,13 +662,10 @@ class WPSP_Login_Protection {
|
|||||||
private function get_lockout( $ip ) {
|
private function get_lockout( $ip ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
$table = WPSP_DB::get_lockout_table();
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
|
||||||
|
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
|
||||||
return $wpdb->get_row(
|
return $wpdb->get_row(
|
||||||
$wpdb->prepare(
|
$wpdb->prepare(
|
||||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
"SELECT * FROM {$wpdb->prefix}wpsp_lockouts WHERE ip_address = %s",
|
||||||
"SELECT * FROM {$table} WHERE ip_address = %s",
|
|
||||||
$ip
|
$ip
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -700,14 +691,14 @@ class WPSP_Login_Protection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if requesting wp-admin.
|
// Check if requesting wp-admin.
|
||||||
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '';
|
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
|
||||||
$is_wp_admin = strpos( $request_uri, '/wp-admin' ) !== false;
|
$is_wp_admin = strpos( $request_uri, '/wp-admin' ) !== false;
|
||||||
$is_ajax = strpos( $request_uri, 'admin-ajax.php' ) !== false;
|
$is_ajax = strpos( $request_uri, 'admin-ajax.php' ) !== false;
|
||||||
|
|
||||||
if ( $is_wp_admin && ! $is_ajax ) {
|
if ( $is_wp_admin && ! $is_ajax ) {
|
||||||
// Redirect to a non-existent URL to trigger WordPress's themed 404 page.
|
// Redirect to a non-existent URL to trigger WordPress's themed 404 page.
|
||||||
$home = get_option( 'home', '' );
|
$home = get_option( 'home', '' );
|
||||||
$fake_url = rtrim( $home, '/' ) . '/wpsp-404-' . mt_rand( 1000, 9999 );
|
$fake_url = rtrim( $home, '/' ) . '/wpsp-404-' . wp_rand( 1000, 9999 );
|
||||||
header( 'Location: ' . $fake_url, true, 302 );
|
header( 'Location: ' . $fake_url, true, 302 );
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -768,7 +759,7 @@ class WPSP_Login_Protection {
|
|||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
private function is_ip_locked_out_early() {
|
private function is_ip_locked_out_early() {
|
||||||
if ( ! WP_Security_Pack::get_setting( 'login_limit_enabled', true ) ) {
|
if ( ! Security_Pack::get_setting( 'login_limit_enabled', true ) ) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -785,13 +776,11 @@ class WPSP_Login_Protection {
|
|||||||
|
|
||||||
// Check lockout status directly from database.
|
// Check lockout status directly from database.
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$table = WPSP_DB::get_lockout_table();
|
|
||||||
|
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Security data must be real-time.
|
||||||
$lockout = $wpdb->get_row(
|
$lockout = $wpdb->get_row(
|
||||||
$wpdb->prepare(
|
$wpdb->prepare(
|
||||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
"SELECT * FROM {$wpdb->prefix}wpsp_lockouts WHERE ip_address = %s",
|
||||||
"SELECT * FROM {$table} WHERE ip_address = %s",
|
|
||||||
$ip
|
$ip
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -800,8 +789,8 @@ class WPSP_Login_Protection {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$max_attempts = (int) WP_Security_Pack::get_setting( 'login_max_attempts', 5 );
|
$max_attempts = (int) Security_Pack::get_setting( 'login_max_attempts', 5 );
|
||||||
$lockout_minutes = (int) WP_Security_Pack::get_setting( 'login_lockout_duration', 15 );
|
$lockout_minutes = (int) Security_Pack::get_setting( 'login_lockout_duration', 15 );
|
||||||
$current_time = time();
|
$current_time = time();
|
||||||
|
|
||||||
// Check if IP has reached max attempts and is still within lockout window.
|
// Check if IP has reached max attempts and is still within lockout window.
|
||||||
@@ -828,18 +817,16 @@ class WPSP_Login_Protection {
|
|||||||
* Show lockout message and exit - early version.
|
* Show lockout message and exit - early version.
|
||||||
*/
|
*/
|
||||||
private function show_lockout_message_early() {
|
private function show_lockout_message_early() {
|
||||||
$lockout_minutes = (int) WP_Security_Pack::get_setting( 'login_lockout_duration', 15 );
|
|
||||||
|
|
||||||
status_header( 403 );
|
status_header( 403 );
|
||||||
nocache_headers();
|
nocache_headers();
|
||||||
|
|
||||||
wp_die(
|
wp_die(
|
||||||
sprintf(
|
sprintf(
|
||||||
/* translators: %d: Lockout duration in minutes */
|
/* translators: %d: Lockout duration in minutes */
|
||||||
esc_html__( 'Too many failed login attempts. Please try again in %d minutes.', 'wp-security-pack' ),
|
esc_html__( 'Too many failed login attempts. Please try again in %d minutes.', 'security-pack' ),
|
||||||
$lockout_minutes
|
absint( Security_Pack::get_setting( 'login_lockout_duration', 15 ) )
|
||||||
),
|
),
|
||||||
esc_html__( 'Access Denied', 'wp-security-pack' ),
|
esc_html__( 'Access Denied', 'security-pack' ),
|
||||||
array(
|
array(
|
||||||
'response' => 403,
|
'response' => 403,
|
||||||
'back_link' => false,
|
'back_link' => false,
|
||||||
@@ -854,13 +841,13 @@ class WPSP_Login_Protection {
|
|||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
private function is_custom_login_slug_request_early() {
|
private function is_custom_login_slug_request_early() {
|
||||||
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '';
|
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
|
||||||
$request_path = trim( parse_url( $request_uri, PHP_URL_PATH ), '/' );
|
$request_path = trim( wp_parse_url( $request_uri, PHP_URL_PATH ), '/' );
|
||||||
|
|
||||||
// Get the site path for subdirectory installations.
|
// Get the site path for subdirectory installations.
|
||||||
// Use get_option directly since home_url() might have filters.
|
// Use get_option directly since home_url() might have filters.
|
||||||
$home = get_option( 'home', '' );
|
$home = get_option( 'home', '' );
|
||||||
$home_path = trim( parse_url( $home, PHP_URL_PATH ), '/' );
|
$home_path = trim( wp_parse_url( $home, PHP_URL_PATH ), '/' );
|
||||||
|
|
||||||
// Remove the home path prefix from the request path.
|
// Remove the home path prefix from the request path.
|
||||||
if ( ! empty( $home_path ) && strpos( $request_path, $home_path ) === 0 ) {
|
if ( ! empty( $home_path ) && strpos( $request_path, $home_path ) === 0 ) {
|
||||||
@@ -887,8 +874,7 @@ class WPSP_Login_Protection {
|
|||||||
|
|
||||||
// Allow if user has the access cookie (came through custom login slug before).
|
// Allow if user has the access cookie (came through custom login slug before).
|
||||||
$expected_value = wp_hash( 'wpsp_login_' . $this->custom_login_slug );
|
$expected_value = wp_hash( 'wpsp_login_' . $this->custom_login_slug );
|
||||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
$cookie_value = isset( $_COOKIE[ $this->cookie_name ] ) ? sanitize_text_field( wp_unslash( $_COOKIE[ $this->cookie_name ] ) ) : '';
|
||||||
$cookie_value = isset( $_COOKIE[ $this->cookie_name ] ) ? $_COOKIE[ $this->cookie_name ] : '';
|
|
||||||
|
|
||||||
if ( $cookie_value === $expected_value ) {
|
if ( $cookie_value === $expected_value ) {
|
||||||
return; // Cookie is valid, allow access.
|
return; // Cookie is valid, allow access.
|
||||||
@@ -904,8 +890,7 @@ class WPSP_Login_Protection {
|
|||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
private function is_custom_login_slug_request() {
|
private function is_custom_login_slug_request() {
|
||||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
|
||||||
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
|
|
||||||
$request_path = trim( wp_parse_url( $request_uri, PHP_URL_PATH ), '/' );
|
$request_path = trim( wp_parse_url( $request_uri, PHP_URL_PATH ), '/' );
|
||||||
|
|
||||||
// Get the site path for subdirectory installations.
|
// Get the site path for subdirectory installations.
|
||||||
@@ -944,8 +929,7 @@ class WPSP_Login_Protection {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
|
||||||
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
|
|
||||||
|
|
||||||
// Check if trying to access wp-admin (works for both root and subdirectory installs).
|
// Check if trying to access wp-admin (works for both root and subdirectory installs).
|
||||||
// Allow admin-ajax.php for frontend AJAX functionality.
|
// Allow admin-ajax.php for frontend AJAX functionality.
|
||||||
@@ -1042,8 +1026,8 @@ class WPSP_Login_Protection {
|
|||||||
include $template;
|
include $template;
|
||||||
} else {
|
} else {
|
||||||
wp_die(
|
wp_die(
|
||||||
esc_html__( 'Page not found.', 'wp-security-pack' ),
|
esc_html__( 'Page not found.', 'security-pack' ),
|
||||||
esc_html__( '404 Not Found', 'wp-security-pack' ),
|
esc_html__( '404 Not Found', 'security-pack' ),
|
||||||
array( 'response' => 404 )
|
array( 'response' => 404 )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1058,7 +1042,7 @@ class WPSP_Login_Protection {
|
|||||||
// Honeypot field - hidden via CSS, bots will fill it.
|
// Honeypot field - hidden via CSS, bots will fill it.
|
||||||
?>
|
?>
|
||||||
<p class="wpsp-hp-field" style="position:absolute;left:-9999px;top:-9999px;">
|
<p class="wpsp-hp-field" style="position:absolute;left:-9999px;top:-9999px;">
|
||||||
<label for="wpsp_hp_email"><?php esc_html_e( 'Leave this field empty', 'wp-security-pack' ); ?></label>
|
<label for="wpsp_hp_email"><?php esc_html_e( 'Leave this field empty', 'security-pack' ); ?></label>
|
||||||
<input type="text" name="wpsp_hp_email" id="wpsp_hp_email" value="" tabindex="-1" autocomplete="off" />
|
<input type="text" name="wpsp_hp_email" id="wpsp_hp_email" value="" tabindex="-1" autocomplete="off" />
|
||||||
</p>
|
</p>
|
||||||
<?php
|
<?php
|
||||||
@@ -1081,19 +1065,19 @@ class WPSP_Login_Protection {
|
|||||||
WPSP_Activity_Log::EVENT_LOGIN_FAILED,
|
WPSP_Activity_Log::EVENT_LOGIN_FAILED,
|
||||||
$ip,
|
$ip,
|
||||||
$username,
|
$username,
|
||||||
__( 'Honeypot triggered', 'wp-security-pack' )
|
__( 'Honeypot triggered', 'security-pack' )
|
||||||
);
|
);
|
||||||
|
|
||||||
// Auto-block this IP using configurable duration.
|
// Auto-block this IP using configurable duration.
|
||||||
$ip_control = wpsp()->get_component( 'ip_control' );
|
$ip_control = wpsp()->get_component( 'ip_control' );
|
||||||
$ban_duration = (int) WP_Security_Pack::get_setting( 'honeypot_ban_duration', 60 );
|
$ban_duration = (int) Security_Pack::get_setting( 'honeypot_ban_duration', 60 );
|
||||||
if ( $ip_control ) {
|
if ( $ip_control ) {
|
||||||
$ip_control->auto_block_ip( $ip, $ban_duration, __( 'Honeypot triggered', 'wp-security-pack' ) );
|
$ip_control->auto_block_ip( $ip, $ban_duration, __( 'Honeypot triggered', 'security-pack' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
return new WP_Error(
|
return new WP_Error(
|
||||||
'wpsp_honeypot',
|
'wpsp_honeypot',
|
||||||
__( 'Authentication failed.', 'wp-security-pack' )
|
__( 'Authentication failed.', 'security-pack' )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1113,15 +1097,15 @@ class WPSP_Login_Protection {
|
|||||||
if ( ! empty( $_POST['wpsp_hp_email'] ) ) {
|
if ( ! empty( $_POST['wpsp_hp_email'] ) ) {
|
||||||
$errors->add(
|
$errors->add(
|
||||||
'wpsp_honeypot',
|
'wpsp_honeypot',
|
||||||
__( 'Registration failed.', 'wp-security-pack' )
|
__( 'Registration failed.', 'security-pack' )
|
||||||
);
|
);
|
||||||
|
|
||||||
// Auto-block this IP using configurable duration.
|
// Auto-block this IP using configurable duration.
|
||||||
$ip = WPSP_Helper::get_client_ip();
|
$ip = WPSP_Helper::get_client_ip();
|
||||||
$ip_control = wpsp()->get_component( 'ip_control' );
|
$ip_control = wpsp()->get_component( 'ip_control' );
|
||||||
$ban_duration = (int) WP_Security_Pack::get_setting( 'honeypot_ban_duration', 60 );
|
$ban_duration = (int) Security_Pack::get_setting( 'honeypot_ban_duration', 60 );
|
||||||
if ( $ip_control && $ip ) {
|
if ( $ip_control && $ip ) {
|
||||||
$ip_control->auto_block_ip( $ip, $ban_duration, __( 'Honeypot triggered on registration', 'wp-security-pack' ) );
|
$ip_control->auto_block_ip( $ip, $ban_duration, __( 'Honeypot triggered on registration', 'security-pack' ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1137,11 +1121,11 @@ class WPSP_Login_Protection {
|
|||||||
* @param int $attempts Number of attempts (optional).
|
* @param int $attempts Number of attempts (optional).
|
||||||
*/
|
*/
|
||||||
private function maybe_send_alert( $type, $ip, $username = '', $attempts = 0 ) {
|
private function maybe_send_alert( $type, $ip, $username = '', $attempts = 0 ) {
|
||||||
if ( ! WP_Security_Pack::get_setting( 'email_alerts_enabled', false ) ) {
|
if ( ! Security_Pack::get_setting( 'email_alerts_enabled', false ) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$email = WP_Security_Pack::get_setting( 'email_alerts_address', get_option( 'admin_email' ) );
|
$email = Security_Pack::get_setting( 'email_alerts_address', get_option( 'admin_email' ) );
|
||||||
if ( empty( $email ) ) {
|
if ( empty( $email ) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1151,23 +1135,23 @@ class WPSP_Login_Protection {
|
|||||||
|
|
||||||
switch ( $type ) {
|
switch ( $type ) {
|
||||||
case 'failed_login':
|
case 'failed_login':
|
||||||
$threshold = WP_Security_Pack::get_setting( 'email_alert_threshold', 3 );
|
$threshold = Security_Pack::get_setting( 'email_alert_threshold', 3 );
|
||||||
if ( $attempts < $threshold ) {
|
if ( $attempts < $threshold ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$subject = sprintf(
|
$subject = sprintf(
|
||||||
/* translators: %s: Site name */
|
/* translators: %s: Site name */
|
||||||
__( '[%s] Multiple Failed Login Attempts', 'wp-security-pack' ),
|
__( '[%s] Multiple Failed Login Attempts', 'security-pack' ),
|
||||||
$site_name
|
$site_name
|
||||||
);
|
);
|
||||||
|
|
||||||
$message = sprintf(
|
$message = sprintf(
|
||||||
/* translators: 1: Number of attempts, 2: IP address, 3: Username, 4: Site URL */
|
/* translators: 1: Number of attempts, 2: IP address, 3: Username, 4: Site URL */
|
||||||
__( "There have been %1\$d failed login attempts on your site.\n\nIP Address: %2\$s\nUsername attempted: %3\$s\nSite: %4\$s\n\nIf this wasn't you, the IP will be automatically locked out after reaching the threshold.", 'wp-security-pack' ),
|
__( "There have been %1\$d failed login attempts on your site.\n\nIP Address: %2\$s\nUsername attempted: %3\$s\nSite: %4\$s\n\nIf this wasn't you, the IP will be automatically locked out after reaching the threshold.", 'security-pack' ),
|
||||||
$attempts,
|
$attempts,
|
||||||
$ip,
|
$ip,
|
||||||
$username ? $username : __( '(empty)', 'wp-security-pack' ),
|
$username ? $username : __( '(empty)', 'security-pack' ),
|
||||||
$site_url
|
$site_url
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@@ -1175,13 +1159,13 @@ class WPSP_Login_Protection {
|
|||||||
case 'lockout':
|
case 'lockout':
|
||||||
$subject = sprintf(
|
$subject = sprintf(
|
||||||
/* translators: %s: Site name */
|
/* translators: %s: Site name */
|
||||||
__( '[%s] IP Address Locked Out', 'wp-security-pack' ),
|
__( '[%s] IP Address Locked Out', 'security-pack' ),
|
||||||
$site_name
|
$site_name
|
||||||
);
|
);
|
||||||
|
|
||||||
$message = sprintf(
|
$message = sprintf(
|
||||||
/* translators: 1: IP address, 2: Site URL */
|
/* translators: 1: IP address, 2: Site URL */
|
||||||
__( "An IP address has been locked out due to too many failed login attempts.\n\nIP Address: %1\$s\nSite: %2\$s", 'wp-security-pack' ),
|
__( "An IP address has been locked out due to too many failed login attempts.\n\nIP Address: %1\$s\nSite: %2\$s", 'security-pack' ),
|
||||||
$ip,
|
$ip,
|
||||||
$site_url
|
$site_url
|
||||||
);
|
);
|
||||||
@@ -1190,13 +1174,13 @@ class WPSP_Login_Protection {
|
|||||||
case 'auto_blacklist':
|
case 'auto_blacklist':
|
||||||
$subject = sprintf(
|
$subject = sprintf(
|
||||||
/* translators: %s: Site name */
|
/* translators: %s: Site name */
|
||||||
__( '[%s] IP Permanently Blacklisted', 'wp-security-pack' ),
|
__( '[%s] IP Permanently Blacklisted', 'security-pack' ),
|
||||||
$site_name
|
$site_name
|
||||||
);
|
);
|
||||||
|
|
||||||
$message = sprintf(
|
$message = sprintf(
|
||||||
/* translators: 1: IP address, 2: Number of lockouts, 3: Site URL */
|
/* translators: 1: IP address, 2: Number of lockouts, 3: Site URL */
|
||||||
__( "An IP address has been permanently added to your blacklist due to repeated lockouts.\n\nIP Address: %1\$s\nTotal Lockouts: %2\$d\nSite: %3\$s\n\nThis IP will no longer be able to access your website.", 'wp-security-pack' ),
|
__( "An IP address has been permanently added to your blacklist due to repeated lockouts.\n\nIP Address: %1\$s\nTotal Lockouts: %2\$d\nSite: %3\$s\n\nThis IP will no longer be able to access your website.", 'security-pack' ),
|
||||||
$ip,
|
$ip,
|
||||||
$attempts,
|
$attempts,
|
||||||
$site_url
|
$site_url
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Malware scanner for WP Security Pack.
|
* Malware scanner for Security Pack.
|
||||||
*
|
*
|
||||||
* @package WP_Security_Pack
|
* @package Security_Pack
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Prevent direct access.
|
// Prevent direct access.
|
||||||
@@ -70,7 +70,7 @@ class WPSP_Malware_Scanner {
|
|||||||
*/
|
*/
|
||||||
private $skip_paths = array(
|
private $skip_paths = array(
|
||||||
// This plugin's own files.
|
// This plugin's own files.
|
||||||
'wp-security-pack',
|
'security-pack',
|
||||||
|
|
||||||
// Common libraries that legitimately use shell/network functions.
|
// Common libraries that legitimately use shell/network functions.
|
||||||
'phpseclib',
|
'phpseclib',
|
||||||
@@ -93,7 +93,7 @@ class WPSP_Malware_Scanner {
|
|||||||
$this->load_signatures();
|
$this->load_signatures();
|
||||||
$this->load_malware_hashes();
|
$this->load_malware_hashes();
|
||||||
|
|
||||||
if ( ! WP_Security_Pack::get_setting( 'malware_scan_enabled', true ) ) {
|
if ( ! Security_Pack::get_setting( 'malware_scan_enabled', true ) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ class WPSP_Malware_Scanner {
|
|||||||
if ( empty( $source_url ) ) {
|
if ( empty( $source_url ) ) {
|
||||||
// Default: Could be a GitHub raw URL or your own endpoint.
|
// Default: Could be a GitHub raw URL or your own endpoint.
|
||||||
// For now, just return - users can provide their own source.
|
// For now, just return - users can provide their own source.
|
||||||
return new WP_Error( 'no_source', __( 'No hash database source URL provided.', 'wp-security-pack' ) );
|
return new WP_Error( 'no_source', __( 'No hash database source URL provided.', 'security-pack' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
$response = wp_remote_get( $source_url, array( 'timeout' => 30 ) );
|
$response = wp_remote_get( $source_url, array( 'timeout' => 30 ) );
|
||||||
@@ -173,7 +173,7 @@ class WPSP_Malware_Scanner {
|
|||||||
$data = json_decode( $body, true );
|
$data = json_decode( $body, true );
|
||||||
|
|
||||||
if ( ! is_array( $data ) ) {
|
if ( ! is_array( $data ) ) {
|
||||||
return new WP_Error( 'invalid_data', __( 'Invalid hash database format.', 'wp-security-pack' ) );
|
return new WP_Error( 'invalid_data', __( 'Invalid hash database format.', 'security-pack' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge with existing hashes.
|
// Merge with existing hashes.
|
||||||
@@ -693,11 +693,11 @@ class WPSP_Malware_Scanner {
|
|||||||
* @param array $results Scan results.
|
* @param array $results Scan results.
|
||||||
*/
|
*/
|
||||||
private function send_alert( $results ) {
|
private function send_alert( $results ) {
|
||||||
if ( ! WP_Security_Pack::get_setting( 'email_alerts_enabled', false ) ) {
|
if ( ! Security_Pack::get_setting( 'email_alerts_enabled', false ) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$email = WP_Security_Pack::get_setting( 'email_alerts_address', get_option( 'admin_email' ) );
|
$email = Security_Pack::get_setting( 'email_alerts_address', get_option( 'admin_email' ) );
|
||||||
$site_name = get_bloginfo( 'name' );
|
$site_name = get_bloginfo( 'name' );
|
||||||
$site_url = home_url();
|
$site_url = home_url();
|
||||||
|
|
||||||
@@ -717,30 +717,34 @@ class WPSP_Malware_Scanner {
|
|||||||
|
|
||||||
$subject = sprintf(
|
$subject = sprintf(
|
||||||
/* translators: %s: Site name */
|
/* translators: %s: Site name */
|
||||||
__( '[%s] Malware Scan Alert - Suspicious Files Found', 'wp-security-pack' ),
|
__( '[%s] Malware Scan Alert - Suspicious Files Found', 'security-pack' ),
|
||||||
$site_name
|
$site_name
|
||||||
);
|
);
|
||||||
|
|
||||||
$message = sprintf(
|
$message = sprintf(
|
||||||
/* translators: %s: Site URL */
|
/* translators: %s: Site URL */
|
||||||
__( "WP Security Pack malware scan has detected suspicious files on %s.\n\n", 'wp-security-pack' ),
|
__( "Security Pack malware scan has detected suspicious files on %s.\n\n", 'security-pack' ),
|
||||||
$site_url
|
$site_url
|
||||||
);
|
);
|
||||||
|
|
||||||
$message .= __( "Summary:\n", 'wp-security-pack' );
|
$message .= __( "Summary:\n", 'security-pack' );
|
||||||
$message .= sprintf( __( "- Critical: %d\n", 'wp-security-pack' ), $counts['critical'] );
|
/* translators: %d: Number of critical severity issues */
|
||||||
$message .= sprintf( __( "- High: %d\n", 'wp-security-pack' ), $counts['high'] );
|
$message .= sprintf( __( "- Critical: %d\n", 'security-pack' ), $counts['critical'] );
|
||||||
$message .= sprintf( __( "- Medium: %d\n", 'wp-security-pack' ), $counts['medium'] );
|
/* translators: %d: Number of high severity issues */
|
||||||
$message .= sprintf( __( "- Low: %d\n\n", 'wp-security-pack' ), $counts['low'] );
|
$message .= sprintf( __( "- High: %d\n", 'security-pack' ), $counts['high'] );
|
||||||
|
/* translators: %d: Number of medium severity issues */
|
||||||
|
$message .= sprintf( __( "- Medium: %d\n", 'security-pack' ), $counts['medium'] );
|
||||||
|
/* translators: %d: Number of low severity issues */
|
||||||
|
$message .= sprintf( __( "- Low: %d\n\n", 'security-pack' ), $counts['low'] );
|
||||||
|
|
||||||
$message .= __( "Files with issues:\n", 'wp-security-pack' );
|
$message .= __( "Files with issues:\n", 'security-pack' );
|
||||||
|
|
||||||
$count = 0;
|
$count = 0;
|
||||||
foreach ( $results as $file => $findings ) {
|
foreach ( $results as $file => $findings ) {
|
||||||
if ( $count >= 20 ) {
|
if ( $count >= 20 ) {
|
||||||
$message .= sprintf(
|
$message .= sprintf(
|
||||||
/* translators: %d: Number of additional files */
|
/* translators: %d: Number of additional files */
|
||||||
__( "... and %d more files\n", 'wp-security-pack' ),
|
__( "... and %d more files\n", 'security-pack' ),
|
||||||
count( $results ) - 20
|
count( $results ) - 20
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@@ -756,8 +760,8 @@ class WPSP_Malware_Scanner {
|
|||||||
$count++;
|
$count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
$message .= "\n" . __( "Please review these files in your WordPress admin panel under Settings > WP Security Pack.", 'wp-security-pack' );
|
$message .= "\n" . __( "Please review these files in your WordPress admin panel under Settings > Security Pack.", 'security-pack' );
|
||||||
$message .= "\n\n" . __( "Note: Some detections may be false positives. Review each file carefully before taking action.", 'wp-security-pack' );
|
$message .= "\n\n" . __( "Note: Some detections may be false positives. Review each file carefully before taking action.", 'security-pack' );
|
||||||
|
|
||||||
wp_mail( $email, $subject, $message );
|
wp_mail( $email, $subject, $message );
|
||||||
}
|
}
|
||||||
@@ -835,12 +839,12 @@ class WPSP_Malware_Scanner {
|
|||||||
*/
|
*/
|
||||||
public function quarantine_file( $file_path ) {
|
public function quarantine_file( $file_path ) {
|
||||||
if ( ! file_exists( $file_path ) ) {
|
if ( ! file_exists( $file_path ) ) {
|
||||||
return new WP_Error( 'file_not_found', __( 'File not found.', 'wp-security-pack' ) );
|
return new WP_Error( 'file_not_found', __( 'File not found.', 'security-pack' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Security: Only allow quarantining files within wp-content.
|
// Security: Only allow quarantining files within wp-content.
|
||||||
if ( strpos( realpath( $file_path ), realpath( WP_CONTENT_DIR ) ) !== 0 ) {
|
if ( strpos( realpath( $file_path ), realpath( WP_CONTENT_DIR ) ) !== 0 ) {
|
||||||
return new WP_Error( 'invalid_path', __( 'Can only quarantine files within wp-content.', 'wp-security-pack' ) );
|
return new WP_Error( 'invalid_path', __( 'Can only quarantine files within wp-content.', 'security-pack' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
$quarantine_dir = $this->get_quarantine_dir();
|
$quarantine_dir = $this->get_quarantine_dir();
|
||||||
@@ -872,7 +876,7 @@ class WPSP_Malware_Scanner {
|
|||||||
// Move file to quarantine.
|
// Move file to quarantine.
|
||||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename
|
// phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename
|
||||||
if ( ! rename( $file_path, $quarantine_path ) ) {
|
if ( ! rename( $file_path, $quarantine_path ) ) {
|
||||||
return new WP_Error( 'move_failed', __( 'Failed to move file to quarantine.', 'wp-security-pack' ) );
|
return new WP_Error( 'move_failed', __( 'Failed to move file to quarantine.', 'security-pack' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save metadata.
|
// Save metadata.
|
||||||
@@ -911,7 +915,7 @@ class WPSP_Malware_Scanner {
|
|||||||
$meta_file = $quarantine_path . '.meta';
|
$meta_file = $quarantine_path . '.meta';
|
||||||
|
|
||||||
if ( ! file_exists( $quarantine_path ) ) {
|
if ( ! file_exists( $quarantine_path ) ) {
|
||||||
return new WP_Error( 'file_not_found', __( 'Quarantined file not found.', 'wp-security-pack' ) );
|
return new WP_Error( 'file_not_found', __( 'Quarantined file not found.', 'security-pack' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get metadata.
|
// Get metadata.
|
||||||
@@ -922,7 +926,7 @@ class WPSP_Malware_Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ( empty( $metadata['original_path'] ) ) {
|
if ( empty( $metadata['original_path'] ) ) {
|
||||||
return new WP_Error( 'no_metadata', __( 'Cannot restore: original path unknown.', 'wp-security-pack' ) );
|
return new WP_Error( 'no_metadata', __( 'Cannot restore: original path unknown.', 'security-pack' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
$original_path = $metadata['original_path'];
|
$original_path = $metadata['original_path'];
|
||||||
@@ -936,7 +940,7 @@ class WPSP_Malware_Scanner {
|
|||||||
// Restore file.
|
// Restore file.
|
||||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename
|
// phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename
|
||||||
if ( ! rename( $quarantine_path, $original_path ) ) {
|
if ( ! rename( $quarantine_path, $original_path ) ) {
|
||||||
return new WP_Error( 'restore_failed', __( 'Failed to restore file.', 'wp-security-pack' ) );
|
return new WP_Error( 'restore_failed', __( 'Failed to restore file.', 'security-pack' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up metadata file.
|
// Clean up metadata file.
|
||||||
@@ -970,13 +974,13 @@ class WPSP_Malware_Scanner {
|
|||||||
$meta_file = $quarantine_path . '.meta';
|
$meta_file = $quarantine_path . '.meta';
|
||||||
|
|
||||||
if ( ! file_exists( $quarantine_path ) ) {
|
if ( ! file_exists( $quarantine_path ) ) {
|
||||||
return new WP_Error( 'file_not_found', __( 'Quarantined file not found.', 'wp-security-pack' ) );
|
return new WP_Error( 'file_not_found', __( 'Quarantined file not found.', 'security-pack' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete file.
|
// Delete file.
|
||||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
|
// phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
|
||||||
if ( ! unlink( $quarantine_path ) ) {
|
if ( ! unlink( $quarantine_path ) ) {
|
||||||
return new WP_Error( 'delete_failed', __( 'Failed to delete file.', 'wp-security-pack' ) );
|
return new WP_Error( 'delete_failed', __( 'Failed to delete file.', 'security-pack' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up metadata file.
|
// Clean up metadata file.
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Two-Factor Authentication for WP Security Pack.
|
* Two-Factor Authentication for Security Pack.
|
||||||
*
|
*
|
||||||
* @package WP_Security_Pack
|
* @package Security_Pack
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Prevent direct access.
|
// Prevent direct access.
|
||||||
@@ -54,7 +54,7 @@ class WPSP_Two_Factor {
|
|||||||
* Constructor.
|
* Constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
if ( ! WP_Security_Pack::get_setting( 'two_factor_enabled', false ) ) {
|
if ( ! Security_Pack::get_setting( 'two_factor_enabled', false ) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +73,9 @@ class WPSP_Two_Factor {
|
|||||||
add_action( 'personal_options_update', array( $this, 'save_user_2fa_settings' ) );
|
add_action( 'personal_options_update', array( $this, 'save_user_2fa_settings' ) );
|
||||||
add_action( 'edit_user_profile_update', array( $this, 'save_user_2fa_settings' ) );
|
add_action( 'edit_user_profile_update', array( $this, 'save_user_2fa_settings' ) );
|
||||||
|
|
||||||
|
// Enqueue scripts on profile pages.
|
||||||
|
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_profile_scripts' ) );
|
||||||
|
|
||||||
// AJAX handlers.
|
// AJAX handlers.
|
||||||
add_action( 'wp_ajax_wpsp_generate_2fa_secret', array( $this, 'ajax_generate_secret' ) );
|
add_action( 'wp_ajax_wpsp_generate_2fa_secret', array( $this, 'ajax_generate_secret' ) );
|
||||||
add_action( 'wp_ajax_wpsp_verify_2fa_setup', array( $this, 'ajax_verify_setup' ) );
|
add_action( 'wp_ajax_wpsp_verify_2fa_setup', array( $this, 'ajax_verify_setup' ) );
|
||||||
@@ -80,11 +83,30 @@ class WPSP_Two_Factor {
|
|||||||
add_action( 'wp_ajax_wpsp_regenerate_backup_codes', array( $this, 'ajax_regenerate_backup_codes' ) );
|
add_action( 'wp_ajax_wpsp_regenerate_backup_codes', array( $this, 'ajax_regenerate_backup_codes' ) );
|
||||||
|
|
||||||
// Enforce 2FA for admins.
|
// Enforce 2FA for admins.
|
||||||
if ( WP_Security_Pack::get_setting( 'two_factor_enforce_admin', false ) ) {
|
if ( Security_Pack::get_setting( 'two_factor_enforce_admin', false ) ) {
|
||||||
add_action( 'admin_init', array( $this, 'enforce_admin_2fa' ) );
|
add_action( 'admin_init', array( $this, 'enforce_admin_2fa' ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue scripts for profile pages.
|
||||||
|
*
|
||||||
|
* @param string $hook Current admin page hook.
|
||||||
|
*/
|
||||||
|
public function enqueue_profile_scripts( $hook ) {
|
||||||
|
if ( 'profile.php' !== $hook && 'user-edit.php' !== $hook ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_enqueue_script(
|
||||||
|
'wpsp-qrcode',
|
||||||
|
WPSP_PLUGIN_URL . 'assets/js/qrcode.min.js',
|
||||||
|
array(),
|
||||||
|
WPSP_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if 2FA is required during authentication.
|
* Check if 2FA is required during authentication.
|
||||||
* This runs BEFORE cookies are set, making it reliable for 2FA.
|
* This runs BEFORE cookies are set, making it reliable for 2FA.
|
||||||
@@ -108,9 +130,12 @@ class WPSP_Two_Factor {
|
|||||||
// Generate a token for the 2FA session.
|
// Generate a token for the 2FA session.
|
||||||
$token = wp_generate_password( 32, false );
|
$token = wp_generate_password( 32, false );
|
||||||
|
|
||||||
|
// Nonce not possible during 2FA authentication flow; transient token validates the session.
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||||
|
$redirect = isset( $_REQUEST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) ) : admin_url();
|
||||||
set_transient( 'wpsp_2fa_' . $token, array(
|
set_transient( 'wpsp_2fa_' . $token, array(
|
||||||
'user_id' => $user->ID,
|
'user_id' => $user->ID,
|
||||||
'redirect' => isset( $_REQUEST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) ) : admin_url(),
|
'redirect' => $redirect,
|
||||||
), 5 * MINUTE_IN_SECONDS );
|
), 5 * MINUTE_IN_SECONDS );
|
||||||
|
|
||||||
// Redirect to 2FA form immediately.
|
// Redirect to 2FA form immediately.
|
||||||
@@ -184,17 +209,14 @@ class WPSP_Two_Factor {
|
|||||||
|
|
||||||
$error = '';
|
$error = '';
|
||||||
|
|
||||||
// Handle form submission.
|
// Handle form submission - 2FA verification uses transient token instead of nonce; user already authenticated via password.
|
||||||
// phpcs:ignore WordPress.Security.NonceVerification.Missing
|
if ( isset( $_POST['wpsp_2fa_code'] ) || isset( $_POST['wpsp_backup_code'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||||
if ( isset( $_POST['wpsp_2fa_code'] ) || isset( $_POST['wpsp_backup_code'] ) ) {
|
|
||||||
// Check TOTP code first, then backup code.
|
// Check TOTP code first, then backup code.
|
||||||
$code = '';
|
$code = '';
|
||||||
// phpcs:ignore WordPress.Security.NonceVerification.Missing
|
if ( ! empty( $_POST['wpsp_2fa_code'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||||
if ( ! empty( $_POST['wpsp_2fa_code'] ) ) {
|
$code = sanitize_text_field( wp_unslash( $_POST['wpsp_2fa_code'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||||
$code = sanitize_text_field( wp_unslash( $_POST['wpsp_2fa_code'] ) );
|
} elseif ( ! empty( $_POST['wpsp_backup_code'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||||
// phpcs:ignore WordPress.Security.NonceVerification.Missing
|
$code = sanitize_text_field( wp_unslash( $_POST['wpsp_backup_code'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||||
} elseif ( ! empty( $_POST['wpsp_backup_code'] ) ) {
|
|
||||||
$code = sanitize_text_field( wp_unslash( $_POST['wpsp_backup_code'] ) );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$user_id = $data['user_id'];
|
$user_id = $data['user_id'];
|
||||||
@@ -212,36 +234,36 @@ class WPSP_Two_Factor {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$error = __( 'Invalid verification code.', 'wp-security-pack' );
|
$error = __( 'Invalid verification code.', 'security-pack' );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render the form.
|
// Render the form.
|
||||||
login_header( __( 'Two-Factor Authentication', 'wp-security-pack' ) );
|
login_header( __( 'Two-Factor Authentication', 'security-pack' ) );
|
||||||
?>
|
?>
|
||||||
<form name="wpsp_2fa_form" id="wpsp_2fa_form" action="" method="post">
|
<form name="wpsp_2fa_form" id="wpsp_2fa_form" action="" method="post">
|
||||||
<?php if ( $error ) : ?>
|
<?php if ( $error ) : ?>
|
||||||
<div id="login_error"><?php echo esc_html( $error ); ?></div>
|
<div id="login_error"><?php echo esc_html( $error ); ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<p><?php esc_html_e( 'Enter the verification code from your authenticator app.', 'wp-security-pack' ); ?></p>
|
<p><?php esc_html_e( 'Enter the verification code from your authenticator app.', 'security-pack' ); ?></p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<label for="wpsp_2fa_code"><?php esc_html_e( 'Verification Code', 'wp-security-pack' ); ?></label>
|
<label for="wpsp_2fa_code"><?php esc_html_e( 'Verification Code', 'security-pack' ); ?></label>
|
||||||
<input type="text" name="wpsp_2fa_code" id="wpsp_2fa_code" class="input" size="20" autocomplete="one-time-code" inputmode="numeric" pattern="[0-9]*" autofocus />
|
<input type="text" name="wpsp_2fa_code" id="wpsp_2fa_code" class="input" size="20" autocomplete="one-time-code" inputmode="numeric" pattern="[0-9]*" autofocus />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="submit">
|
<p class="submit">
|
||||||
<input type="submit" name="wp-submit" id="wp-submit" class="button button-primary button-large" value="<?php esc_attr_e( 'Verify', 'wp-security-pack' ); ?>" />
|
<input type="submit" name="wp-submit" id="wp-submit" class="button button-primary button-large" value="<?php esc_attr_e( 'Verify', 'security-pack' ); ?>" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="wpsp-backup-code-link">
|
<p class="wpsp-backup-code-link">
|
||||||
<a href="#" onclick="document.getElementById('wpsp-backup-field').style.display='block';this.style.display='none';return false;">
|
<a href="#" onclick="document.getElementById('wpsp-backup-field').style.display='block';this.style.display='none';return false;">
|
||||||
<?php esc_html_e( 'Use a backup code', 'wp-security-pack' ); ?>
|
<?php esc_html_e( 'Use a backup code', 'security-pack' ); ?>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div id="wpsp-backup-field" style="display:none;">
|
<div id="wpsp-backup-field" style="display:none;">
|
||||||
<p><?php esc_html_e( 'Or enter a backup code:', 'wp-security-pack' ); ?></p>
|
<p><?php esc_html_e( 'Or enter a backup code:', 'security-pack' ); ?></p>
|
||||||
<p>
|
<p>
|
||||||
<input type="text" name="wpsp_backup_code" id="wpsp_backup_code" class="input" size="20" />
|
<input type="text" name="wpsp_backup_code" id="wpsp_backup_code" class="input" size="20" />
|
||||||
</p>
|
</p>
|
||||||
@@ -258,7 +280,7 @@ class WPSP_Two_Factor {
|
|||||||
* @param WP_User $user User object.
|
* @param WP_User $user User object.
|
||||||
*/
|
*/
|
||||||
public function show_user_2fa_settings( $user ) {
|
public function show_user_2fa_settings( $user ) {
|
||||||
if ( ! WP_Security_Pack::get_setting( 'two_factor_enabled', false ) ) {
|
if ( ! Security_Pack::get_setting( 'two_factor_enabled', false ) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,57 +291,57 @@ class WPSP_Two_Factor {
|
|||||||
if ( isset( $_GET['wpsp_2fa_required'] ) && ! $is_enabled ) :
|
if ( isset( $_GET['wpsp_2fa_required'] ) && ! $is_enabled ) :
|
||||||
?>
|
?>
|
||||||
<div class="notice notice-warning" style="margin-bottom: 20px;">
|
<div class="notice notice-warning" style="margin-bottom: 20px;">
|
||||||
<p><strong><?php esc_html_e( 'Two-Factor Authentication Required', 'wp-security-pack' ); ?></strong></p>
|
<p><strong><?php esc_html_e( 'Two-Factor Authentication Required', 'security-pack' ); ?></strong></p>
|
||||||
<p><?php esc_html_e( 'You must set up 2FA to access the dashboard.', 'wp-security-pack' ); ?></p>
|
<p><?php esc_html_e( 'You must set up 2FA to access the dashboard.', 'security-pack' ); ?></p>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
endif;
|
endif;
|
||||||
?>
|
?>
|
||||||
<h2><?php esc_html_e( 'Two-Factor Authentication', 'wp-security-pack' ); ?></h2>
|
<h2><?php esc_html_e( 'Two-Factor Authentication', 'security-pack' ); ?></h2>
|
||||||
<table class="form-table" role="presentation">
|
<table class="form-table" role="presentation">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row"><?php esc_html_e( 'Status', 'wp-security-pack' ); ?></th>
|
<th scope="row"><?php esc_html_e( 'Status', 'security-pack' ); ?></th>
|
||||||
<td>
|
<td>
|
||||||
<?php if ( $is_enabled ) : ?>
|
<?php if ( $is_enabled ) : ?>
|
||||||
<?php $remaining_codes = $this->get_backup_codes_count( $user->ID ); ?>
|
<?php $remaining_codes = absint( $this->get_backup_codes_count( $user->ID ) ); ?>
|
||||||
<span class="wpsp-2fa-status wpsp-2fa-enabled"><?php esc_html_e( 'Enabled', 'wp-security-pack' ); ?></span>
|
<span class="wpsp-2fa-status wpsp-2fa-enabled"><?php esc_html_e( 'Enabled', 'security-pack' ); ?></span>
|
||||||
<p>
|
<p>
|
||||||
<button type="button" class="button" id="wpsp-disable-2fa"><?php esc_html_e( 'Disable 2FA', 'wp-security-pack' ); ?></button>
|
<button type="button" class="button" id="wpsp-disable-2fa"><?php esc_html_e( 'Disable 2FA', 'security-pack' ); ?></button>
|
||||||
<button type="button" class="button" id="wpsp-regenerate-backup-codes"><?php esc_html_e( 'Regenerate Backup Codes', 'wp-security-pack' ); ?></button>
|
<button type="button" class="button" id="wpsp-regenerate-backup-codes"><?php esc_html_e( 'Regenerate Backup Codes', 'security-pack' ); ?></button>
|
||||||
</p>
|
</p>
|
||||||
<p class="description">
|
<p class="description">
|
||||||
<?php
|
<?php
|
||||||
printf(
|
printf(
|
||||||
/* translators: %d: number of remaining backup codes */
|
/* translators: %d: number of remaining backup codes */
|
||||||
esc_html__( 'You have %d backup codes remaining.', 'wp-security-pack' ),
|
esc_html__( 'You have %d backup codes remaining.', 'security-pack' ),
|
||||||
$remaining_codes
|
intval( $remaining_codes )
|
||||||
);
|
);
|
||||||
?>
|
?>
|
||||||
<?php if ( $remaining_codes < 3 ) : ?>
|
<?php if ( $remaining_codes < 3 ) : ?>
|
||||||
<strong style="color: #dc3232;"><?php esc_html_e( 'Consider regenerating your backup codes.', 'wp-security-pack' ); ?></strong>
|
<strong style="color: #dc3232;"><?php esc_html_e( 'Consider regenerating your backup codes.', 'security-pack' ); ?></strong>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</p>
|
</p>
|
||||||
<div id="wpsp-backup-codes-display" style="display:none; background: #f6f7f7; padding: 15px; margin-top: 10px; border-radius: 4px;">
|
<div id="wpsp-backup-codes-display" style="display:none; background: #f6f7f7; padding: 15px; margin-top: 10px; border-radius: 4px;">
|
||||||
<p><strong><?php esc_html_e( 'New Backup Codes:', 'wp-security-pack' ); ?></strong></p>
|
<p><strong><?php esc_html_e( 'New Backup Codes:', 'security-pack' ); ?></strong></p>
|
||||||
<pre id="wpsp-new-backup-codes" style="background: #fff; padding: 10px;"></pre>
|
<pre id="wpsp-new-backup-codes" style="background: #fff; padding: 10px;"></pre>
|
||||||
<p class="description" style="color: #dc3232;">
|
<p class="description" style="color: #dc3232;">
|
||||||
<strong><?php esc_html_e( 'IMPORTANT: Save these codes now!', 'wp-security-pack' ); ?></strong><br>
|
<strong><?php esc_html_e( 'IMPORTANT: Save these codes now!', 'security-pack' ); ?></strong><br>
|
||||||
<?php esc_html_e( 'These codes will NOT be shown again. Store them in a safe place.', 'wp-security-pack' ); ?>
|
<?php esc_html_e( 'These codes will NOT be shown again. Store them in a safe place.', 'security-pack' ); ?>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<?php else : ?>
|
<?php else : ?>
|
||||||
<span class="wpsp-2fa-status wpsp-2fa-disabled"><?php esc_html_e( 'Disabled', 'wp-security-pack' ); ?></span>
|
<span class="wpsp-2fa-status wpsp-2fa-disabled"><?php esc_html_e( 'Disabled', 'security-pack' ); ?></span>
|
||||||
<p>
|
<p>
|
||||||
<button type="button" class="button button-primary" id="wpsp-setup-2fa"><?php esc_html_e( 'Set Up 2FA', 'wp-security-pack' ); ?></button>
|
<button type="button" class="button button-primary" id="wpsp-setup-2fa"><?php esc_html_e( 'Set Up 2FA', 'security-pack' ); ?></button>
|
||||||
</p>
|
</p>
|
||||||
<div id="wpsp-2fa-setup" style="display:none;">
|
<div id="wpsp-2fa-setup" style="display:none;">
|
||||||
<p><?php esc_html_e( 'Scan this QR code with your authenticator app:', 'wp-security-pack' ); ?></p>
|
<p><?php esc_html_e( 'Scan this QR code with your authenticator app:', 'security-pack' ); ?></p>
|
||||||
<div id="wpsp-2fa-qr"></div>
|
<div id="wpsp-2fa-qr"></div>
|
||||||
<p><strong><?php esc_html_e( 'Manual entry key:', 'wp-security-pack' ); ?></strong> <code id="wpsp-2fa-secret"></code></p>
|
<p><strong><?php esc_html_e( 'Manual entry key:', 'security-pack' ); ?></strong> <code id="wpsp-2fa-secret"></code></p>
|
||||||
<p>
|
<p>
|
||||||
<label for="wpsp-2fa-verify-code"><?php esc_html_e( 'Enter verification code to confirm:', 'wp-security-pack' ); ?></label>
|
<label for="wpsp-2fa-verify-code"><?php esc_html_e( 'Enter verification code to confirm:', 'security-pack' ); ?></label>
|
||||||
<input type="text" id="wpsp-2fa-verify-code" class="regular-text" autocomplete="off" />
|
<input type="text" id="wpsp-2fa-verify-code" class="regular-text" autocomplete="off" />
|
||||||
<button type="button" class="button button-primary" id="wpsp-verify-2fa-setup"><?php esc_html_e( 'Verify & Enable', 'wp-security-pack' ); ?></button>
|
<button type="button" class="button button-primary" id="wpsp-verify-2fa-setup"><?php esc_html_e( 'Verify & Enable', 'security-pack' ); ?></button>
|
||||||
</p>
|
</p>
|
||||||
<div id="wpsp-2fa-setup-result"></div>
|
<div id="wpsp-2fa-setup-result"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -327,7 +349,6 @@ class WPSP_Two_Factor {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
jQuery(document).ready(function($) {
|
jQuery(document).ready(function($) {
|
||||||
$('#wpsp-setup-2fa').on('click', function() {
|
$('#wpsp-setup-2fa').on('click', function() {
|
||||||
@@ -363,11 +384,11 @@ class WPSP_Two_Factor {
|
|||||||
if (response.success && response.data.backup_codes) {
|
if (response.success && response.data.backup_codes) {
|
||||||
// Show backup codes - user MUST save these.
|
// Show backup codes - user MUST save these.
|
||||||
var codesHtml = '<div style="background:#d4edda;border:1px solid #c3e6cb;padding:20px;margin:10px 0;border-radius:4px;">';
|
var codesHtml = '<div style="background:#d4edda;border:1px solid #c3e6cb;padding:20px;margin:10px 0;border-radius:4px;">';
|
||||||
codesHtml += '<h3 style="margin-top:0;color:#155724;"><?php echo esc_js( __( '2FA Enabled! Save Your Backup Codes', 'wp-security-pack' ) ); ?></h3>';
|
codesHtml += '<h3 style="margin-top:0;color:#155724;"><?php echo esc_js( __( '2FA Enabled! Save Your Backup Codes', 'security-pack' ) ); ?></h3>';
|
||||||
codesHtml += '<p style="color:#dc3232;font-weight:bold;"><?php echo esc_js( __( 'IMPORTANT: These codes will NOT be shown again!', 'wp-security-pack' ) ); ?></p>';
|
codesHtml += '<p style="color:#dc3232;font-weight:bold;"><?php echo esc_js( __( 'IMPORTANT: These codes will NOT be shown again!', 'security-pack' ) ); ?></p>';
|
||||||
codesHtml += '<pre style="background:#fff;padding:15px;font-size:14px;line-height:1.8;">' + response.data.backup_codes.join('\n') + '</pre>';
|
codesHtml += '<pre style="background:#fff;padding:15px;font-size:14px;line-height:1.8;">' + response.data.backup_codes.join('\n') + '</pre>';
|
||||||
codesHtml += '<p><?php echo esc_js( __( 'Store these codes in a safe place. Each code can only be used once.', 'wp-security-pack' ) ); ?></p>';
|
codesHtml += '<p><?php echo esc_js( __( 'Store these codes in a safe place. Each code can only be used once.', 'security-pack' ) ); ?></p>';
|
||||||
codesHtml += '<button type="button" class="button button-primary" onclick="location.reload();"><?php echo esc_js( __( 'I have saved my codes', 'wp-security-pack' ) ); ?></button>';
|
codesHtml += '<button type="button" class="button button-primary" onclick="location.reload();"><?php echo esc_js( __( 'I have saved my codes', 'security-pack' ) ); ?></button>';
|
||||||
codesHtml += '</div>';
|
codesHtml += '</div>';
|
||||||
$('#wpsp-2fa-setup').html(codesHtml);
|
$('#wpsp-2fa-setup').html(codesHtml);
|
||||||
} else if (response.success) {
|
} else if (response.success) {
|
||||||
@@ -379,7 +400,7 @@ class WPSP_Two_Factor {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('#wpsp-disable-2fa').on('click', function() {
|
$('#wpsp-disable-2fa').on('click', function() {
|
||||||
if (confirm('<?php echo esc_js( __( 'Are you sure you want to disable 2FA?', 'wp-security-pack' ) ); ?>')) {
|
if (confirm('<?php echo esc_js( __( 'Are you sure you want to disable 2FA?', 'security-pack' ) ); ?>')) {
|
||||||
$.post(ajaxurl, {
|
$.post(ajaxurl, {
|
||||||
action: 'wpsp_disable_2fa',
|
action: 'wpsp_disable_2fa',
|
||||||
user_id: <?php echo (int) $user->ID; ?>,
|
user_id: <?php echo (int) $user->ID; ?>,
|
||||||
@@ -393,7 +414,7 @@ class WPSP_Two_Factor {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('#wpsp-regenerate-backup-codes').on('click', function() {
|
$('#wpsp-regenerate-backup-codes').on('click', function() {
|
||||||
if (confirm('<?php echo esc_js( __( 'This will invalidate all existing backup codes. Are you sure?', 'wp-security-pack' ) ); ?>')) {
|
if (confirm('<?php echo esc_js( __( 'This will invalidate all existing backup codes. Are you sure?', 'security-pack' ) ); ?>')) {
|
||||||
$.post(ajaxurl, {
|
$.post(ajaxurl, {
|
||||||
action: 'wpsp_regenerate_backup_codes',
|
action: 'wpsp_regenerate_backup_codes',
|
||||||
user_id: <?php echo (int) $user->ID; ?>,
|
user_id: <?php echo (int) $user->ID; ?>,
|
||||||
@@ -429,7 +450,7 @@ class WPSP_Two_Factor {
|
|||||||
$user_id = isset( $_POST['user_id'] ) ? (int) $_POST['user_id'] : 0;
|
$user_id = isset( $_POST['user_id'] ) ? (int) $_POST['user_id'] : 0;
|
||||||
|
|
||||||
if ( ! current_user_can( 'edit_user', $user_id ) ) {
|
if ( ! current_user_can( 'edit_user', $user_id ) ) {
|
||||||
wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-security-pack' ) ) );
|
wp_send_json_error( array( 'message' => __( 'Permission denied.', 'security-pack' ) ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
$secret = $this->generate_secret();
|
$secret = $this->generate_secret();
|
||||||
@@ -465,18 +486,18 @@ class WPSP_Two_Factor {
|
|||||||
$code = isset( $_POST['code'] ) ? sanitize_text_field( wp_unslash( $_POST['code'] ) ) : '';
|
$code = isset( $_POST['code'] ) ? sanitize_text_field( wp_unslash( $_POST['code'] ) ) : '';
|
||||||
|
|
||||||
if ( ! current_user_can( 'edit_user', $user_id ) ) {
|
if ( ! current_user_can( 'edit_user', $user_id ) ) {
|
||||||
wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-security-pack' ) ) );
|
wp_send_json_error( array( 'message' => __( 'Permission denied.', 'security-pack' ) ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
$pending_secret = get_user_meta( $user_id, self::SECRET_META_KEY . '_pending', true );
|
$pending_secret = get_user_meta( $user_id, self::SECRET_META_KEY . '_pending', true );
|
||||||
|
|
||||||
if ( empty( $pending_secret ) ) {
|
if ( empty( $pending_secret ) ) {
|
||||||
wp_send_json_error( array( 'message' => __( 'No pending setup found.', 'wp-security-pack' ) ) );
|
wp_send_json_error( array( 'message' => __( 'No pending setup found.', 'security-pack' ) ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the code.
|
// Verify the code.
|
||||||
if ( ! $this->verify_totp( $pending_secret, $code ) ) {
|
if ( ! $this->verify_totp( $pending_secret, $code ) ) {
|
||||||
wp_send_json_error( array( 'message' => __( 'Invalid code. Please try again.', 'wp-security-pack' ) ) );
|
wp_send_json_error( array( 'message' => __( 'Invalid code. Please try again.', 'security-pack' ) ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable 2FA.
|
// Enable 2FA.
|
||||||
@@ -490,7 +511,7 @@ class WPSP_Two_Factor {
|
|||||||
update_user_meta( $user_id, self::BACKUP_CODES_META_KEY, $hashed_codes );
|
update_user_meta( $user_id, self::BACKUP_CODES_META_KEY, $hashed_codes );
|
||||||
|
|
||||||
wp_send_json_success( array(
|
wp_send_json_success( array(
|
||||||
'message' => __( '2FA enabled successfully. Save your backup codes now - they will not be shown again!', 'wp-security-pack' ),
|
'message' => __( '2FA enabled successfully. Save your backup codes now - they will not be shown again!', 'security-pack' ),
|
||||||
'backup_codes' => $plain_codes,
|
'backup_codes' => $plain_codes,
|
||||||
'show_codes' => true,
|
'show_codes' => true,
|
||||||
) );
|
) );
|
||||||
@@ -505,14 +526,14 @@ class WPSP_Two_Factor {
|
|||||||
$user_id = isset( $_POST['user_id'] ) ? (int) $_POST['user_id'] : 0;
|
$user_id = isset( $_POST['user_id'] ) ? (int) $_POST['user_id'] : 0;
|
||||||
|
|
||||||
if ( ! current_user_can( 'edit_user', $user_id ) ) {
|
if ( ! current_user_can( 'edit_user', $user_id ) ) {
|
||||||
wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-security-pack' ) ) );
|
wp_send_json_error( array( 'message' => __( 'Permission denied.', 'security-pack' ) ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
delete_user_meta( $user_id, self::SECRET_META_KEY );
|
delete_user_meta( $user_id, self::SECRET_META_KEY );
|
||||||
delete_user_meta( $user_id, self::ENABLED_META_KEY );
|
delete_user_meta( $user_id, self::ENABLED_META_KEY );
|
||||||
delete_user_meta( $user_id, self::BACKUP_CODES_META_KEY );
|
delete_user_meta( $user_id, self::BACKUP_CODES_META_KEY );
|
||||||
|
|
||||||
wp_send_json_success( array( 'message' => __( '2FA disabled.', 'wp-security-pack' ) ) );
|
wp_send_json_success( array( 'message' => __( '2FA disabled.', 'security-pack' ) ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -524,7 +545,7 @@ class WPSP_Two_Factor {
|
|||||||
$user_id = isset( $_POST['user_id'] ) ? (int) $_POST['user_id'] : 0;
|
$user_id = isset( $_POST['user_id'] ) ? (int) $_POST['user_id'] : 0;
|
||||||
|
|
||||||
if ( ! current_user_can( 'edit_user', $user_id ) ) {
|
if ( ! current_user_can( 'edit_user', $user_id ) ) {
|
||||||
wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-security-pack' ) ) );
|
wp_send_json_error( array( 'message' => __( 'Permission denied.', 'security-pack' ) ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate new codes.
|
// Generate new codes.
|
||||||
@@ -537,7 +558,7 @@ class WPSP_Two_Factor {
|
|||||||
// Return plain codes to show user ONCE.
|
// Return plain codes to show user ONCE.
|
||||||
wp_send_json_success( array(
|
wp_send_json_success( array(
|
||||||
'codes' => $plain_codes,
|
'codes' => $plain_codes,
|
||||||
'message' => __( 'Backup codes regenerated. Save these codes now - they will not be shown again.', 'wp-security-pack' ),
|
'message' => __( 'Backup codes regenerated. Save these codes now - they will not be shown again.', 'security-pack' ),
|
||||||
) );
|
) );
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2,5 +2,5 @@
|
|||||||
/**
|
/**
|
||||||
* Silence is golden.
|
* Silence is golden.
|
||||||
*
|
*
|
||||||
* @package WP_Security_Pack
|
* @package Security_Pack
|
||||||
*/
|
*/
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Silence is golden.
|
||||||
|
*
|
||||||
|
* @package Security_Pack
|
||||||
|
*/
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Silence is golden.
|
||||||
|
*
|
||||||
|
* @package Security_Pack
|
||||||
|
*/
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
=== WP Security Pack ===
|
=== Security Pack ===
|
||||||
Contributors: arkhost
|
Contributors: arkhost
|
||||||
Tags: security, firewall, login, 2fa, malware
|
Tags: security, firewall, login, 2fa, malware
|
||||||
Requires at least: 5.0
|
Requires at least: 5.0
|
||||||
@@ -83,7 +83,7 @@ External connections:
|
|||||||
|
|
||||||
== Installation ==
|
== Installation ==
|
||||||
|
|
||||||
1. Upload the plugin files to `/wp-content/plugins/wp-security-pack/`
|
1. Upload the plugin files to `/wp-content/plugins/security-pack/`
|
||||||
2. Activate the plugin through the 'Plugins' screen
|
2. Activate the plugin through the 'Plugins' screen
|
||||||
3. Configure under the Security menu
|
3. Configure under the Security menu
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Plugin Name: WP Security Pack
|
* Plugin Name: Security Pack
|
||||||
* Description: A free, lightweight security plugin with zero upsells. Login protection, IP blocking, hardening, and activity logging.
|
* Description: A free, lightweight security plugin with zero upsells. Login protection, IP blocking, hardening, and activity logging.
|
||||||
* Version: 1.0
|
* Version: 1.0
|
||||||
* Requires at least: 5.0
|
* Requires at least: 5.0
|
||||||
@@ -9,10 +9,10 @@
|
|||||||
* Author URI: https://arkhost.com
|
* Author URI: https://arkhost.com
|
||||||
* License: GPL v2 or later
|
* License: GPL v2 or later
|
||||||
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||||
* Text Domain: wp-security-pack
|
* Text Domain: security-pack
|
||||||
* Domain Path: /languages
|
* Domain Path: /languages
|
||||||
*
|
*
|
||||||
* @package WP_Security_Pack
|
* @package Security_Pack
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Prevent direct access.
|
// Prevent direct access.
|
||||||
@@ -30,12 +30,12 @@ define( 'WPSP_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
|
|||||||
/**
|
/**
|
||||||
* Main plugin class.
|
* Main plugin class.
|
||||||
*/
|
*/
|
||||||
final class WP_Security_Pack {
|
final class Security_Pack {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Single instance.
|
* Single instance.
|
||||||
*
|
*
|
||||||
* @var WP_Security_Pack
|
* @var Security_Pack
|
||||||
*/
|
*/
|
||||||
private static $instance = null;
|
private static $instance = null;
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ final class WP_Security_Pack {
|
|||||||
/**
|
/**
|
||||||
* Get single instance.
|
* Get single instance.
|
||||||
*
|
*
|
||||||
* @return WP_Security_Pack
|
* @return Security_Pack
|
||||||
*/
|
*/
|
||||||
public static function instance() {
|
public static function instance() {
|
||||||
if ( null === self::$instance ) {
|
if ( null === self::$instance ) {
|
||||||
@@ -93,7 +93,6 @@ final class WP_Security_Pack {
|
|||||||
register_activation_hook( WPSP_PLUGIN_FILE, array( 'WPSP_DB', 'activate' ) );
|
register_activation_hook( WPSP_PLUGIN_FILE, array( 'WPSP_DB', 'activate' ) );
|
||||||
register_deactivation_hook( WPSP_PLUGIN_FILE, array( 'WPSP_DB', 'deactivate' ) );
|
register_deactivation_hook( WPSP_PLUGIN_FILE, array( 'WPSP_DB', 'deactivate' ) );
|
||||||
|
|
||||||
add_action( 'plugins_loaded', array( $this, 'load_textdomain' ) );
|
|
||||||
add_action( 'plugins_loaded', array( 'WPSP_DB', 'maybe_upgrade' ) );
|
add_action( 'plugins_loaded', array( 'WPSP_DB', 'maybe_upgrade' ) );
|
||||||
add_action( 'init', array( $this, 'init_components' ), 1 );
|
add_action( 'init', array( $this, 'init_components' ), 1 );
|
||||||
|
|
||||||
@@ -105,17 +104,6 @@ final class WP_Security_Pack {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Load plugin textdomain.
|
|
||||||
*/
|
|
||||||
public function load_textdomain() {
|
|
||||||
load_plugin_textdomain(
|
|
||||||
'wp-security-pack',
|
|
||||||
false,
|
|
||||||
dirname( WPSP_PLUGIN_BASENAME ) . '/languages/'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize plugin components.
|
* Initialize plugin components.
|
||||||
*
|
*
|
||||||
@@ -267,10 +255,10 @@ final class WP_Security_Pack {
|
|||||||
/**
|
/**
|
||||||
* Get plugin instance.
|
* Get plugin instance.
|
||||||
*
|
*
|
||||||
* @return WP_Security_Pack
|
* @return Security_Pack
|
||||||
*/
|
*/
|
||||||
function wpsp() {
|
function wpsp() {
|
||||||
return WP_Security_Pack::instance();
|
return Security_Pack::instance();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize plugin.
|
// Initialize plugin.
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* WP Security Pack Uninstaller.
|
* Security Pack Uninstaller.
|
||||||
*
|
*
|
||||||
* @package WP_Security_Pack
|
* @package Security_Pack
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// If uninstall not called from WordPress, exit.
|
// If uninstall not called from WordPress, exit.
|
||||||
@@ -18,7 +18,9 @@ WPSP_DB::uninstall();
|
|||||||
|
|
||||||
// Clear any remaining transients.
|
// Clear any remaining transients.
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Cleanup during uninstall, caching not applicable.
|
||||||
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_wpsp_%'" );
|
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_wpsp_%'" );
|
||||||
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Cleanup during uninstall, caching not applicable.
|
||||||
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_wpsp_%'" );
|
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_wpsp_%'" );
|
||||||
|
|
||||||
// Clear scheduled events.
|
// Clear scheduled events.
|
||||||
@@ -27,4 +29,5 @@ wp_clear_scheduled_hook( 'wpsp_daily_file_scan' );
|
|||||||
wp_clear_scheduled_hook( 'wpsp_weekly_malware_scan' );
|
wp_clear_scheduled_hook( 'wpsp_weekly_malware_scan' );
|
||||||
|
|
||||||
// Delete user meta for 2FA.
|
// Delete user meta for 2FA.
|
||||||
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Cleanup during uninstall, caching not applicable.
|
||||||
$wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE '_wpsp_%'" );
|
$wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE '_wpsp_%'" );
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# Deny direct access to database files
|
|
||||||
<FilesMatch "\.(bin|BIN|mmdb|MMDB)$">
|
|
||||||
Order Allow,Deny
|
|
||||||
Deny from all
|
|
||||||
</FilesMatch>
|
|
||||||
@@ -1,278 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Activity logging for WP Security Pack.
|
|
||||||
*
|
|
||||||
* @package WP_Security_Pack
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Prevent direct access.
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Activity log class.
|
|
||||||
*/
|
|
||||||
class WPSP_Activity_Log {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Event types.
|
|
||||||
*/
|
|
||||||
const EVENT_LOGIN_SUCCESS = 'login_success';
|
|
||||||
const EVENT_LOGIN_FAILED = 'login_failed';
|
|
||||||
const EVENT_LOCKOUT = 'lockout';
|
|
||||||
const EVENT_IP_BLOCKED = 'ip_blocked';
|
|
||||||
const EVENT_GEO_BLOCKED = 'geo_blocked';
|
|
||||||
const EVENT_LOCKOUT_LIFTED = 'lockout_lifted';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor.
|
|
||||||
*/
|
|
||||||
public function __construct() {
|
|
||||||
// Hooks are set up by Login Protection class.
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log an event.
|
|
||||||
*
|
|
||||||
* @param string $event_type Event type.
|
|
||||||
* @param string|null $ip_address IP address (auto-detected if null).
|
|
||||||
* @param string|null $username Username.
|
|
||||||
* @param string|null $details Additional details.
|
|
||||||
* @return int|false Insert ID or false on failure.
|
|
||||||
*/
|
|
||||||
public static function log( $event_type, $ip_address = null, $username = null, $details = null ) {
|
|
||||||
global $wpdb;
|
|
||||||
|
|
||||||
if ( null === $ip_address ) {
|
|
||||||
$ip_address = WPSP_Helper::get_client_ip();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get user agent.
|
|
||||||
$user_agent = isset( $_SERVER['HTTP_USER_AGENT'] )
|
|
||||||
? substr( sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ), 0, 255 )
|
|
||||||
: '';
|
|
||||||
|
|
||||||
// Get country code if geo-blocking is available.
|
|
||||||
$country_code = null;
|
|
||||||
$geo_blocking = wpsp()->get_component( 'geo_blocking' );
|
|
||||||
if ( $geo_blocking && $ip_address ) {
|
|
||||||
$country_code = $geo_blocking->get_country_code( $ip_address );
|
|
||||||
}
|
|
||||||
|
|
||||||
$table = WPSP_DB::get_log_table();
|
|
||||||
|
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
|
||||||
$result = $wpdb->insert(
|
|
||||||
$table,
|
|
||||||
array(
|
|
||||||
'event_type' => $event_type,
|
|
||||||
'ip_address' => $ip_address ? $ip_address : '',
|
|
||||||
'username' => $username,
|
|
||||||
'user_agent' => $user_agent,
|
|
||||||
'country_code' => $country_code,
|
|
||||||
'details' => $details,
|
|
||||||
'created_at' => current_time( 'mysql' ),
|
|
||||||
),
|
|
||||||
array( '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
|
|
||||||
);
|
|
||||||
|
|
||||||
return $result ? $wpdb->insert_id : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get recent logs.
|
|
||||||
*
|
|
||||||
* @param array $args Query arguments.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function get_logs( $args = array() ) {
|
|
||||||
global $wpdb;
|
|
||||||
|
|
||||||
$defaults = array(
|
|
||||||
'limit' => 50,
|
|
||||||
'offset' => 0,
|
|
||||||
'event_type' => '',
|
|
||||||
'ip_address' => '',
|
|
||||||
'orderby' => 'created_at',
|
|
||||||
'order' => 'DESC',
|
|
||||||
);
|
|
||||||
|
|
||||||
$args = wp_parse_args( $args, $defaults );
|
|
||||||
$table = WPSP_DB::get_log_table();
|
|
||||||
|
|
||||||
$where_clauses = array( '1=1' );
|
|
||||||
$where_values = array();
|
|
||||||
|
|
||||||
if ( ! empty( $args['event_type'] ) ) {
|
|
||||||
$where_clauses[] = 'event_type = %s';
|
|
||||||
$where_values[] = $args['event_type'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ! empty( $args['ip_address'] ) ) {
|
|
||||||
$where_clauses[] = 'ip_address = %s';
|
|
||||||
$where_values[] = $args['ip_address'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$where_sql = implode( ' AND ', $where_clauses );
|
|
||||||
|
|
||||||
// Sanitize orderby.
|
|
||||||
$allowed_orderby = array( 'id', 'event_type', 'ip_address', 'username', 'created_at' );
|
|
||||||
$orderby = in_array( $args['orderby'], $allowed_orderby, true ) ? $args['orderby'] : 'created_at';
|
|
||||||
$order = 'ASC' === strtoupper( $args['order'] ) ? 'ASC' : 'DESC';
|
|
||||||
|
|
||||||
$limit = absint( $args['limit'] );
|
|
||||||
$offset = absint( $args['offset'] );
|
|
||||||
|
|
||||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
||||||
$sql = "SELECT * FROM {$table} WHERE {$where_sql} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d";
|
|
||||||
|
|
||||||
$where_values[] = $limit;
|
|
||||||
$where_values[] = $offset;
|
|
||||||
|
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.NotPrepared
|
|
||||||
return $wpdb->get_results( $wpdb->prepare( $sql, $where_values ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get total log count.
|
|
||||||
*
|
|
||||||
* @param array $args Query arguments.
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public static function get_log_count( $args = array() ) {
|
|
||||||
global $wpdb;
|
|
||||||
|
|
||||||
$table = WPSP_DB::get_log_table();
|
|
||||||
|
|
||||||
$where_clauses = array( '1=1' );
|
|
||||||
$where_values = array();
|
|
||||||
|
|
||||||
if ( ! empty( $args['event_type'] ) ) {
|
|
||||||
$where_clauses[] = 'event_type = %s';
|
|
||||||
$where_values[] = $args['event_type'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ! empty( $args['ip_address'] ) ) {
|
|
||||||
$where_clauses[] = 'ip_address = %s';
|
|
||||||
$where_values[] = $args['ip_address'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$where_sql = implode( ' AND ', $where_clauses );
|
|
||||||
|
|
||||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
||||||
$sql = "SELECT COUNT(*) FROM {$table} WHERE {$where_sql}";
|
|
||||||
|
|
||||||
if ( ! empty( $where_values ) ) {
|
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.NotPrepared
|
|
||||||
return (int) $wpdb->get_var( $wpdb->prepare( $sql, $where_values ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.NotPrepared
|
|
||||||
return (int) $wpdb->get_var( $sql );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get log statistics.
|
|
||||||
*
|
|
||||||
* @param int $days Number of days to look back.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function get_stats( $days = 30 ) {
|
|
||||||
global $wpdb;
|
|
||||||
|
|
||||||
$table = WPSP_DB::get_log_table();
|
|
||||||
$cutoff = gmdate( 'Y-m-d H:i:s', strtotime( "-{$days} days" ) );
|
|
||||||
|
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
|
||||||
$results = $wpdb->get_results(
|
|
||||||
$wpdb->prepare(
|
|
||||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
||||||
"SELECT event_type, COUNT(*) as count FROM {$table} WHERE created_at >= %s GROUP BY event_type",
|
|
||||||
$cutoff
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
$stats = array(
|
|
||||||
'login_success' => 0,
|
|
||||||
'login_failed' => 0,
|
|
||||||
'lockout' => 0,
|
|
||||||
'ip_blocked' => 0,
|
|
||||||
'geo_blocked' => 0,
|
|
||||||
'total' => 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach ( $results as $row ) {
|
|
||||||
$stats[ $row->event_type ] = (int) $row->count;
|
|
||||||
$stats['total'] += (int) $row->count;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup old logs (cron job).
|
|
||||||
*/
|
|
||||||
public static function cleanup_old_logs() {
|
|
||||||
global $wpdb;
|
|
||||||
|
|
||||||
$retention_days = WP_Security_Pack::get_setting( 'log_retention_days', 30 );
|
|
||||||
$cutoff = gmdate( 'Y-m-d H:i:s', strtotime( "-{$retention_days} days" ) );
|
|
||||||
|
|
||||||
$log_table = WPSP_DB::get_log_table();
|
|
||||||
$lockout_table = WPSP_DB::get_lockout_table();
|
|
||||||
|
|
||||||
// Delete old logs.
|
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
|
||||||
$wpdb->query(
|
|
||||||
$wpdb->prepare(
|
|
||||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
||||||
"DELETE FROM {$log_table} WHERE created_at < %s",
|
|
||||||
$cutoff
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Delete expired lockouts (lockout_until stores Unix timestamp as integer).
|
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
|
||||||
$wpdb->query(
|
|
||||||
$wpdb->prepare(
|
|
||||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
||||||
"DELETE FROM {$lockout_table} WHERE lockout_until IS NOT NULL AND lockout_until > 0 AND lockout_until < %d",
|
|
||||||
time()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all logs.
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public static function clear_all_logs() {
|
|
||||||
global $wpdb;
|
|
||||||
|
|
||||||
$table = WPSP_DB::get_log_table();
|
|
||||||
|
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
||||||
return false !== $wpdb->query( "TRUNCATE TABLE {$table}" );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get event type label.
|
|
||||||
*
|
|
||||||
* @param string $event_type Event type.
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public static function get_event_label( $event_type ) {
|
|
||||||
$labels = array(
|
|
||||||
self::EVENT_LOGIN_SUCCESS => __( 'Login Success', 'wp-security-pack' ),
|
|
||||||
self::EVENT_LOGIN_FAILED => __( 'Login Failed', 'wp-security-pack' ),
|
|
||||||
self::EVENT_LOCKOUT => __( 'Lockout', 'wp-security-pack' ),
|
|
||||||
self::EVENT_IP_BLOCKED => __( 'IP Blocked', 'wp-security-pack' ),
|
|
||||||
self::EVENT_GEO_BLOCKED => __( 'Geo Blocked', 'wp-security-pack' ),
|
|
||||||
self::EVENT_LOCKOUT_LIFTED => __( 'Lockout Lifted', 'wp-security-pack' ),
|
|
||||||
);
|
|
||||||
|
|
||||||
return isset( $labels[ $event_type ] ) ? $labels[ $event_type ] : $event_type;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,555 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Helper functions for WP Security Pack.
|
|
||||||
*
|
|
||||||
* @package WP_Security_Pack
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Prevent direct access.
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper class with utility functions.
|
|
||||||
*/
|
|
||||||
class WPSP_Helper {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the real client IP address.
|
|
||||||
*
|
|
||||||
* Handles proxies and CDNs like Cloudflare.
|
|
||||||
*
|
|
||||||
* @return string|null
|
|
||||||
*/
|
|
||||||
public static function get_client_ip() {
|
|
||||||
$ip = null;
|
|
||||||
|
|
||||||
// Priority order for IP detection.
|
|
||||||
$headers = array(
|
|
||||||
'HTTP_CF_CONNECTING_IP', // Cloudflare.
|
|
||||||
'HTTP_X_REAL_IP', // Nginx reverse proxy.
|
|
||||||
'HTTP_X_FORWARDED_FOR', // Generic proxy.
|
|
||||||
'REMOTE_ADDR', // Direct connection.
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach ( $headers as $header ) {
|
|
||||||
if ( ! empty( $_SERVER[ $header ] ) ) {
|
|
||||||
// X-Forwarded-For can contain multiple IPs, get the first one.
|
|
||||||
$ip = sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) );
|
|
||||||
if ( 'HTTP_X_FORWARDED_FOR' === $header && strpos( $ip, ',' ) !== false ) {
|
|
||||||
$ips = explode( ',', $ip );
|
|
||||||
$ip = trim( $ips[0] );
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate IP.
|
|
||||||
if ( $ip && filter_var( $ip, FILTER_VALIDATE_IP ) ) {
|
|
||||||
return $ip;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if an IP is in a CIDR range.
|
|
||||||
*
|
|
||||||
* @param string $ip IP address to check.
|
|
||||||
* @param string $cidr CIDR notation (e.g., 192.168.1.0/24).
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public static function ip_in_cidr( $ip, $cidr ) {
|
|
||||||
// Handle exact IP match (no CIDR notation).
|
|
||||||
if ( strpos( $cidr, '/' ) === false ) {
|
|
||||||
return $ip === $cidr;
|
|
||||||
}
|
|
||||||
|
|
||||||
list( $network, $mask ) = explode( '/', $cidr );
|
|
||||||
|
|
||||||
// Detect IP version.
|
|
||||||
$ip_is_v6 = filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 );
|
|
||||||
$network_is_v6 = filter_var( $network, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 );
|
|
||||||
|
|
||||||
// Both must be same version.
|
|
||||||
if ( $ip_is_v6 !== $network_is_v6 ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( $ip_is_v6 ) {
|
|
||||||
return self::ipv6_in_cidr( $ip, $network, (int) $mask );
|
|
||||||
}
|
|
||||||
|
|
||||||
return self::ipv4_in_cidr( $ip, $network, (int) $mask );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if IPv4 is in CIDR range.
|
|
||||||
*
|
|
||||||
* @param string $ip IPv4 address.
|
|
||||||
* @param string $network Network address.
|
|
||||||
* @param int $mask CIDR mask.
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
private static function ipv4_in_cidr( $ip, $network, $mask ) {
|
|
||||||
$ip_long = ip2long( $ip );
|
|
||||||
$network_long = ip2long( $network );
|
|
||||||
|
|
||||||
if ( false === $ip_long || false === $network_long ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate network mask.
|
|
||||||
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
|
|
||||||
$network_mask = ~( ( 1 << ( 32 - $mask ) ) - 1 );
|
|
||||||
|
|
||||||
return ( $ip_long & $network_mask ) === ( $network_long & $network_mask );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if IPv6 is in CIDR range.
|
|
||||||
*
|
|
||||||
* @param string $ip IPv6 address.
|
|
||||||
* @param string $network Network address.
|
|
||||||
* @param int $mask CIDR mask.
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
private static function ipv6_in_cidr( $ip, $network, $mask ) {
|
|
||||||
$ip_bin = inet_pton( $ip );
|
|
||||||
$network_bin = inet_pton( $network );
|
|
||||||
|
|
||||||
if ( false === $ip_bin || false === $network_bin ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare full bytes.
|
|
||||||
$full_bytes = (int) floor( $mask / 8 );
|
|
||||||
for ( $i = 0; $i < $full_bytes; $i++ ) {
|
|
||||||
if ( $ip_bin[ $i ] !== $network_bin[ $i ] ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare remaining bits.
|
|
||||||
$remaining_bits = $mask % 8;
|
|
||||||
if ( $remaining_bits > 0 && $full_bytes < 16 ) {
|
|
||||||
$bit_mask = ( ( 1 << $remaining_bits ) - 1 ) << ( 8 - $remaining_bits );
|
|
||||||
if ( ( ord( $ip_bin[ $full_bytes ] ) & $bit_mask ) !== ( ord( $network_bin[ $full_bytes ] ) & $bit_mask ) ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if an IP matches any rule in a list.
|
|
||||||
*
|
|
||||||
* @param string $ip IP address to check.
|
|
||||||
* @param array $rules Array of IP addresses or CIDR ranges.
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public static function ip_matches_rules( $ip, $rules ) {
|
|
||||||
if ( empty( $rules ) || ! is_array( $rules ) ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ( $rules as $rule ) {
|
|
||||||
$rule = trim( $rule );
|
|
||||||
if ( empty( $rule ) || strpos( $rule, '#' ) === 0 ) {
|
|
||||||
continue; // Skip empty lines and comments.
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( self::ip_in_cidr( $ip, $rule ) ) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse IP list from textarea.
|
|
||||||
*
|
|
||||||
* @param string $text Textarea content with IPs/CIDRs.
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function parse_ip_list( $text ) {
|
|
||||||
if ( empty( $text ) ) {
|
|
||||||
return array();
|
|
||||||
}
|
|
||||||
|
|
||||||
$lines = explode( "\n", $text );
|
|
||||||
$ips = array();
|
|
||||||
|
|
||||||
foreach ( $lines as $line ) {
|
|
||||||
$line = trim( $line );
|
|
||||||
|
|
||||||
// Skip empty lines and comments.
|
|
||||||
if ( empty( $line ) || strpos( $line, '#' ) === 0 ) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove inline comments.
|
|
||||||
if ( strpos( $line, '#' ) !== false ) {
|
|
||||||
$line = trim( substr( $line, 0, strpos( $line, '#' ) ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate IP or CIDR.
|
|
||||||
if ( self::is_valid_ip_or_cidr( $line ) ) {
|
|
||||||
$ips[] = $line;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $ips;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a string is a valid IP or CIDR.
|
|
||||||
*
|
|
||||||
* @param string $value Value to check.
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public static function is_valid_ip_or_cidr( $value ) {
|
|
||||||
// Plain IP.
|
|
||||||
if ( filter_var( $value, FILTER_VALIDATE_IP ) ) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// CIDR notation.
|
|
||||||
if ( strpos( $value, '/' ) !== false ) {
|
|
||||||
list( $ip, $mask ) = explode( '/', $value );
|
|
||||||
|
|
||||||
if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$mask = (int) $mask;
|
|
||||||
$is_ipv6 = filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 );
|
|
||||||
$max_mask = $is_ipv6 ? 128 : 32;
|
|
||||||
|
|
||||||
return $mask >= 0 && $mask <= $max_mask;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get country list for geo-blocking UI.
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public static function get_countries() {
|
|
||||||
return array(
|
|
||||||
'AF' => __( 'Afghanistan', 'wp-security-pack' ),
|
|
||||||
'AL' => __( 'Albania', 'wp-security-pack' ),
|
|
||||||
'DZ' => __( 'Algeria', 'wp-security-pack' ),
|
|
||||||
'AS' => __( 'American Samoa', 'wp-security-pack' ),
|
|
||||||
'AD' => __( 'Andorra', 'wp-security-pack' ),
|
|
||||||
'AO' => __( 'Angola', 'wp-security-pack' ),
|
|
||||||
'AI' => __( 'Anguilla', 'wp-security-pack' ),
|
|
||||||
'AQ' => __( 'Antarctica', 'wp-security-pack' ),
|
|
||||||
'AG' => __( 'Antigua and Barbuda', 'wp-security-pack' ),
|
|
||||||
'AR' => __( 'Argentina', 'wp-security-pack' ),
|
|
||||||
'AM' => __( 'Armenia', 'wp-security-pack' ),
|
|
||||||
'AW' => __( 'Aruba', 'wp-security-pack' ),
|
|
||||||
'AU' => __( 'Australia', 'wp-security-pack' ),
|
|
||||||
'AT' => __( 'Austria', 'wp-security-pack' ),
|
|
||||||
'AZ' => __( 'Azerbaijan', 'wp-security-pack' ),
|
|
||||||
'BS' => __( 'Bahamas', 'wp-security-pack' ),
|
|
||||||
'BH' => __( 'Bahrain', 'wp-security-pack' ),
|
|
||||||
'BD' => __( 'Bangladesh', 'wp-security-pack' ),
|
|
||||||
'BB' => __( 'Barbados', 'wp-security-pack' ),
|
|
||||||
'BY' => __( 'Belarus', 'wp-security-pack' ),
|
|
||||||
'BE' => __( 'Belgium', 'wp-security-pack' ),
|
|
||||||
'BZ' => __( 'Belize', 'wp-security-pack' ),
|
|
||||||
'BJ' => __( 'Benin', 'wp-security-pack' ),
|
|
||||||
'BM' => __( 'Bermuda', 'wp-security-pack' ),
|
|
||||||
'BT' => __( 'Bhutan', 'wp-security-pack' ),
|
|
||||||
'BO' => __( 'Bolivia', 'wp-security-pack' ),
|
|
||||||
'BA' => __( 'Bosnia and Herzegovina', 'wp-security-pack' ),
|
|
||||||
'BW' => __( 'Botswana', 'wp-security-pack' ),
|
|
||||||
'BR' => __( 'Brazil', 'wp-security-pack' ),
|
|
||||||
'BN' => __( 'Brunei', 'wp-security-pack' ),
|
|
||||||
'BG' => __( 'Bulgaria', 'wp-security-pack' ),
|
|
||||||
'BF' => __( 'Burkina Faso', 'wp-security-pack' ),
|
|
||||||
'BI' => __( 'Burundi', 'wp-security-pack' ),
|
|
||||||
'KH' => __( 'Cambodia', 'wp-security-pack' ),
|
|
||||||
'CM' => __( 'Cameroon', 'wp-security-pack' ),
|
|
||||||
'CA' => __( 'Canada', 'wp-security-pack' ),
|
|
||||||
'CV' => __( 'Cape Verde', 'wp-security-pack' ),
|
|
||||||
'KY' => __( 'Cayman Islands', 'wp-security-pack' ),
|
|
||||||
'CF' => __( 'Central African Republic', 'wp-security-pack' ),
|
|
||||||
'TD' => __( 'Chad', 'wp-security-pack' ),
|
|
||||||
'CL' => __( 'Chile', 'wp-security-pack' ),
|
|
||||||
'CN' => __( 'China', 'wp-security-pack' ),
|
|
||||||
'CO' => __( 'Colombia', 'wp-security-pack' ),
|
|
||||||
'KM' => __( 'Comoros', 'wp-security-pack' ),
|
|
||||||
'CG' => __( 'Congo', 'wp-security-pack' ),
|
|
||||||
'CD' => __( 'Congo (DRC)', 'wp-security-pack' ),
|
|
||||||
'CR' => __( 'Costa Rica', 'wp-security-pack' ),
|
|
||||||
'CI' => __( 'Ivory Coast', 'wp-security-pack' ),
|
|
||||||
'HR' => __( 'Croatia', 'wp-security-pack' ),
|
|
||||||
'CU' => __( 'Cuba', 'wp-security-pack' ),
|
|
||||||
'CY' => __( 'Cyprus', 'wp-security-pack' ),
|
|
||||||
'CZ' => __( 'Czech Republic', 'wp-security-pack' ),
|
|
||||||
'DK' => __( 'Denmark', 'wp-security-pack' ),
|
|
||||||
'DJ' => __( 'Djibouti', 'wp-security-pack' ),
|
|
||||||
'DM' => __( 'Dominica', 'wp-security-pack' ),
|
|
||||||
'DO' => __( 'Dominican Republic', 'wp-security-pack' ),
|
|
||||||
'EC' => __( 'Ecuador', 'wp-security-pack' ),
|
|
||||||
'EG' => __( 'Egypt', 'wp-security-pack' ),
|
|
||||||
'SV' => __( 'El Salvador', 'wp-security-pack' ),
|
|
||||||
'GQ' => __( 'Equatorial Guinea', 'wp-security-pack' ),
|
|
||||||
'ER' => __( 'Eritrea', 'wp-security-pack' ),
|
|
||||||
'EE' => __( 'Estonia', 'wp-security-pack' ),
|
|
||||||
'ET' => __( 'Ethiopia', 'wp-security-pack' ),
|
|
||||||
'FJ' => __( 'Fiji', 'wp-security-pack' ),
|
|
||||||
'FI' => __( 'Finland', 'wp-security-pack' ),
|
|
||||||
'FR' => __( 'France', 'wp-security-pack' ),
|
|
||||||
'GA' => __( 'Gabon', 'wp-security-pack' ),
|
|
||||||
'GM' => __( 'Gambia', 'wp-security-pack' ),
|
|
||||||
'GE' => __( 'Georgia', 'wp-security-pack' ),
|
|
||||||
'DE' => __( 'Germany', 'wp-security-pack' ),
|
|
||||||
'GH' => __( 'Ghana', 'wp-security-pack' ),
|
|
||||||
'GR' => __( 'Greece', 'wp-security-pack' ),
|
|
||||||
'GL' => __( 'Greenland', 'wp-security-pack' ),
|
|
||||||
'GD' => __( 'Grenada', 'wp-security-pack' ),
|
|
||||||
'GU' => __( 'Guam', 'wp-security-pack' ),
|
|
||||||
'GT' => __( 'Guatemala', 'wp-security-pack' ),
|
|
||||||
'GN' => __( 'Guinea', 'wp-security-pack' ),
|
|
||||||
'GW' => __( 'Guinea-Bissau', 'wp-security-pack' ),
|
|
||||||
'GY' => __( 'Guyana', 'wp-security-pack' ),
|
|
||||||
'HT' => __( 'Haiti', 'wp-security-pack' ),
|
|
||||||
'HN' => __( 'Honduras', 'wp-security-pack' ),
|
|
||||||
'HK' => __( 'Hong Kong', 'wp-security-pack' ),
|
|
||||||
'HU' => __( 'Hungary', 'wp-security-pack' ),
|
|
||||||
'IS' => __( 'Iceland', 'wp-security-pack' ),
|
|
||||||
'IN' => __( 'India', 'wp-security-pack' ),
|
|
||||||
'ID' => __( 'Indonesia', 'wp-security-pack' ),
|
|
||||||
'IR' => __( 'Iran', 'wp-security-pack' ),
|
|
||||||
'IQ' => __( 'Iraq', 'wp-security-pack' ),
|
|
||||||
'IE' => __( 'Ireland', 'wp-security-pack' ),
|
|
||||||
'IL' => __( 'Israel', 'wp-security-pack' ),
|
|
||||||
'IT' => __( 'Italy', 'wp-security-pack' ),
|
|
||||||
'JM' => __( 'Jamaica', 'wp-security-pack' ),
|
|
||||||
'JP' => __( 'Japan', 'wp-security-pack' ),
|
|
||||||
'JO' => __( 'Jordan', 'wp-security-pack' ),
|
|
||||||
'KZ' => __( 'Kazakhstan', 'wp-security-pack' ),
|
|
||||||
'KE' => __( 'Kenya', 'wp-security-pack' ),
|
|
||||||
'KI' => __( 'Kiribati', 'wp-security-pack' ),
|
|
||||||
'KP' => __( 'North Korea', 'wp-security-pack' ),
|
|
||||||
'KR' => __( 'South Korea', 'wp-security-pack' ),
|
|
||||||
'KW' => __( 'Kuwait', 'wp-security-pack' ),
|
|
||||||
'KG' => __( 'Kyrgyzstan', 'wp-security-pack' ),
|
|
||||||
'LA' => __( 'Laos', 'wp-security-pack' ),
|
|
||||||
'LV' => __( 'Latvia', 'wp-security-pack' ),
|
|
||||||
'LB' => __( 'Lebanon', 'wp-security-pack' ),
|
|
||||||
'LS' => __( 'Lesotho', 'wp-security-pack' ),
|
|
||||||
'LR' => __( 'Liberia', 'wp-security-pack' ),
|
|
||||||
'LY' => __( 'Libya', 'wp-security-pack' ),
|
|
||||||
'LI' => __( 'Liechtenstein', 'wp-security-pack' ),
|
|
||||||
'LT' => __( 'Lithuania', 'wp-security-pack' ),
|
|
||||||
'LU' => __( 'Luxembourg', 'wp-security-pack' ),
|
|
||||||
'MO' => __( 'Macao', 'wp-security-pack' ),
|
|
||||||
'MK' => __( 'North Macedonia', 'wp-security-pack' ),
|
|
||||||
'MG' => __( 'Madagascar', 'wp-security-pack' ),
|
|
||||||
'MW' => __( 'Malawi', 'wp-security-pack' ),
|
|
||||||
'MY' => __( 'Malaysia', 'wp-security-pack' ),
|
|
||||||
'MV' => __( 'Maldives', 'wp-security-pack' ),
|
|
||||||
'ML' => __( 'Mali', 'wp-security-pack' ),
|
|
||||||
'MT' => __( 'Malta', 'wp-security-pack' ),
|
|
||||||
'MH' => __( 'Marshall Islands', 'wp-security-pack' ),
|
|
||||||
'MR' => __( 'Mauritania', 'wp-security-pack' ),
|
|
||||||
'MU' => __( 'Mauritius', 'wp-security-pack' ),
|
|
||||||
'MX' => __( 'Mexico', 'wp-security-pack' ),
|
|
||||||
'FM' => __( 'Micronesia', 'wp-security-pack' ),
|
|
||||||
'MD' => __( 'Moldova', 'wp-security-pack' ),
|
|
||||||
'MC' => __( 'Monaco', 'wp-security-pack' ),
|
|
||||||
'MN' => __( 'Mongolia', 'wp-security-pack' ),
|
|
||||||
'ME' => __( 'Montenegro', 'wp-security-pack' ),
|
|
||||||
'MA' => __( 'Morocco', 'wp-security-pack' ),
|
|
||||||
'MZ' => __( 'Mozambique', 'wp-security-pack' ),
|
|
||||||
'MM' => __( 'Myanmar', 'wp-security-pack' ),
|
|
||||||
'NA' => __( 'Namibia', 'wp-security-pack' ),
|
|
||||||
'NR' => __( 'Nauru', 'wp-security-pack' ),
|
|
||||||
'NP' => __( 'Nepal', 'wp-security-pack' ),
|
|
||||||
'NL' => __( 'Netherlands', 'wp-security-pack' ),
|
|
||||||
'NZ' => __( 'New Zealand', 'wp-security-pack' ),
|
|
||||||
'NI' => __( 'Nicaragua', 'wp-security-pack' ),
|
|
||||||
'NE' => __( 'Niger', 'wp-security-pack' ),
|
|
||||||
'NG' => __( 'Nigeria', 'wp-security-pack' ),
|
|
||||||
'NO' => __( 'Norway', 'wp-security-pack' ),
|
|
||||||
'OM' => __( 'Oman', 'wp-security-pack' ),
|
|
||||||
'PK' => __( 'Pakistan', 'wp-security-pack' ),
|
|
||||||
'PW' => __( 'Palau', 'wp-security-pack' ),
|
|
||||||
'PS' => __( 'Palestine', 'wp-security-pack' ),
|
|
||||||
'PA' => __( 'Panama', 'wp-security-pack' ),
|
|
||||||
'PG' => __( 'Papua New Guinea', 'wp-security-pack' ),
|
|
||||||
'PY' => __( 'Paraguay', 'wp-security-pack' ),
|
|
||||||
'PE' => __( 'Peru', 'wp-security-pack' ),
|
|
||||||
'PH' => __( 'Philippines', 'wp-security-pack' ),
|
|
||||||
'PL' => __( 'Poland', 'wp-security-pack' ),
|
|
||||||
'PT' => __( 'Portugal', 'wp-security-pack' ),
|
|
||||||
'PR' => __( 'Puerto Rico', 'wp-security-pack' ),
|
|
||||||
'QA' => __( 'Qatar', 'wp-security-pack' ),
|
|
||||||
'RO' => __( 'Romania', 'wp-security-pack' ),
|
|
||||||
'RU' => __( 'Russia', 'wp-security-pack' ),
|
|
||||||
'RW' => __( 'Rwanda', 'wp-security-pack' ),
|
|
||||||
'SA' => __( 'Saudi Arabia', 'wp-security-pack' ),
|
|
||||||
'SN' => __( 'Senegal', 'wp-security-pack' ),
|
|
||||||
'RS' => __( 'Serbia', 'wp-security-pack' ),
|
|
||||||
'SC' => __( 'Seychelles', 'wp-security-pack' ),
|
|
||||||
'SL' => __( 'Sierra Leone', 'wp-security-pack' ),
|
|
||||||
'SG' => __( 'Singapore', 'wp-security-pack' ),
|
|
||||||
'SK' => __( 'Slovakia', 'wp-security-pack' ),
|
|
||||||
'SI' => __( 'Slovenia', 'wp-security-pack' ),
|
|
||||||
'SB' => __( 'Solomon Islands', 'wp-security-pack' ),
|
|
||||||
'SO' => __( 'Somalia', 'wp-security-pack' ),
|
|
||||||
'ZA' => __( 'South Africa', 'wp-security-pack' ),
|
|
||||||
'SS' => __( 'South Sudan', 'wp-security-pack' ),
|
|
||||||
'ES' => __( 'Spain', 'wp-security-pack' ),
|
|
||||||
'LK' => __( 'Sri Lanka', 'wp-security-pack' ),
|
|
||||||
'SD' => __( 'Sudan', 'wp-security-pack' ),
|
|
||||||
'SR' => __( 'Suriname', 'wp-security-pack' ),
|
|
||||||
'SZ' => __( 'Eswatini', 'wp-security-pack' ),
|
|
||||||
'SE' => __( 'Sweden', 'wp-security-pack' ),
|
|
||||||
'CH' => __( 'Switzerland', 'wp-security-pack' ),
|
|
||||||
'SY' => __( 'Syria', 'wp-security-pack' ),
|
|
||||||
'TW' => __( 'Taiwan', 'wp-security-pack' ),
|
|
||||||
'TJ' => __( 'Tajikistan', 'wp-security-pack' ),
|
|
||||||
'TZ' => __( 'Tanzania', 'wp-security-pack' ),
|
|
||||||
'TH' => __( 'Thailand', 'wp-security-pack' ),
|
|
||||||
'TL' => __( 'Timor-Leste', 'wp-security-pack' ),
|
|
||||||
'TG' => __( 'Togo', 'wp-security-pack' ),
|
|
||||||
'TO' => __( 'Tonga', 'wp-security-pack' ),
|
|
||||||
'TT' => __( 'Trinidad and Tobago', 'wp-security-pack' ),
|
|
||||||
'TN' => __( 'Tunisia', 'wp-security-pack' ),
|
|
||||||
'TR' => __( 'Turkey', 'wp-security-pack' ),
|
|
||||||
'TM' => __( 'Turkmenistan', 'wp-security-pack' ),
|
|
||||||
'TV' => __( 'Tuvalu', 'wp-security-pack' ),
|
|
||||||
'UG' => __( 'Uganda', 'wp-security-pack' ),
|
|
||||||
'UA' => __( 'Ukraine', 'wp-security-pack' ),
|
|
||||||
'AE' => __( 'United Arab Emirates', 'wp-security-pack' ),
|
|
||||||
'GB' => __( 'United Kingdom', 'wp-security-pack' ),
|
|
||||||
'US' => __( 'United States', 'wp-security-pack' ),
|
|
||||||
'UY' => __( 'Uruguay', 'wp-security-pack' ),
|
|
||||||
'UZ' => __( 'Uzbekistan', 'wp-security-pack' ),
|
|
||||||
'VU' => __( 'Vanuatu', 'wp-security-pack' ),
|
|
||||||
'VE' => __( 'Venezuela', 'wp-security-pack' ),
|
|
||||||
'VN' => __( 'Vietnam', 'wp-security-pack' ),
|
|
||||||
'YE' => __( 'Yemen', 'wp-security-pack' ),
|
|
||||||
'ZM' => __( 'Zambia', 'wp-security-pack' ),
|
|
||||||
'ZW' => __( 'Zimbabwe', 'wp-security-pack' ),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sanitize a custom login URL slug.
|
|
||||||
*
|
|
||||||
* @param string $slug The slug to sanitize.
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public static function sanitize_login_slug( $slug ) {
|
|
||||||
$slug = sanitize_title( $slug );
|
|
||||||
$slug = preg_replace( '/[^a-z0-9\-]/', '', $slug );
|
|
||||||
|
|
||||||
// Prevent common reserved slugs.
|
|
||||||
$reserved = array( 'wp-admin', 'wp-login', 'admin', 'login', 'wp-content', 'wp-includes' );
|
|
||||||
if ( in_array( $slug, $reserved, true ) ) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the server's public IP address.
|
|
||||||
*
|
|
||||||
* Uses external service to determine the server's outbound IP.
|
|
||||||
* Result is cached for 1 hour to avoid excessive external requests.
|
|
||||||
*
|
|
||||||
* @return string|null Server IP or null on failure.
|
|
||||||
*/
|
|
||||||
public static function get_server_ip() {
|
|
||||||
$cached = get_transient( 'wpsp_server_ip' );
|
|
||||||
if ( false !== $cached ) {
|
|
||||||
return $cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try multiple services for reliability.
|
|
||||||
$services = array(
|
|
||||||
'https://api.ipify.org',
|
|
||||||
'https://ifconfig.me/ip',
|
|
||||||
'https://icanhazip.com',
|
|
||||||
);
|
|
||||||
|
|
||||||
$ip = null;
|
|
||||||
|
|
||||||
foreach ( $services as $service ) {
|
|
||||||
$response = wp_remote_get(
|
|
||||||
$service,
|
|
||||||
array(
|
|
||||||
'timeout' => 5,
|
|
||||||
'sslverify' => true,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) ) {
|
|
||||||
$body = trim( wp_remote_retrieve_body( $response ) );
|
|
||||||
if ( filter_var( $body, FILTER_VALIDATE_IP ) ) {
|
|
||||||
$ip = $body;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( $ip ) {
|
|
||||||
// Cache for 1 hour.
|
|
||||||
set_transient( 'wpsp_server_ip', $ip, HOUR_IN_SECONDS );
|
|
||||||
}
|
|
||||||
|
|
||||||
return $ip;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get country flag emoji from country code.
|
|
||||||
*
|
|
||||||
* Converts ISO 3166-1 alpha-2 country codes to Unicode flag emojis.
|
|
||||||
*
|
|
||||||
* @param string $country_code Two-letter country code (e.g., "US", "GB").
|
|
||||||
* @return string Flag emoji or globe emoji for invalid codes.
|
|
||||||
*/
|
|
||||||
public static function get_country_flag( $country_code ) {
|
|
||||||
if ( empty( $country_code ) || strlen( $country_code ) !== 2 ) {
|
|
||||||
return '🌐'; // Globe emoji for invalid codes.
|
|
||||||
}
|
|
||||||
|
|
||||||
$country_code = strtoupper( $country_code );
|
|
||||||
|
|
||||||
// Convert country code to Unicode regional indicator symbols.
|
|
||||||
// Regional indicators are U+1F1E6 (A) through U+1F1FF (Z).
|
|
||||||
$first_letter = mb_chr( ord( $country_code[0] ) - ord( 'A' ) + 0x1F1E6, 'UTF-8' );
|
|
||||||
$second_letter = mb_chr( ord( $country_code[1] ) - ord( 'A' ) + 0x1F1E6, 'UTF-8' );
|
|
||||||
|
|
||||||
return $first_letter . $second_letter;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get countries array with flags.
|
|
||||||
*
|
|
||||||
* @return array Country code => "Flag Name" format.
|
|
||||||
*/
|
|
||||||
public static function get_countries_with_flags() {
|
|
||||||
$countries = self::get_countries();
|
|
||||||
$countries_flags = array();
|
|
||||||
|
|
||||||
foreach ( $countries as $code => $name ) {
|
|
||||||
$flag = self::get_country_flag( $code );
|
|
||||||
$countries_flags[ $code ] = $flag . ' ' . $name;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $countries_flags;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Silence is golden.
|
|
||||||
*
|
|
||||||
* @package WP_Security_Pack
|
|
||||||
*/
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Silence is golden.
|
|
||||||
*
|
|
||||||
* @package WP_Security_Pack
|
|
||||||
*/
|
|
||||||
|
Before Width: | Height: | Size: 667 KiB |