From a2a56c8af76a7f6f8f521fe76d848649496be67f Mon Sep 17 00:00:00 2001 From: Champ Camba Date: Sat, 1 Jul 2023 17:48:45 +0800 Subject: [PATCH 01/45] Add secure class for security measures --- includes/class-init.php | 15 ++ includes/core/class-secure.php | 279 +++++++++++++++++++++++++++++++++ 2 files changed, 294 insertions(+) create mode 100644 includes/core/class-secure.php diff --git a/includes/class-init.php b/includes/class-init.php index 3c839c9a..7a3a0c27 100644 --- a/includes/class-init.php +++ b/includes/class-init.php @@ -607,6 +607,7 @@ if ( ! class_exists( 'UM' ) ) { $this->gdpr(); $this->member_directory(); $this->blocks(); + $this->secure(); //if multisite networks active if ( is_multisite() ) { @@ -650,6 +651,20 @@ if ( ! class_exists( 'UM' ) ) { } + /** + * @since 2.6.7 + * + * @return um\core\Secure() + */ + public function secure() { + if ( empty( $this->classes['secure'] ) ) { + $this->classes['secure'] = new um\core\Secure(); + } + + return $this->classes['secure']; + } + + /** * Get extension API * diff --git a/includes/core/class-secure.php b/includes/core/class-secure.php new file mode 100644 index 00000000..f6f40c88 --- /dev/null +++ b/includes/core/class-secure.php @@ -0,0 +1,279 @@ +drop_sessions(); + wp_safe_redirect( admin_url() ); + exit; + } + + } + + /** + * Add Login notice for Reset Password + * + * @param array $args + * @since 2.6.7 + */ + public function reset_password_notice( $args ) { + + if ( ! UM()->options()->get( 'display_login_form_notice' ) ) { + return; + } + + // phpcs:disable WordPress.Security.NonceVerification + if ( ! isset( $_REQUEST['notice'] ) || 'expired_password' !== $_REQUEST['notice'] ) { + return; + } + // phpcs:enable WordPress.Security.NonceVerification + + echo "

"; + echo wp_kses( + sprintf( + // translators: One-time change requires you to reset your password + __( 'Important: Your password has expired. This (one-time) change requires you to reset your password. Please click here to reset your password via Email.', 'ultimate-member' ), + um_get_core_page( 'password-reset' ) + ), + array( + 'strong' => array(), + 'a' => array( + 'href' => array(), + ), + ) + ); + echo '

'; + } + + /** + * Add Login notice for Under Maintance + * + * @param array $args + * @since 2.6.7 + */ + public function under_maintanance_notice( $args ) { + + if ( ! UM()->options()->get( 'lock_register_forms' ) ) { + return; + } + + // phpcs:disable WordPress.Security.NonceVerification + if ( ! isset( $_REQUEST['notice'] ) || 'maintanance' !== $_REQUEST['notice'] ) { + return; + } + // phpcs:enable WordPress.Security.NonceVerification + + echo "

"; + echo wp_kses( + sprintf( + // translators: One-time change requires you to reset your password + __( 'Important: This site is currently under maintance. Come back soon.', 'ultimate-member' ), + um_get_core_page( 'password-reset' ) + ), + array( + 'strong' => array(), + 'a' => array( + 'href' => array(), + ), + ) + ); + echo '

'; + } + + /** + * Register Secure Settings + * + * @param array $settings + * @since 2.6.7 + */ + public function add_settings( $settings ) { + $nonce = wp_create_nonce( 'um-secure-expire-session-nonce' ); + $count_users = count_users(); + $settings['secure']['title'] = __( 'Secure', 'ultimate-member' ); + $settings['secure']['sections'] = + array( + '' => + array( + 'title' => __( 'Secure Ultimate Member', 'ultimate-member' ), + 'fields' => array( + array( + 'id' => 'lock_register_forms', + 'type' => 'checkbox', + 'label' => __( 'Lock All Register Forms', 'ultimate-member' ), + 'description' => __( 'This prevents all users from registering with Ultimate Member on your site temporarily.', 'ultimate-member' ), + ), + array( + 'id' => 'display_login_form_notice', + 'type' => 'checkbox', + 'label' => __( 'Display Login form notice to reset their passwords', 'ultimate-member' ), + 'description' => __( 'Enforces users to reset their passwords( one-time ) and prevent from entering old password.', 'ultimate-member' ), + ), + array( + 'id' => 'force_reset_passwords', + 'type' => 'info_text', + 'label' => __( 'Expire All Users Sessions', 'ultimate-member' ), + 'value' => 'Logout Users(' . esc_attr( $count_users['total_users'] ) . ') ', + 'description' => __( 'This will logout all users on your site and forces them to reset passwords when "Display Login form notice to reset their passwords" is enabled.', 'ultimate-member' ), + ), + ), + ), + ); + + return $settings; + } + + /** + * Block all UM Register form submissions. + * + * @param array $args Form settings. + * @since 2.6.7 + */ + public function block_register_forms( $args ) { + if ( UM()->options()->get( 'lock_register_forms' ) ) { + $login_url = add_query_arg( 'notice', 'maintanance', um_get_core_page( 'login' ) ); + nocache_headers(); + wp_safe_redirect( $login_url ); + exit; + } + } + + /** + * Validate when user has expired password + * + * @param array $submitted_data + * @since 2.6.7 + */ + public function login_validate_expired_pass( $submitted_data ) { + + if ( UM()->options()->get( 'display_login_form_notice' ) ) { + $has_expired = get_user_meta( um_user( 'ID' ), 'um_secure_has_reset_password', true ); + if ( ! $has_expired ) { + $login_url = add_query_arg( 'notice', 'expired_password', um_get_core_page( 'login' ) ); + wp_safe_redirect( $login_url ); + exit; + } + } + } + + /** + * Prevent users from using Old Passwords on Password Reset form + * + * @param object $errors + * @param object $user + * @since 2.6.7 + */ + public function avoid_old_password( $errors, $user ) { + $wp_hasher = new \PasswordHash( 8, true ); + + if ( isset( $_REQUEST['user_password'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification + $new_user_pass = $_REQUEST['user_password']; // phpcs:ignore WordPress.Security.NonceVerification + if ( $wp_hasher->CheckPassword( $new_user_pass, $user->data->user_pass ) ) { + UM()->form()->add_error( 'user_password', __( 'Your new password cannot be same as old password.', 'ultimate-member' ) ); + $errors->add( 'invalid_old_password', __( 'Your new password cannot be same as old password.', 'ultimate-member' ) ); + } else { + update_user_meta( $user->data->ID, 'um_secure_has_reset_password', true ); + update_user_meta( $user->data->ID, 'um_secure_has_reset_password__timestamp', current_time( 'mysql' ) ); + } + } + } + + /** + * Checks user capabilities and remove administrative ones + */ + public function check_user_capabilities( $user_id, $submitted_data, $form_data ) { + + // Fetch the WP_User object of our user. + um_fetch_user( $user_id ); + $user = new \WP_User( $user_id ); + $has_admin_cap = false; + $disallowed_roles = array( 'administrator', 'editor' ); + foreach ( $disallowed_roles as $role ) { + $admin_caps = get_role( $role )->capabilities; + foreach ( $admin_caps as $cap ) { + if ( user_can( $user_id, $cap ) ) { + $has_admin_cap = true; + $this->revoke_caps( $cap, $user ); + } + } + } + + if ( user_can( $user_id, 'manage_options' ) ) { + $this->revoke_caps( $cap, $user ); + $has_admin_cap = true; + } + + $arr_levels = array( 'level_10', 'level_9', 'level_8' ); + foreach ( $arr_levels as $level ) { + if ( user_can( $user_id, $level ) ) { + $this->revoke_caps( $level, $user ); + $has_admin_cap = true; + } + } + + if ( $has_admin_cap ) { + wp_die( esc_html__( 'Security Check!', 'ultimate-member' ) ); + } + } + + /** + * Revoka Caps + * + * @param string $cap Capability slug + * @param object $user \WP_User + */ + public function revoke_caps( $cap, $user ) { + $user->remove_cap( $cap ); //revoke editable capabilities + $user->set_role( 'rejected' ); // Set role to rejected + UM()->user()->set_status( 'rejected' ); // Set UM role to rejected + } + + } + + +} From 77cd63ca75e3af8952fafa799c1fb92a2b941385 Mon Sep 17 00:00:00 2001 From: Champ Camba Date: Sat, 1 Jul 2023 17:52:55 +0800 Subject: [PATCH 02/45] Tweak cap --- includes/core/class-secure.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/core/class-secure.php b/includes/core/class-secure.php index f6f40c88..948d393b 100644 --- a/includes/core/class-secure.php +++ b/includes/core/class-secure.php @@ -244,7 +244,7 @@ if ( ! class_exists( 'um\core\Secure' ) ) { } if ( user_can( $user_id, 'manage_options' ) ) { - $this->revoke_caps( $cap, $user ); + $this->revoke_caps( 'manage_options', $user ); $has_admin_cap = true; } From fefdc5ecb8ccef4ae5e4b04c784628b79142b0b2 Mon Sep 17 00:00:00 2001 From: Champ Camba Date: Sat, 1 Jul 2023 18:02:16 +0800 Subject: [PATCH 03/45] Set array_keys to expose capabilities slugs --- includes/core/class-secure.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/core/class-secure.php b/includes/core/class-secure.php index 948d393b..86dd3d7f 100644 --- a/includes/core/class-secure.php +++ b/includes/core/class-secure.php @@ -234,8 +234,8 @@ if ( ! class_exists( 'um\core\Secure' ) ) { $has_admin_cap = false; $disallowed_roles = array( 'administrator', 'editor' ); foreach ( $disallowed_roles as $role ) { - $admin_caps = get_role( $role )->capabilities; - foreach ( $admin_caps as $cap ) { + $admin_caps = array_keys( get_role( $role )->capabilities ); + foreach ( $admin_caps as $i => $cap ) { if ( user_can( $user_id, $cap ) ) { $has_admin_cap = true; $this->revoke_caps( $cap, $user ); From 5c1b6d99e1b094c19b416420d0a4011424afd308 Mon Sep 17 00:00:00 2001 From: Champ Camba Date: Sat, 1 Jul 2023 18:19:02 +0800 Subject: [PATCH 04/45] Tweak text --- includes/core/class-secure.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/core/class-secure.php b/includes/core/class-secure.php index 86dd3d7f..15ae12db 100644 --- a/includes/core/class-secure.php +++ b/includes/core/class-secure.php @@ -113,7 +113,7 @@ if ( ! class_exists( 'um\core\Secure' ) ) { echo wp_kses( sprintf( // translators: One-time change requires you to reset your password - __( 'Important: This site is currently under maintance. Come back soon.', 'ultimate-member' ), + __( 'Important: This site is currently under maintenance. Please check back soon.', 'ultimate-member' ), um_get_core_page( 'password-reset' ) ), array( From 9b3447ab54d076c86932619bc267b664fb42ed07 Mon Sep 17 00:00:00 2001 From: Champ Camba Date: Mon, 3 Jul 2023 15:38:29 +0800 Subject: [PATCH 05/45] Initial update --- includes/core/class-secure.php | 73 ++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 30 deletions(-) diff --git a/includes/core/class-secure.php b/includes/core/class-secure.php index 15ae12db..baf9ccea 100644 --- a/includes/core/class-secure.php +++ b/includes/core/class-secure.php @@ -5,7 +5,6 @@ if ( ! defined( 'ABSPATH' ) ) { exit; } - if ( ! class_exists( 'um\core\Secure' ) ) { /** @@ -13,13 +12,13 @@ if ( ! class_exists( 'um\core\Secure' ) ) { * * @package um\core * - * @since 2.6.7 + * @since 2.6.8 */ class Secure { /** * Login constructor. - * @since 2.6.7 + * @since 2.6.8 */ public function __construct() { add_action( 'admin_init', array( $this, 'admin_init' ) ); @@ -36,9 +35,14 @@ if ( ! class_exists( 'um\core\Secure' ) ) { add_action( 'validate_password_reset', array( $this, 'avoid_old_password' ), 1, 2 ); - add_action( 'um_after_save_registration_details', array( $this, 'check_user_capabilities' ), 10, 3 ); + add_action( 'um_after_save_registration_details', array( $this, 'secure_user_capabilities' ), 10, 3 ); } + /** + * Admin Init + * + * @since 2.6.8 + */ public function admin_init() { if ( isset( $_REQUEST['um_secure_expire_all_sessions'] ) ) { if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'um-secure-expire-session-nonce' ) ) { @@ -60,7 +64,7 @@ if ( ! class_exists( 'um\core\Secure' ) ) { * Add Login notice for Reset Password * * @param array $args - * @since 2.6.7 + * @since 2.6.8 */ public function reset_password_notice( $args ) { @@ -95,7 +99,7 @@ if ( ! class_exists( 'um\core\Secure' ) ) { * Add Login notice for Under Maintance * * @param array $args - * @since 2.6.7 + * @since 2.6.8 */ public function under_maintanance_notice( $args ) { @@ -130,7 +134,7 @@ if ( ! class_exists( 'um\core\Secure' ) ) { * Register Secure Settings * * @param array $settings - * @since 2.6.7 + * @since 2.6.8 */ public function add_settings( $settings ) { $nonce = wp_create_nonce( 'um-secure-expire-session-nonce' ); @@ -172,7 +176,7 @@ if ( ! class_exists( 'um\core\Secure' ) ) { * Block all UM Register form submissions. * * @param array $args Form settings. - * @since 2.6.7 + * @since 2.6.8 */ public function block_register_forms( $args ) { if ( UM()->options()->get( 'lock_register_forms' ) ) { @@ -187,7 +191,7 @@ if ( ! class_exists( 'um\core\Secure' ) ) { * Validate when user has expired password * * @param array $submitted_data - * @since 2.6.7 + * @since 2.6.8 */ public function login_validate_expired_pass( $submitted_data ) { @@ -206,7 +210,7 @@ if ( ! class_exists( 'um\core\Secure' ) ) { * * @param object $errors * @param object $user - * @since 2.6.7 + * @since 2.6.8 */ public function avoid_old_password( $errors, $user ) { $wp_hasher = new \PasswordHash( 8, true ); @@ -215,7 +219,7 @@ if ( ! class_exists( 'um\core\Secure' ) ) { $new_user_pass = $_REQUEST['user_password']; // phpcs:ignore WordPress.Security.NonceVerification if ( $wp_hasher->CheckPassword( $new_user_pass, $user->data->user_pass ) ) { UM()->form()->add_error( 'user_password', __( 'Your new password cannot be same as old password.', 'ultimate-member' ) ); - $errors->add( 'invalid_old_password', __( 'Your new password cannot be same as old password.', 'ultimate-member' ) ); + $errors->add( 'block_old_password', __( 'Your new password cannot be same as old password.', 'ultimate-member' ) ); } else { update_user_meta( $user->data->ID, 'um_secure_has_reset_password', true ); update_user_meta( $user->data->ID, 'um_secure_has_reset_password__timestamp', current_time( 'mysql' ) ); @@ -224,35 +228,43 @@ if ( ! class_exists( 'um\core\Secure' ) ) { } /** - * Checks user capabilities and remove administrative ones + * Secure user capabilities and revoke administrative ones */ - public function check_user_capabilities( $user_id, $submitted_data, $form_data ) { - + public function secure_user_capabilities( $user_id, $submitted_data, $form_data ) { + global $wpdb; // Fetch the WP_User object of our user. um_fetch_user( $user_id ); $user = new \WP_User( $user_id ); $has_admin_cap = false; - $disallowed_roles = array( 'administrator', 'editor' ); + $disallowed_roles = array( 'administrator' ); foreach ( $disallowed_roles as $role ) { $admin_caps = array_keys( get_role( $role )->capabilities ); foreach ( $admin_caps as $i => $cap ) { - if ( user_can( $user_id, $cap ) ) { + /** + * When there's at least one administrator cap added to the user, + * immediately revoke caps and mark as rejected. + */ + if ( $user->has_cap( $cap ) ) { $has_admin_cap = true; - $this->revoke_caps( $cap, $user ); + $this->revoke_caps( $user ); + break; } } } - if ( user_can( $user_id, 'manage_options' ) ) { - $this->revoke_caps( 'manage_options', $user ); - $has_admin_cap = true; - } - - $arr_levels = array( 'level_10', 'level_9', 'level_8' ); - foreach ( $arr_levels as $level ) { - if ( user_can( $user_id, $level ) ) { - $this->revoke_caps( $level, $user ); - $has_admin_cap = true; + /** + * Double-check if *_user_level has been modified with the highest level + * when user has no administrator capabilities. + */ + $user_level = um_user( $wpdb->get_blog_prefix() . 'user_level' ); + if ( ! empty( $user_level ) ) { + $arr_levels = array( 'level_10' ); + foreach ( $arr_levels as $level ) { + if ( $level === $user_level ) { + $this->revoke_caps( $user ); + $has_admin_cap = true; + break; + } } } @@ -262,13 +274,14 @@ if ( ! class_exists( 'um\core\Secure' ) ) { } /** - * Revoka Caps + * Revoke Caps * * @param string $cap Capability slug * @param object $user \WP_User */ - public function revoke_caps( $cap, $user ) { - $user->remove_cap( $cap ); //revoke editable capabilities + public function revoke_caps( $user ) { + + $user->remove_all_caps(); $user->set_role( 'rejected' ); // Set role to rejected UM()->user()->set_status( 'rejected' ); // Set UM role to rejected } From 1313d592518f2017afdc31df58f215a53f651354 Mon Sep 17 00:00:00 2001 From: Champ Camba Date: Mon, 3 Jul 2023 16:48:36 +0800 Subject: [PATCH 06/45] Add banned capabilities --- includes/core/class-secure.php | 110 ++++++++++++++++++++++++++------- 1 file changed, 88 insertions(+), 22 deletions(-) diff --git a/includes/core/class-secure.php b/includes/core/class-secure.php index baf9ccea..3eaa14e4 100644 --- a/includes/core/class-secure.php +++ b/includes/core/class-secure.php @@ -16,6 +16,15 @@ if ( ! class_exists( 'um\core\Secure' ) ) { */ class Secure { + /** + * Banned Administrative Capabilities + * + * @since 2.6.8 + * + * @var array + */ + private $banned_admin_capabilities = array(); + /** * Login constructor. * @since 2.6.8 @@ -35,6 +44,70 @@ if ( ! class_exists( 'um\core\Secure' ) ) { add_action( 'validate_password_reset', array( $this, 'avoid_old_password' ), 1, 2 ); + /** + * UM hook + * + * @type filter + * @title um_secure_register_form_banned_capabilities + * @description Modify banned capabilities for Register forms + * @input_vars + * [{"var":"$capabilities","type":"array","desc":"WordPress Administratrive Capabilities"}] + * @change_log + * ["Since: 2.6.8"] + * @usage + * + * @example + * + */ + $this->banned_admin_capabilities = apply_filters( + 'um_secure_register_form_banned_capabilities', + array( + 'create_sites', + 'delete_sites', + 'manage_network', + 'manage_sites', + 'manage_network_users', + 'manage_network_plugins', + 'manage_network_themes', + 'manage_network_options', + 'upgrade_network', + 'setup_network', + 'activate_plugins', + 'edit_dashboard', + 'edit_theme_options', + 'export', + 'import', + 'list_users', + 'manage_options', + 'promote_users', + 'remove_users', + 'switch_themes', + 'customize', + 'delete_site', + 'update_core', + 'update_plugins', + 'update_themes', + 'install_plugins', + 'install_themes', + 'delete_themes', + 'delete_plugins', + 'edit_plugins', + 'edit_themes', + 'edit_files', + 'edit_users', + 'add_users', + 'create_users', + 'delete_users', + ) + ); + add_action( 'um_after_save_registration_details', array( $this, 'secure_user_capabilities' ), 10, 3 ); } @@ -150,12 +223,12 @@ if ( ! class_exists( 'um\core\Secure' ) ) { 'id' => 'lock_register_forms', 'type' => 'checkbox', 'label' => __( 'Lock All Register Forms', 'ultimate-member' ), - 'description' => __( 'This prevents all users from registering with Ultimate Member on your site temporarily.', '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 their passwords', 'ultimate-member' ), + 'label' => __( 'Display Login form notice to reset passwords', 'ultimate-member' ), 'description' => __( 'Enforces users to reset their passwords( one-time ) and prevent from entering old password.', 'ultimate-member' ), ), array( @@ -163,7 +236,7 @@ if ( ! class_exists( 'um\core\Secure' ) ) { 'type' => 'info_text', 'label' => __( 'Expire All Users Sessions', 'ultimate-member' ), 'value' => 'Logout Users(' . esc_attr( $count_users['total_users'] ) . ') ', - 'description' => __( 'This will logout all users on your site and forces them to reset passwords when "Display Login form notice to reset their passwords" is enabled.', 'ultimate-member' ), + 'description' => __( 'This will logout all users on your site and forces them to reset passwords
when "Display Login form notice to reset passwords" is enabled/checked.', 'ultimate-member' ), ), ), ), @@ -233,13 +306,11 @@ if ( ! class_exists( 'um\core\Secure' ) ) { public function secure_user_capabilities( $user_id, $submitted_data, $form_data ) { global $wpdb; // Fetch the WP_User object of our user. - um_fetch_user( $user_id ); - $user = new \WP_User( $user_id ); - $has_admin_cap = false; - $disallowed_roles = array( 'administrator' ); - foreach ( $disallowed_roles as $role ) { - $admin_caps = array_keys( get_role( $role )->capabilities ); - foreach ( $admin_caps as $i => $cap ) { + um_fetch_user( 29 ); + $user = new \WP_User( 29 ); + $has_admin_cap = false; + if ( ! empty( $this->banned_admin_capabilities ) ) { + foreach ( $this->banned_admin_capabilities as $i => $cap ) { /** * When there's at least one administrator cap added to the user, * immediately revoke caps and mark as rejected. @@ -254,17 +325,13 @@ if ( ! class_exists( 'um\core\Secure' ) ) { /** * Double-check if *_user_level has been modified with the highest level - * when user has no administrator capabilities. + * when user has no administrative capabilities. */ $user_level = um_user( $wpdb->get_blog_prefix() . 'user_level' ); if ( ! empty( $user_level ) ) { - $arr_levels = array( 'level_10' ); - foreach ( $arr_levels as $level ) { - if ( $level === $user_level ) { - $this->revoke_caps( $user ); - $has_admin_cap = true; - break; - } + if ( 10 === $user_level ) { + $this->revoke_caps( $user ); + $has_admin_cap = true; } } @@ -274,16 +341,15 @@ if ( ! class_exists( 'um\core\Secure' ) ) { } /** - * Revoke Caps + * Revoke Caps & Mark rejected as suspicious * * @param string $cap Capability slug * @param object $user \WP_User */ public function revoke_caps( $user ) { - $user->remove_all_caps(); - $user->set_role( 'rejected' ); // Set role to rejected - UM()->user()->set_status( 'rejected' ); // Set UM role to rejected + UM()->user()->set_status( 'rejected' ); + update_user_meta( $user->get( 'ID' ), 'um_user_blocked', 'suspicious_activity' ); } } From a7fe0b17f08158d0387fb8ea7e2e142f5faacb02 Mon Sep 17 00:00:00 2001 From: Champ Camba Date: Mon, 3 Jul 2023 16:54:16 +0800 Subject: [PATCH 07/45] Remove test account --- includes/core/class-secure.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/core/class-secure.php b/includes/core/class-secure.php index 3eaa14e4..e6c79fc1 100644 --- a/includes/core/class-secure.php +++ b/includes/core/class-secure.php @@ -306,8 +306,8 @@ if ( ! class_exists( 'um\core\Secure' ) ) { public function secure_user_capabilities( $user_id, $submitted_data, $form_data ) { global $wpdb; // Fetch the WP_User object of our user. - um_fetch_user( 29 ); - $user = new \WP_User( 29 ); + um_fetch_user( $user_id ); + $user = new \WP_User( $user_id ); $has_admin_cap = false; if ( ! empty( $this->banned_admin_capabilities ) ) { foreach ( $this->banned_admin_capabilities as $i => $cap ) { From 2ac32d921d75e65bbf0f5312d75e009ef0ba4939 Mon Sep 17 00:00:00 2001 From: Champ Camba Date: Mon, 3 Jul 2023 19:38:16 +0800 Subject: [PATCH 08/45] Add blocked status & time in the account status column --- includes/core/class-secure.php | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/includes/core/class-secure.php b/includes/core/class-secure.php index e6c79fc1..058fc7fe 100644 --- a/includes/core/class-secure.php +++ b/includes/core/class-secure.php @@ -109,6 +109,8 @@ if ( ! class_exists( 'um\core\Secure' ) ) { ); add_action( 'um_after_save_registration_details', array( $this, 'secure_user_capabilities' ), 10, 3 ); + + add_filter( 'manage_users_custom_column', array( $this, 'manage_users_custom_column' ), 10, 3 ); } /** @@ -302,6 +304,7 @@ if ( ! class_exists( 'um\core\Secure' ) ) { /** * Secure user capabilities and revoke administrative ones + * @since 2.6.8 */ public function secure_user_capabilities( $user_id, $submitted_data, $form_data ) { global $wpdb; @@ -345,11 +348,39 @@ if ( ! class_exists( 'um\core\Secure' ) ) { * * @param string $cap Capability slug * @param object $user \WP_User + * + * @since 2.6.8 */ public function revoke_caps( $user ) { $user->remove_all_caps(); UM()->user()->set_status( 'rejected' ); update_user_meta( $user->get( 'ID' ), 'um_user_blocked', 'suspicious_activity' ); + update_user_meta( $user->get( 'ID' ), 'um_user_blocked__datetime', current_time( 'mysql' ) ); + } + + /** + * Append blocked status to the `account_status` column rows + * + * @param string $val Default column row value + * @param string $column_name Current column name + * @param integer $user_id User ID in loop + * + * @since 2.6.8 + * + * @return string $val + */ + public function manage_users_custom_column( $val, $column_name, $user_id ) { + if ( 'account_status' === $column_name ) { + $is_blocked = get_user_meta( $user_id, 'um_user_blocked', true ); + if ( ! empty( $is_blocked ) ) { + $datetime = get_user_meta( $user_id, 'um_user_blocked__datetime', true ); + $val = $val . '
' . __( 'Blocked Due to Suspicious Activity', 'ultimate-member' ) . '
'; + if ( ! empty( $datetime ) ) { + $val = $val . '
' . human_time_diff( strtotime( $datetime ), strtotime( current_time( 'mysql' ) ) ) . ' ' . __( 'ago', 'ultimate-member' ) . '
'; + } + } + } + return $val; } } From 85b08f45b35253e246624b8452f34e13bc457e43 Mon Sep 17 00:00:00 2001 From: Champ Camba Date: Mon, 3 Jul 2023 23:28:49 +0800 Subject: [PATCH 09/45] Add Notification with Schedules --- includes/core/class-secure.php | 305 ++++++++++++++++++++++++++++++--- 1 file changed, 284 insertions(+), 21 deletions(-) diff --git a/includes/core/class-secure.php b/includes/core/class-secure.php index 058fc7fe..a2343cf8 100644 --- a/includes/core/class-secure.php +++ b/includes/core/class-secure.php @@ -1,6 +1,8 @@ banned_admin_capabilities as $i => $cap ) { + if ( in_array( $cap, array( 'manage_options', 'promote_users', 'level_10' ), true ) ) { + continue; + } + $banned_admin_capabilities_options[ $cap ] = $cap; + } + $settings['secure']['sections'] = array( '' => @@ -240,6 +282,33 @@ if ( ! class_exists( 'um\core\Secure' ) ) { 'value' => 'Logout Users(' . esc_attr( $count_users['total_users'] ) . ') ', 'description' => __( 'This will logout all users on your site and forces them to reset passwords
when "Display Login form notice to reset passwords" is enabled/checked.', 'ultimate-member' ), ), + array( + 'id' => 'banned_capabilities', + 'type' => 'multi_checkbox', + 'multi' => true, + 'columns' => 2, + 'options' => $banned_admin_capabilities_options, + 'value' => UM()->options()->get( 'banned_capabilities' ) ? array_keys( UM()->options()->get( 'banned_capabilities' ) ) : array_keys( $banned_admin_capabilities_options ), + 'label' => __( 'Banned Administrative Capabilities', 'ultimate-member' ), + 'description' => __( 'All the above are default Administrator & Super Admin capabilities. When someone tries to inject capabilities to the Profile & Register form submission, it will be flagged with this option. The manage_options, promote_users & level_10 capabilities are locked to ensure no users will be created with these capabilities.', 'ultimate-member' ), + ), + array( + 'id' => 'secure_notify_admins_banned_accounts', + 'type' => 'checkbox', + 'label' => __( 'Notify Administrators', 'ultimate-member' ), + 'description' => __( 'When enabled, All administrators will be notified when someone has suspicious activities in Profile & Register forms.', 'ultimate-member' ), + ), + 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 ), + ), ), ), ); @@ -306,23 +375,33 @@ if ( ! class_exists( 'um\core\Secure' ) ) { * Secure user capabilities and revoke administrative ones * @since 2.6.8 */ - public function secure_user_capabilities( $user_id, $submitted_data, $form_data ) { + public function secure_user_capabilities( $user_id ) { global $wpdb; // Fetch the WP_User object of our user. um_fetch_user( $user_id ); - $user = new \WP_User( $user_id ); - $has_admin_cap = false; - if ( ! empty( $this->banned_admin_capabilities ) ) { - foreach ( $this->banned_admin_capabilities as $i => $cap ) { - /** - * When there's at least one administrator cap added to the user, - * immediately revoke caps and mark as rejected. - */ - if ( $user->has_cap( $cap ) ) { - $has_admin_cap = true; - $this->revoke_caps( $user ); - break; - } + $user = new \WP_User( $user_id ); + $has_admin_cap = false; + $arr_banned_caps = array(); + + if ( UM()->options()->get( 'banned_capabilities' ) ) { + $arr_banned_caps = array_keys( UM()->options()->get( 'banned_capabilities' ) ); + } else { + $arr_banned_caps = $this->banned_admin_capabilities; + } + + // Add locked administratrive capabilities + $arr_banned_caps[] = 'manage_options'; + $arr_banned_caps[] = 'promote_users'; + + foreach ( $arr_banned_caps as $i => $cap ) { + /** + * When there's at least one administrator cap added to the user, + * immediately revoke caps and mark as rejected. + */ + if ( $user->has_cap( $cap ) ) { + $has_admin_cap = true; + $this->revoke_caps( $user ); + break; } } @@ -339,7 +418,24 @@ if ( ! class_exists( 'um\core\Secure' ) ) { } if ( $has_admin_cap ) { - wp_die( esc_html__( 'Security Check!', 'ultimate-member' ) ); + /** + * Notify Administrators Immediately + */ + if ( UM()->options()->get( 'secure_notify_admins_banned_accounts' ) ) { + $interval = UM()->options()->get( 'secure_notify_admins_banned_accounts__interval' ); + if ( 'instant' === $interval ) { + $this->send_email( array( $user->get( 'ID' ) ) ); + } + } + + // Destroy Sessions & Redirect. + wp_destroy_current_session(); + wp_logout(); + session_unset(); + $login_url = add_query_arg( 'err', 'inactive', um_get_core_page( 'login' ) ); + wp_safe_redirect( $login_url ); + exit; + } } @@ -352,8 +448,17 @@ if ( ! class_exists( 'um\core\Secure' ) ) { * @since 2.6.8 */ public function revoke_caps( $user ) { + $captured = array( + 'capabilities' => $user->allcaps, + 'submitted' => $_REQUEST, //phpcs:ignore WordPress.Security.NonceVerification + ); + update_user_meta( $user->get( 'ID' ), 'um_user_blocked__metadata', $captured ); $user->remove_all_caps(); - UM()->user()->set_status( 'rejected' ); + if ( is_user_logged_in() ) { + UM()->user()->set_status( 'inactive' ); + } else { + UM()->user()->set_status( 'rejected' ); + } update_user_meta( $user->get( 'ID' ), 'um_user_blocked', 'suspicious_activity' ); update_user_meta( $user->get( 'ID' ), 'um_user_blocked__datetime', current_time( 'mysql' ) ); } @@ -383,6 +488,164 @@ if ( ! class_exists( 'um\core\Secure' ) ) { return $val; } + /** + * Register Events + * + * @since 2.6.8 + */ + public function schedule_events() { + + if ( UM()->options()->get( 'secure_notify_admins_banned_accounts' ) ) { + add_action( 'um_secure_notify_administrator_hourly', array( $this, 'notify_administrators_hourly' ) ); + add_action( 'um_secure_notify_administrator_daily', array( $this, 'notify_administrators_daily' ) ); + if ( ! wp_next_scheduled( 'um_secure_notify_administrator' ) ) { + wp_schedule_event( current_time( 'mysql' ), 'hourly', 'um_secure_notify_administrator_hourly' ); + wp_schedule_event( current_time( 'mysql' ), 'daily', 'um_secure_notify_administrator_daily' ); + } + } + } + + /** + * Notify Administrators hourly - Suspicious activities in an hour + * + * @since 2.6.8 + */ + public function notify_administrators_hourly() { + + $interval = UM()->options()->get( 'secure_notify_admins_banned_accounts__interval' ); + if ( 'hourly' === $interval ) { + $args = array( + 'fields' => 'ID', + 'meta_query' => array( + 'relation' => 'AND', + array( + 'key' => 'um_user_blocked__datetime', + 'value' => gmdate( 'Y-m-d H:i:s', strtotime( '-1 hour' ) ), + 'compare' => '>=', + 'type' => 'DATETIME', + ), + ), + ); + + $users = new \WP_User_Query( $args ); + + $this->send_email( array_values( $users->get_results() ) ); + } + + } + + /** + * Notify Administrators daily - Today's suspicious activity + * + * @since 2.6.8 + */ + public function notify_administrators_daily() { + + $interval = UM()->options()->get( 'secure_notify_admins_banned_accounts__interval' ); + if ( 'daily' === $interval ) { + $args = array( + 'fields' => 'ID', + 'relation' => 'AND', + 'meta_query' => array( + 'relation' => 'AND', + array( + 'key' => 'um_user_blocked__datetime', + 'value' => gmdate( 'Y-m-d H:i:s', strtotime( '-1 day' ) ), + 'compare' => '>=', + 'type' => 'DATE', + ), + array( + 'key' => 'um_user_blocked__datetime', + 'value' => gmdate( 'Y-m-d H:i:s', strtotime( 'now' ) ), + 'compare' => '<=', + 'type' => 'DATE', + ), + ), + ); + + $users = new \WP_User_Query( $args ); + + $this->send_email( array_values( $users->get_results() ) ); + + } + + } + + /** + * Get Email template + * + * @param bool $single Whether the template is for single or multiple user activities + * @param array $profile_urls Profile URLs to include in the email body + * + * @since 2.6.8 + */ + public function get_email_template( $single = true, $profile_urls = array() ) { + $action = ''; + if ( ! is_user_logged_in() ) { + $action = 'Rejected'; + } else { + $action = 'Deactivated'; + } + + $body = ''; + if ( $single ) { + $body = 'This is to inform you that there\'s a suspicious activity with the following account: '; + $body .= '
'; + $body .= '{user_profile_link}'; + $body .= '

'; + $body .= 'Due to that we have set the account status to ' . $action . ', Revoked Roles & Destroyed the Login Session.'; + $body .= '
'; + } else { + $body = 'This is to inform you that there are suspicious activities with the following accounts: '; + $body .= '
'; + $body .= '{user_profile_link}'; + $body .= '

'; + $body .= 'Due to that we have set each account\'s status to ' . $action . ', revoked roles & destroyed the login session.'; + $body .= '
'; + } + + $urls = implode( '
', $profile_urls ); + $body = str_replace( '{user_profile_link}', $urls, $body ); + $body .= '

- Sent via Ultimate Member plugin. '; + + return $body; + } + + /** + * Send Email + * + * @param array $user_ids User IDs. + * + * @since 2.6.8 + */ + public function send_email( $user_ids = array() ) { + + if ( empty( $user_ids ) ) { + return ''; + } + $multiple_recipients = array(); + $admins = get_users( 'role=Administrator' ); + foreach ( $admins as $user ) { + $multiple_recipients[] = $user->user_email; + } + + if ( count( $user_ids ) <= 1 ) { + $subject = __( 'Suspicious Account Activity on ', 'ultimate-member' ) . wp_parse_url( get_site_url() )['host']; + $url = UM()->user()->get_profile_link( $user_ids[0] ); + $body = $this->get_email_template( true, array( $url ) ); + } else { + $subject = __( 'Suspicious Accounts & Activities on ', 'ultimate-member' ) . wp_parse_url( get_site_url() )['host']; + $arr_urls = array(); + foreach ( $user_ids as $i => $uid ) { + $arr_urls[] = UM()->user()->get_profile_link( $uid ); + } + $body = $this->get_email_template( false, $arr_urls ); + } + + wp_mail( $multiple_recipients, $subject, $body ); + + } + } From 753db264f8565e62695a078ccff3fa33747fa8af Mon Sep 17 00:00:00 2001 From: Champ Camba Date: Tue, 4 Jul 2023 00:18:36 +0800 Subject: [PATCH 10/45] Add filter hook for redirecting flagged user --- includes/core/class-secure.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/includes/core/class-secure.php b/includes/core/class-secure.php index a2343cf8..d740f42a 100644 --- a/includes/core/class-secure.php +++ b/includes/core/class-secure.php @@ -432,11 +432,18 @@ if ( ! class_exists( 'um\core\Secure' ) ) { wp_destroy_current_session(); wp_logout(); session_unset(); - $login_url = add_query_arg( 'err', 'inactive', um_get_core_page( 'login' ) ); - wp_safe_redirect( $login_url ); - exit; + $redirect = apply_filters( 'um_secure_blocked_user_redirect_immediately', true ); + if ( $redirect ) { + $login_url = add_query_arg( 'err', 'inactive', um_get_core_page( 'login' ) ); + wp_safe_redirect( $login_url ); + exit; + } + + return true; } + + return false; } /** From 42188baea6e7e81f770ef733b81d6caa2d5093dc Mon Sep 17 00:00:00 2001 From: Champ Camba Date: Tue, 4 Jul 2023 00:37:17 +0800 Subject: [PATCH 11/45] Fix wp schedule event for email notifications --- includes/core/class-secure.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/includes/core/class-secure.php b/includes/core/class-secure.php index d740f42a..117c8109 100644 --- a/includes/core/class-secure.php +++ b/includes/core/class-secure.php @@ -505,10 +505,15 @@ if ( ! class_exists( 'um\core\Secure' ) ) { if ( UM()->options()->get( 'secure_notify_admins_banned_accounts' ) ) { add_action( 'um_secure_notify_administrator_hourly', array( $this, 'notify_administrators_hourly' ) ); add_action( 'um_secure_notify_administrator_daily', array( $this, 'notify_administrators_daily' ) ); - if ( ! wp_next_scheduled( 'um_secure_notify_administrator' ) ) { + + if ( ! wp_next_scheduled( 'um_secure_notify_administrator_hourly' ) ) { wp_schedule_event( current_time( 'mysql' ), 'hourly', 'um_secure_notify_administrator_hourly' ); + } + + if ( ! wp_next_scheduled( 'um_secure_notify_administrator_daily' ) ) { wp_schedule_event( current_time( 'mysql' ), 'daily', 'um_secure_notify_administrator_daily' ); } + } } From e1c3950fb9f16b72628499840104b602ef53e2a5 Mon Sep 17 00:00:00 2001 From: Champ Camba Date: Tue, 4 Jul 2023 01:05:54 +0800 Subject: [PATCH 12/45] capture submitted data for flagged users --- includes/core/class-secure.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/includes/core/class-secure.php b/includes/core/class-secure.php index 117c8109..20fbd3e6 100644 --- a/includes/core/class-secure.php +++ b/includes/core/class-secure.php @@ -457,7 +457,8 @@ if ( ! class_exists( 'um\core\Secure' ) ) { public function revoke_caps( $user ) { $captured = array( 'capabilities' => $user->allcaps, - 'submitted' => $_REQUEST, //phpcs:ignore WordPress.Security.NonceVerification + 'submitted' => UM()->form()->post_form, + 'roles' => $user->roles, ); update_user_meta( $user->get( 'ID' ), 'um_user_blocked__metadata', $captured ); $user->remove_all_caps(); From 46720f0a3254c0a42c092b482e7a2bf7fa538a49 Mon Sep 17 00:00:00 2001 From: Champ Camba Date: Tue, 4 Jul 2023 01:42:27 +0800 Subject: [PATCH 13/45] Tweak property visibillity for WP CLI --- includes/core/class-secure.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/includes/core/class-secure.php b/includes/core/class-secure.php index 20fbd3e6..cc4fb300 100644 --- a/includes/core/class-secure.php +++ b/includes/core/class-secure.php @@ -1,8 +1,6 @@ Date: Tue, 4 Jul 2023 02:53:53 +0800 Subject: [PATCH 14/45] Refactor email subject --- includes/core/class-secure.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/includes/core/class-secure.php b/includes/core/class-secure.php index cc4fb300..3948af22 100644 --- a/includes/core/class-secure.php +++ b/includes/core/class-secure.php @@ -512,7 +512,6 @@ if ( ! class_exists( 'um\core\Secure' ) ) { if ( ! wp_next_scheduled( 'um_secure_notify_administrator_daily' ) ) { wp_schedule_event( current_time( 'mysql' ), 'daily', 'um_secure_notify_administrator_daily' ); } - } } @@ -640,12 +639,12 @@ if ( ! class_exists( 'um\core\Secure' ) ) { $multiple_recipients[] = $user->user_email; } + $subject = _n( 'Suspicious Account Activity on ', 'Suspicious Accounts & Activities on ', count( $user_ids ), 'ultimate-member' ) . wp_parse_url( get_site_url() )['host']; + if ( count( $user_ids ) <= 1 ) { - $subject = __( 'Suspicious Account Activity on ', 'ultimate-member' ) . wp_parse_url( get_site_url() )['host']; - $url = UM()->user()->get_profile_link( $user_ids[0] ); - $body = $this->get_email_template( true, array( $url ) ); + $url = UM()->user()->get_profile_link( $user_ids[0] ); + $body = $this->get_email_template( true, array( $url ) ); } else { - $subject = __( 'Suspicious Accounts & Activities on ', 'ultimate-member' ) . wp_parse_url( get_site_url() )['host']; $arr_urls = array(); foreach ( $user_ids as $i => $uid ) { $arr_urls[] = UM()->user()->get_profile_link( $uid ); From 8dba1534a0ac553ca6e90eff414b4465ddd4f0f8 Mon Sep 17 00:00:00 2001 From: Champ Camba Date: Tue, 4 Jul 2023 03:34:19 +0800 Subject: [PATCH 15/45] Add account form to check capabilities integrity --- includes/core/class-secure.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/core/class-secure.php b/includes/core/class-secure.php index 3948af22..ac5682cf 100644 --- a/includes/core/class-secure.php +++ b/includes/core/class-secure.php @@ -139,6 +139,7 @@ if ( ! class_exists( 'um\core\Secure' ) ) { add_action( 'um_after_save_registration_details', array( $this, 'secure_user_capabilities' ), 10 ); if ( is_user_logged_in() && ! current_user_can( 'manage_options' ) ) { // Exclude current Logged-in Administrator from validation checks. add_action( 'um_after_user_updated', array( $this, 'secure_user_capabilities' ), 1 ); + add_action( 'um_after_user_account_updated', array( $this, 'secure_user_capabilities' ), 1 ); } } From 85ebc8b83b113c244222cd792ca8f660b211fce2 Mon Sep 17 00:00:00 2001 From: Champ Camba Date: Tue, 4 Jul 2023 03:57:29 +0800 Subject: [PATCH 16/45] Include flagged user's browser data in the captured metadata --- includes/core/class-secure.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/includes/core/class-secure.php b/includes/core/class-secure.php index ac5682cf..344b2937 100644 --- a/includes/core/class-secure.php +++ b/includes/core/class-secure.php @@ -454,10 +454,19 @@ if ( ! class_exists( 'um\core\Secure' ) ) { * @since 2.6.8 */ public function revoke_caps( $user ) { + + if ( ! class_exists( '\Browser' ) ) { + require_once um_path . 'includes/lib/browser.php'; + } + + // Detect browser + $browser = new \Browser(); + $captured = array( 'capabilities' => $user->allcaps, 'submitted' => UM()->form()->post_form, 'roles' => $user->roles, + 'user_browser' => $browser, ); update_user_meta( $user->get( 'ID' ), 'um_user_blocked__metadata', $captured ); $user->remove_all_caps(); From a3cdba07cb6ef3f43e19f8b6bfd436a9bce73567 Mon Sep 17 00:00:00 2001 From: Champ Camba Date: Wed, 5 Jul 2023 12:00:26 +0800 Subject: [PATCH 17/45] Add secure's restore account --- includes/core/class-secure.php | 99 +++++++++++++++++++++++++++------- 1 file changed, 79 insertions(+), 20 deletions(-) diff --git a/includes/core/class-secure.php b/includes/core/class-secure.php index 344b2937..17fe9d3a 100644 --- a/includes/core/class-secure.php +++ b/includes/core/class-secure.php @@ -1,6 +1,9 @@ get( 'um_user_blocked__metadata' ); + + // Restore Roles. + if ( isset( $metadata['roles'] ) ) { + foreach ( $metadata['roles'] as $role ) { + $user->add_role( $role ); + } + } + // Restore Account Status. + if ( isset( $metadata['account_status'] ) ) { + update_user_meta( $user_id, 'account_status', $metadata['account_status'] ); + } + // Clear Cache + UM()->user()->remove_cache( $user_id ); + + set_transient( 'um_secure_restore_account_notice_success', 1, 5 ); + + wp_safe_redirect( wp_get_referer() ); + exit; + + } + } /** @@ -378,19 +416,18 @@ if ( ! class_exists( 'um\core\Secure' ) ) { global $wpdb; // Fetch the WP_User object of our user. um_fetch_user( $user_id ); - $user = new \WP_User( $user_id ); + $user = new WP_User( $user_id ); $has_admin_cap = false; $arr_banned_caps = array(); if ( UM()->options()->get( 'banned_capabilities' ) ) { $arr_banned_caps = array_keys( UM()->options()->get( 'banned_capabilities' ) ); - } else { - $arr_banned_caps = $this->banned_admin_capabilities; } // Add locked administratrive capabilities $arr_banned_caps[] = 'manage_options'; $arr_banned_caps[] = 'promote_users'; + $arr_banned_caps[] = 'level_10'; foreach ( $arr_banned_caps as $i => $cap ) { /** @@ -459,16 +496,18 @@ if ( ! class_exists( 'um\core\Secure' ) ) { require_once um_path . 'includes/lib/browser.php'; } - // Detect browser + // Detect browser. $browser = new \Browser(); - + // Capture details. $captured = array( - 'capabilities' => $user->allcaps, - 'submitted' => UM()->form()->post_form, - 'roles' => $user->roles, - 'user_browser' => $browser, + 'capabilities' => $user->allcaps, + 'submitted' => UM()->form()->post_form, + 'roles' => $user->roles, + 'user_browser' => $browser, + 'account_status' => get_user_meta( $user->get( 'ID' ), 'account_status', true ), ); update_user_meta( $user->get( 'ID' ), 'um_user_blocked__metadata', $captured ); + $user->remove_all_caps(); if ( is_user_logged_in() ) { UM()->user()->set_status( 'inactive' ); @@ -492,14 +531,20 @@ if ( ! class_exists( 'um\core\Secure' ) ) { */ public function manage_users_custom_column( $val, $column_name, $user_id ) { if ( 'account_status' === $column_name ) { - $is_blocked = get_user_meta( $user_id, 'um_user_blocked', true ); - if ( ! empty( $is_blocked ) ) { - $datetime = get_user_meta( $user_id, 'um_user_blocked__datetime', true ); - $val = $val . '
' . __( 'Blocked Due to Suspicious Activity', 'ultimate-member' ) . '
'; + um_fetch_user( $user_id ); + $is_blocked = um_user( 'um_user_blocked' ); + $account_status = um_user( 'account_status' ); + if ( ! empty( $is_blocked ) && in_array( $account_status, array( 'rejected', 'inactive' ), true ) ) { + $datetime = um_user( 'um_user_blocked__datetime' ); + $val = $val . '
' . __( 'Blocked Due to Suspicious Activity', 'ultimate-member' ) . '
'; + $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 = ' · ' . __( 'Restore Account', 'ultimate-member' ) . ''; if ( ! empty( $datetime ) ) { - $val = $val . '
' . human_time_diff( strtotime( $datetime ), strtotime( current_time( 'mysql' ) ) ) . ' ' . __( 'ago', 'ultimate-member' ) . '
'; + $val = $val . '
' . human_time_diff( strtotime( $datetime ), strtotime( current_time( 'mysql' ) ) ) . ' ' . __( 'ago', 'ultimate-member' ) . '' . $action . '
'; } } + um_reset_user(); } return $val; } @@ -547,7 +592,7 @@ if ( ! class_exists( 'um\core\Secure' ) ) { ), ); - $users = new \WP_User_Query( $args ); + $users = new WP_User_Query( $args ); $this->send_email( array_values( $users->get_results() ) ); } @@ -583,7 +628,7 @@ if ( ! class_exists( 'um\core\Secure' ) ) { ), ); - $users = new \WP_User_Query( $args ); + $users = new WP_User_Query( $args ); $this->send_email( array_values( $users->get_results() ) ); @@ -666,7 +711,21 @@ if ( ! class_exists( 'um\core\Secure' ) ) { } + public function admin_notice() { + if ( get_transient( 'um_secure_restore_account_notice_success' ) ) { ?> +
+

+ +

+
+ Date: Wed, 5 Jul 2023 12:51:48 +0800 Subject: [PATCH 18/45] Add secure global notice for first-time --- includes/core/class-secure.php | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/includes/core/class-secure.php b/includes/core/class-secure.php index 17fe9d3a..d519503d 100644 --- a/includes/core/class-secure.php +++ b/includes/core/class-secure.php @@ -203,6 +203,10 @@ if ( ! class_exists( 'um\core\Secure' ) ) { } + if ( isset( $_REQUEST['um_dismiss_security_first_time_notice'] ) ) { + set_transient( 'um_secure_first_time_admin_notice', 1, 5 ); + } + } /** @@ -327,7 +331,7 @@ if ( ! class_exists( 'um\core\Secure' ) ) { 'options' => $banned_admin_capabilities_options, 'value' => UM()->options()->get( 'banned_capabilities' ) ? array_keys( UM()->options()->get( 'banned_capabilities' ) ) : array_keys( $banned_admin_capabilities_options ), 'label' => __( 'Banned Administrative Capabilities', 'ultimate-member' ), - 'description' => __( 'All the above are default Administrator & Super Admin capabilities. When someone tries to inject capabilities to the Profile & Register form submission, it will be flagged with this option. The manage_options, promote_users & level_10 capabilities are locked to ensure no users will be created with these capabilities.', 'ultimate-member' ), + 'description' => __( '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 manage_options, promote_users & level_10 capabilities are locked to ensure no users will be created with these capabilities.', 'ultimate-member' ), ), array( 'id' => 'secure_notify_admins_banned_accounts', @@ -724,6 +728,24 @@ if ( ! class_exists( 'um\core\Secure' ) ) { // Delete transient, only display this notice once. delete_transient( 'um_secure_restore_account_notice_success' ); } + // phpcs:disable WordPress.Security.NonceVerification + if ( ! get_transient( 'um_secure_first_time_admin_notice' ) && ( ! isset( $_REQUEST['page'] ) || 'um_options' !== $_REQUEST['page'] ) ) { + ?> +
+

+
+ +

+

+ + +

+
+ Date: Wed, 5 Jul 2023 12:54:04 +0800 Subject: [PATCH 19/45] Fix admin notice expiration --- includes/core/class-secure.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/core/class-secure.php b/includes/core/class-secure.php index d519503d..d621575e 100644 --- a/includes/core/class-secure.php +++ b/includes/core/class-secure.php @@ -204,7 +204,7 @@ if ( ! class_exists( 'um\core\Secure' ) ) { } if ( isset( $_REQUEST['um_dismiss_security_first_time_notice'] ) ) { - set_transient( 'um_secure_first_time_admin_notice', 1, 5 ); + set_transient( 'um_secure_first_time_admin_notice', 1, 0 ); } } From 51a5b5ed66ebefdf14f93c0c5adfa58d741ea24e Mon Sep 17 00:00:00 2001 From: Champ Camba Date: Wed, 5 Jul 2023 12:57:42 +0800 Subject: [PATCH 20/45] Add secure doc article link --- includes/core/class-secure.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/core/class-secure.php b/includes/core/class-secure.php index d621575e..437e6de0 100644 --- a/includes/core/class-secure.php +++ b/includes/core/class-secure.php @@ -740,7 +740,7 @@ if ( ! class_exists( 'um\core\Secure' ) ) {

- +

Date: Wed, 5 Jul 2023 13:04:08 +0800 Subject: [PATCH 21/45] Fix default locked capabilities --- includes/core/class-secure.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/includes/core/class-secure.php b/includes/core/class-secure.php index 437e6de0..16093100 100644 --- a/includes/core/class-secure.php +++ b/includes/core/class-secure.php @@ -291,8 +291,9 @@ if ( ! class_exists( 'um\core\Secure' ) ) { $settings['secure']['title'] = __( 'Secure', 'ultimate-member' ); $banned_admin_capabilities_options = array(); + $default_locked_cap_options = array( 'manage_options', 'promote_users', 'level_10' ); foreach ( $this->banned_admin_capabilities as $i => $cap ) { - if ( in_array( $cap, array( 'manage_options', 'promote_users', 'level_10' ), true ) ) { + if ( in_array( $cap, $default_locked_cap_options, true ) ) { continue; } $banned_admin_capabilities_options[ $cap ] = $cap; @@ -329,7 +330,7 @@ if ( ! class_exists( 'um\core\Secure' ) ) { 'multi' => true, 'columns' => 2, 'options' => $banned_admin_capabilities_options, - 'value' => UM()->options()->get( 'banned_capabilities' ) ? array_keys( UM()->options()->get( 'banned_capabilities' ) ) : array_keys( $banned_admin_capabilities_options ), + 'value' => UM()->options()->get( 'banned_capabilities' ) ? array_keys( UM()->options()->get( 'banned_capabilities' ) ) : array_keys( $default_locked_cap_options ), 'label' => __( 'Banned Administrative Capabilities', 'ultimate-member' ), 'description' => __( '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 manage_options, promote_users & level_10 capabilities are locked to ensure no users will be created with these capabilities.', 'ultimate-member' ), ), From a0b52f26989e53876de21df31aa38e3c94c47b82 Mon Sep 17 00:00:00 2001 From: Champ Camba Date: Wed, 5 Jul 2023 13:13:33 +0800 Subject: [PATCH 22/45] Add disabled options in multi checkbox render --- includes/admin/core/class-admin-forms.php | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/includes/admin/core/class-admin-forms.php b/includes/admin/core/class-admin-forms.php index 92355732..e91efd3b 100644 --- a/includes/admin/core/class-admin-forms.php +++ b/includes/admin/core/class-admin-forms.php @@ -1204,19 +1204,20 @@ if ( ! class_exists( 'um\admin\core\Admin_Forms' ) ) { $values = array(); } - $i = 0; + $i = 0; $html = ''; $columns = ( ! empty( $field_data['columns'] ) && is_numeric( $field_data['columns'] ) ) ? $field_data['columns'] : 1; while ( $i < $columns ) { - $per_page = ceil( count( $field_data['options'] ) / $columns ); - $section_fields_per_page = array_slice( $field_data['options'], $i*$per_page, $per_page, true ); - $html .= ''; + $per_page = ceil( count( $field_data['options'] ) / $columns ); + $section_fields_per_page = array_slice( $field_data['options'], $i * $per_page, $per_page, true ); + $html .= ''; foreach ( $section_fields_per_page as $k => $title ) { - $id_attr = ' id="' . esc_attr( $id . '_' . $k ) . '" '; - $for_attr = ' for="' . esc_attr( $id . '_' . $k ) . '" '; - $name_attr = ' name="' . $name . '[' . $k . ']" '; + $id_attr = ' id="' . esc_attr( $id . '_' . $k ) . '" '; + $for_attr = ' for="' . esc_attr( $id . '_' . $k ) . '" '; + $name_attr = ' name="' . $name . '[' . $k . ']" '; + $disabed_attr = ''; $data = array( 'field_id' => $field_data['id'] . '_' . $k, @@ -1234,8 +1235,13 @@ if ( ! class_exists( 'um\admin\core\Admin_Forms' ) ) { $data_attr .= ' data-' . $key . '="' . esc_attr( $value ) . '" '; } + if ( isset( $field_data['options_disabled'] ) && in_array( $k, $field_data['options_disabled'], true ) ) { + $disabed_attr = 'disabled="disabled"'; + $values = array_merge( $values, $field_data['options_disabled'] ); + } + $html .= ""; } From 110f22c92e1070c45888a21f3a68445130689773 Mon Sep 17 00:00:00 2001 From: Champ Camba Date: Wed, 5 Jul 2023 13:16:38 +0800 Subject: [PATCH 23/45] Add locked capabilities in the settings to improve readability --- includes/core/class-secure.php | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/includes/core/class-secure.php b/includes/core/class-secure.php index 16093100..3b860170 100644 --- a/includes/core/class-secure.php +++ b/includes/core/class-secure.php @@ -88,8 +88,6 @@ if ( ! class_exists( 'um\core\Secure' ) ) { 'export', 'import', 'list_users', - 'manage_options', - 'promote_users', 'remove_users', 'switch_themes', 'customize', @@ -109,6 +107,8 @@ if ( ! class_exists( 'um\core\Secure' ) ) { 'create_users', 'delete_users', 'level_10', + 'manage_options', + 'promote_users', ) ); @@ -293,9 +293,6 @@ if ( ! class_exists( 'um\core\Secure' ) ) { $banned_admin_capabilities_options = array(); $default_locked_cap_options = array( 'manage_options', 'promote_users', 'level_10' ); foreach ( $this->banned_admin_capabilities as $i => $cap ) { - if ( in_array( $cap, $default_locked_cap_options, true ) ) { - continue; - } $banned_admin_capabilities_options[ $cap ] = $cap; } @@ -325,14 +322,15 @@ if ( ! class_exists( 'um\core\Secure' ) ) { 'description' => __( 'This will logout all users on your site and forces them to reset passwords
when "Display Login form notice to reset passwords" is enabled/checked.', 'ultimate-member' ), ), array( - 'id' => 'banned_capabilities', - 'type' => 'multi_checkbox', - 'multi' => true, - 'columns' => 2, - 'options' => $banned_admin_capabilities_options, - 'value' => UM()->options()->get( 'banned_capabilities' ) ? array_keys( UM()->options()->get( 'banned_capabilities' ) ) : array_keys( $default_locked_cap_options ), - 'label' => __( 'Banned Administrative Capabilities', 'ultimate-member' ), - 'description' => __( '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 manage_options, promote_users & level_10 capabilities are locked to ensure no users will be created with these capabilities.', 'ultimate-member' ), + 'id' => 'banned_capabilities', + 'type' => 'multi_checkbox', + 'multi' => true, + 'columns' => 2, + 'options_disabled' => $default_locked_cap_options, + 'options' => $banned_admin_capabilities_options, + 'value' => UM()->options()->get( 'banned_capabilities' ) ? array_keys( UM()->options()->get( 'banned_capabilities' ) ) : array_keys( $default_locked_cap_options ), + 'label' => __( 'Banned Administrative Capabilities', 'ultimate-member' ), + 'description' => __( '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 manage_options, promote_users & level_10 capabilities are locked to ensure no users will be created with these capabilities.', 'ultimate-member' ), ), array( 'id' => 'secure_notify_admins_banned_accounts', From 25aa40b1c290c82d879aeedf207af4d430bbbee5 Mon Sep 17 00:00:00 2001 From: Mykyta Synelnikov Date: Thu, 6 Jul 2023 01:56:59 +0300 Subject: [PATCH 24/45] - review for secure functionality; --- includes/admin/class-admin.php | 12 + includes/admin/core/class-admin-forms.php | 19 +- includes/admin/core/class-admin-notices.php | 77 ++- includes/admin/core/class-admin-settings.php | 15 + includes/admin/core/class-secure.php | 348 ++++++++++++ includes/class-config.php | 12 +- includes/class-functions.php | 3 + includes/class-init.php | 1 + includes/core/class-options.php | 5 +- includes/core/class-secure.php | 531 +++++-------------- 10 files changed, 581 insertions(+), 442 deletions(-) create mode 100644 includes/admin/core/class-secure.php diff --git a/includes/admin/class-admin.php b/includes/admin/class-admin.php index d9131416..8684b5ae 100644 --- a/includes/admin/class-admin.php +++ b/includes/admin/class-admin.php @@ -2068,5 +2068,17 @@ if ( ! class_exists( 'um\admin\Admin' ) ) { } return UM()->classes['admin_notices']; } + + /** + * @since 2.6.8 + * + * @return core\Secure + */ + public function secure() { + if ( empty( UM()->classes['admin_secure'] ) ) { + UM()->classes['admin_secure'] = new core\Secure(); + } + return UM()->classes['admin_secure']; + } } } diff --git a/includes/admin/core/class-admin-forms.php b/includes/admin/core/class-admin-forms.php index e91efd3b..4b1a7b51 100644 --- a/includes/admin/core/class-admin-forms.php +++ b/includes/admin/core/class-admin-forms.php @@ -117,13 +117,14 @@ if ( ! class_exists( 'um\admin\core\Admin_Forms' ) ) { $data['value'] = esc_attr( $data['value'] ); } - if( in_array( $data['type'], array('info_text') ) ){ + if ( 'info_text' === $data['type'] ) { $arr_kses = array( 'a' => array( - 'href' => array(), - 'title' => array(), - 'target' => array(), - 'class' => array(), + 'href' => array(), + 'title' => array(), + 'target' => array(), + 'class' => array(), + 'onclick' => array(), ), 'button' => array( 'class' => array(), @@ -1185,15 +1186,14 @@ if ( ! class_exists( 'um\admin\core\Admin_Forms' ) ) { * @return bool|string */ function render_multi_checkbox( $field_data ) { - if ( empty( $field_data['id'] ) ) { return false; } $id = ( ! empty( $this->form_data['prefix_id'] ) ? $this->form_data['prefix_id'] : '' ) . '_' . $field_data['id']; - $class = ! empty( $field_data['class'] ) ? $field_data['class'] : ''; - $class .= ! empty( $field_data['size'] ) ? $field_data['size'] : 'um-long-field'; + $class = ! empty( $field_data['class'] ) ? $field_data['class'] : ''; + $class .= ! empty( $field_data['size'] ) ? $field_data['size'] : 'um-long-field'; $class_attr = ' class="um-forms-field ' . esc_attr( $class ) . '" '; $name = $field_data['id']; @@ -1229,7 +1229,7 @@ if ( ! class_exists( 'um\admin\core\Admin_Forms' ) ) { $data_attr = ''; foreach ( $data as $key => $value ) { - if ( $value == 'checkbox_key' ) { + if ( 'checkbox_key' === $value ) { $value = $k; } $data_attr .= ' data-' . $key . '="' . esc_attr( $value ) . '" '; @@ -1237,7 +1237,6 @@ if ( ! class_exists( 'um\admin\core\Admin_Forms' ) ) { if ( isset( $field_data['options_disabled'] ) && in_array( $k, $field_data['options_disabled'], true ) ) { $disabed_attr = 'disabled="disabled"'; - $values = array_merge( $values, $field_data['options_disabled'] ); } $html .= "