Files
Mykyta Synelnikov fc2c5456e4 Implement directory privacy settings and add rate limiting
Adds configurable privacy options for member directories, allowing restrictions on visibility based on roles or login status. Introduces rate limiting for unauthenticated AJAX requests to prevent brute-force attacks or abuse.
2025-12-11 17:36:42 +02:00

391 lines
16 KiB
PHP

<?php
/**
* Usermeta which we use:
*
* um_user_blocked__metadata
* um_user_blocked
* um_user_blocked__timestamp
*
* um_secure_has_reset_password
* um_secure_has_reset_password__timestamp
*/
namespace um\admin;
use WP_Session_Tokens;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'um\admin\Secure' ) ) {
/**
* Class Secure
*
* @package um\admin
*
* @since 2.6.8
*/
class Secure {
/**
* Used for flushing user metas.
*
* @var bool
*/
private $need_flush_meta = false;
/**
* Secure constructor.
*
* @since 2.6.8
*/
public function __construct() {
add_action( 'admin_init', array( $this, 'admin_init' ) );
add_filter( 'um_settings_structure', array( $this, 'add_settings' ) );
add_filter( 'manage_users_custom_column', array( $this, 'add_restore_account' ), 9999, 3 );
add_filter( 'pre_get_users', array( $this, 'filter_users_by_date_registered' ) );
add_action( 'um_settings_before_save', array( $this, 'check_secure_changes' ) );
add_action( 'um_settings_save', array( $this, 'on_settings_save' ) );
add_action( 'wp_ajax_um_secure_scan_affected_users', array( $this, 'ajax_scanner' ) );
}
/**
* Filter users by Register Date
*
* @since 2.6.8
* @param object $query WP query `pre_get_users`
*/
public function filter_users_by_date_registered( $query ) {
global $pagenow;
if ( 'users.php' === $pagenow && is_admin() ) {
// phpcs:disable WordPress.Security.NonceVerification
$date_from = isset( $_GET['um_secure_date_from'] ) ? $_GET['um_secure_date_from'] : null;
$date_to = isset( $_GET['um_secure_date_to'] ) ? $_GET['um_secure_date_to'] : null;
// phpcs:enable WordPress.Security.NonceVerification
if ( $date_from ) {
$date_query_attr = array(
'after' => gmdate( get_option( 'date_format', 'F j, Y' ), strtotime( '-1 day', $date_from ) ),
'before' => gmdate( get_option( 'date_format', 'F j, Y' ), strtotime( '+1 day', $date_from ) ),
);
if ( $date_to ) {
$date_query_attr['before'] = gmdate( get_option( 'date_format', 'F j, Y' ), strtotime( '+1 day', $date_to ) );
}
$query->set( 'date_query', $date_query_attr );
}
}
return $query;
}
/**
* Handle secure actions.
*
* @since 2.6.8
*/
public function admin_init() {
global $wpdb;
if ( isset( $_REQUEST['um_secure_expire_all_sessions'] ) && ! wp_doing_ajax() ) {
if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'um-secure-expire-session-nonce' ) || ! current_user_can( 'manage_options' ) ) {
// This nonce is not valid or current logged-in user has no administrative rights.
wp_die( esc_html__( 'Security check', 'ultimate-member' ) );
}
/**
* Destroy all user sessions except the current logged-in user.
*/
$wpdb->query(
$wpdb->prepare(
"DELETE
FROM {$wpdb->usermeta}
WHERE meta_key='session_tokens' AND
user_id != %d",
get_current_user_id()
)
);
if ( UM()->options()->get( 'display_login_form_notice' ) ) {
global $wpdb;
$wpdb->query(
$wpdb->prepare(
"DELETE
FROM {$wpdb->usermeta}
WHERE user_id != %d AND
( meta_key = 'um_secure_has_reset_password' OR meta_key = 'um_secure_has_reset_password__timestamp' )",
get_current_user_id()
)
);
}
wp_safe_redirect( add_query_arg( 'update', 'um_secure_expire_sessions', wp_get_referer() ) );
exit;
}
if ( isset( $_REQUEST['um_secure_restore_account'], $_REQUEST['user_id'] ) && ! wp_doing_ajax() ) {
$user_id = absint( $_REQUEST['user_id'] );
if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'um-security-restore-account-nonce-' . $user_id ) || ! current_user_can( 'manage_options' ) ) {
// This nonce is not valid or current logged-in user has no administrative rights.
wp_die( esc_html__( 'Security check', 'ultimate-member' ) );
}
$user = get_userdata( $user_id );
if ( ! $user ) {
wp_die( esc_html__( 'Invalid user.', 'ultimate-member' ) );
}
um_fetch_user( $user_id );
$metadata = get_user_meta( $user_id, 'um_user_blocked__metadata', true );
$user->update_user_level_from_caps();
// Restore Roles.
if ( isset( $metadata['roles'] ) ) {
foreach ( $metadata['roles'] as $role ) {
$user->add_role( $role );
}
}
// Restore Account Status.
if ( isset( $metadata['account_status'] ) ) {
// Force update of the user status without email notifications.
UM()->common()->users()->set_status( $user_id, $metadata['account_status'] );
}
// Delete blocked meta.
delete_user_meta( $user_id, 'um_user_blocked__metadata' );
delete_user_meta( $user_id, 'um_user_blocked' );
delete_user_meta( $user_id, 'um_user_blocked__timestamp' );
// Don't need to reset a password.
if ( UM()->options()->get( 'display_login_form_notice' ) ) {
update_user_meta( $user_id, 'um_secure_has_reset_password', true );
update_user_meta( $user_id, 'um_secure_has_reset_password__timestamp', current_time( 'mysql', true ) );
}
// Clear Cache.
UM()->user()->remove_cache( $user_id );
um_reset_user();
wp_safe_redirect( add_query_arg( 'update', 'um_secure_restore', wp_get_referer() ) );
exit;
}
}
/**
* Register Secure Settings
*
* @since 2.6.8
*
* @param array $settings
* @return array
*/
public function add_settings( $settings ) {
$nonce = wp_create_nonce( 'um-secure-expire-session-nonce' );
$banned_capabilities = array();
$banned_admin_capabilities = UM()->common()->secure()->get_banned_capabilities_list();
foreach ( $banned_admin_capabilities as $cap ) {
$banned_capabilities[ $cap ] = $cap;
}
$disabled_capabilities = UM()->options()->get_default( 'banned_capabilities' );
$disabled_capabilities_text = '<strong>' . implode( '</strong>, <strong>', $disabled_capabilities ) . '</strong>';
$scanner_content = '<button class="button um-secure-scan-content">' . esc_html__( 'Scan Now', 'ultimate-member' ) . '</button>';
$scanner_content .= '<span class="um-secure-scan-results">';
$scanner_content .= esc_html__( 'Last scan:', 'ultimate-member' ) . ' ';
$scan_status = get_option( 'um_secure_scan_status' );
$last_scanned_time = get_option( 'um_secure_last_time_scanned' );
if ( ! empty( $last_scanned_time ) ) {
$scanner_content .= human_time_diff( strtotime( $last_scanned_time ) ) . ' ' . esc_html__( 'ago', 'ultimate-member' );
if ( 'started' === $scan_status ) {
$scanner_content .= ' - ' . esc_html__( 'Not Completed.', 'ultimate-member' );
}
} else {
$scanner_content .= esc_html__( 'Not Scanned yet.', 'ultimate-member' );
}
$scanner_content .= '</span>';
$secure_fields = array(
array(
'id' => 'ajax_nopriv_rate_limit',
'type' => 'checkbox',
'label' => __( 'Rate Limit', 'ultimate-member' ),
'checkbox_label' => __( 'Enable Rate Limiting', 'ultimate-member' ),
'description' => __( 'This prevents brute-force enumeration attempts in guest AJAX requests.', 'ultimate-member' ),
),
array(
'id' => 'banned_capabilities',
'type' => 'multi_checkbox',
'multi' => true,
'assoc' => true,
'checkbox_key' => true,
'columns' => 2,
'options_disabled' => $disabled_capabilities,
'options' => $banned_capabilities,
'label' => __( 'Banned Administrative Capabilities', 'ultimate-member' ),
// translators: %s are disabled default capabilities that are enabled by default.
'description' => sprintf( __( 'All the above are default Administrator & Super Admin capabilities. When someone tries to inject capabilities to the Account, Profile & Register forms submission, it will be flagged with this option. The %s capabilities are locked to ensure no users will be created with these capabilities.', 'ultimate-member' ), $disabled_capabilities_text ),
),
array(
'id' => 'secure_scan_affected_users',
'type' => 'info_text',
'label' => __( 'Scanner', 'ultimate-member' ),
'value' => $scanner_content,
'description' => __( 'Scan your site to check for vulnerabilities prior to Ultimate Member version 2.6.7 and get recommendations to secure your site.', 'ultimate-member' ),
),
array(
'id' => 'lock_register_forms',
'type' => 'checkbox',
'label' => __( 'Lock All Register Forms', 'ultimate-member' ),
'checkbox_label' => __( 'Lock Forms', 'ultimate-member' ),
'description' => __( 'This prevents all users from registering with Ultimate Member on your site.', 'ultimate-member' ),
),
array(
'id' => 'display_login_form_notice',
'type' => 'checkbox',
'label' => __( 'Display Login form notice to reset passwords', 'ultimate-member' ),
'checkbox_label' => __( 'Enable Login form notice', 'ultimate-member' ),
'description' => __( 'Enforces users to reset their passwords (one-time) and prevent from entering old password.', 'ultimate-member' ),
),
);
global $wpdb;
$count_users = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->users}" );
$count_users_exclude_me = $count_users - 1;
if ( $count_users_exclude_me > 0 ) {
$secure_fields[] = array(
'id' => 'force_reset_passwords',
'type' => 'info_text',
'label' => __( 'Expire All Users Sessions', 'ultimate-member' ),
// translators: %d is the users count.
'value' => '<a class="button um_secure_force_reset_passwords" href="' . admin_url( '?um_secure_expire_all_sessions=1&_wpnonce=' . esc_attr( $nonce ) ) . '" onclick=\'return confirm("' . esc_js( __( 'Are you sure that you want to make all users sessions expired?', 'ultimate-member' ) ) . '");\'>' . esc_html( sprintf( __( 'Logout Users (%d)', 'ultimate-member' ), $count_users_exclude_me ) ) . '</a>',
'description' => __( 'This will log out all users on your site and forces them to reset passwords <br/>when <strong>"Display Login form notice to reset passwords" is enabled/checked.</strong>', 'ultimate-member' ),
);
}
$secure_fields = array_merge(
$secure_fields,
array(
array(
'id' => 'secure_ban_admins_accounts',
'type' => 'checkbox',
'label' => __( 'Administrative capabilities ban', 'ultimate-member' ),
'checkbox_label' => __( 'Enable ban for administrative capabilities', 'ultimate-member' ),
'description' => __( ' When someone tries to inject capabilities to the Account, Profile & Register forms submission, it will be banned.', 'ultimate-member' ),
),
array(
'id' => 'secure_notify_admins_banned_accounts',
'type' => 'checkbox',
'label' => __( 'Notify Administrators', 'ultimate-member' ),
'checkbox_label' => __( 'Enable notification', 'ultimate-member' ),
'description' => __( 'When enabled, All administrators will be notified when someone has suspicious activities in the Account, Profile & Register forms.', 'ultimate-member' ),
'conditional' => array( 'secure_ban_admins_accounts', '=', 1 ),
),
array(
'id' => 'secure_notify_admins_banned_accounts__interval',
'type' => 'select',
'options' => array(
'instant' => __( 'Send Immediately', 'ultimate-member' ),
'hourly' => __( 'Hourly', 'ultimate-member' ),
'daily' => __( 'Daily', 'ultimate-member' ),
),
'label' => __( 'Notification Schedule', 'ultimate-member' ),
'conditional' => array( 'secure_notify_admins_banned_accounts', '=', 1 ),
),
array(
'id' => 'secure_allowed_redirect_hosts',
'type' => 'textarea',
'label' => __( 'Allowed hosts for safe redirect (one host per line)', 'ultimate-member' ),
'description' => __( 'Extend allowed hosts for frontend pages redirects.', 'ultimate-member' ),
),
)
);
$settings['advanced']['sections'] = UM()->array_insert_before(
$settings['advanced']['sections'],
'developers',
array(
'security' => array(
'title' => __( 'Security', 'ultimate-member' ),
'description' => __( 'This feature scans for suspicious registered accounts, bans the usage of administrative capabilities to site subscribers/members, allows the website administrators to force all users to reset their passwords, preventing users from logging-in using their old passwords that may have been exposed.', 'ultimate-member' ),
'fields' => $secure_fields,
),
)
);
return $settings;
}
/**
* Append blocked status to the `account_status` column rows.
*
* @param string $val Default column row value.
* @param string $column_name Current column name.
* @param int $user_id User ID in loop.
*
* @since 2.6.8
*
* @return string
*/
public function add_restore_account( $val, $column_name, $user_id ) {
if ( 'account_status' === $column_name ) {
um_fetch_user( $user_id );
$is_blocked = um_user( 'um_user_blocked' );
$account_status = UM()->common()->users()->get_status( $user_id );
if ( ! empty( $is_blocked ) && in_array( $account_status, array( 'rejected', 'inactive' ), true ) ) {
$datetime = um_user( 'um_user_blocked__timestamp' );
$val .= '<div><small>' . esc_html__( 'Blocked Due to Suspicious Activity', 'ultimate-member' ) . '</small></div>';
$nonce = wp_create_nonce( 'um-security-restore-account-nonce-' . $user_id );
$restore_account_url = admin_url( 'users.php?user_id=' . $user_id . '&um_secure_restore_account=1&_wpnonce=' . $nonce );
$action = ' &#183; <a href=" ' . esc_url( $restore_account_url ) . ' " onclick=\'return confirm("' . esc_js( __( 'Are you sure that you want to restore this account after getting flagged for suspicious activity?', 'ultimate-member' ) ) . '");\'><small>' . esc_html__( 'Restore Account', 'ultimate-member' ) . '</small></a>';
if ( ! empty( $datetime ) ) {
$val .= '<div><small>' . human_time_diff( strtotime( $datetime ) ) . ' ' . __( 'ago', 'ultimate-member' ) . '</small>' . $action . '</div>';
}
}
um_reset_user();
}
return $val;
}
/**
*
*/
public function check_secure_changes() {
if ( isset( $_POST['um_options']['display_login_form_notice'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification
$current_option_value = UM()->options()->get( 'display_login_form_notice' );
if ( empty( $current_option_value ) ) {
return;
}
if ( empty( $_POST['um_options']['display_login_form_notice'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification
$this->need_flush_meta = true;
}
}
}
/**
*
*/
public function on_settings_save() {
if ( isset( $_POST['um_options']['display_login_form_notice'] ) && ! empty( $this->need_flush_meta ) ) { //phpcs:ignore WordPress.Security.NonceVerification
global $wpdb;
$wpdb->query(
"DELETE FROM {$wpdb->usermeta} WHERE meta_key = 'um_secure_has_reset_password' OR meta_key = 'um_secure_has_reset_password__timestamp'"
);
}
if ( isset( $_POST['um_options']['secure_notify_admins_banned_accounts'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification
if ( ! empty( $_POST['um_options']['secure_notify_admins_banned_accounts'] ) ) { //phpcs:ignore WordPress.Security.NonceVerification
UM()->options()->update( 'suspicious-activity_on', 1 );
} else {
UM()->options()->update( 'suspicious-activity_on', 0 );
}
}
}
}
}