From 70146230109e9d02b5bb37a0d27e684f06fbefbc Mon Sep 17 00:00:00 2001 From: Mykyta Synelnikov Date: Fri, 7 Mar 2025 01:25:05 +0200 Subject: [PATCH 1/7] Enhance SQL injection protection in regex patterns Updated regular expressions to strengthen protection against sleep and benchmark injection attacks. This change improves the security of queries by handling potential edge cases more effectively. --- includes/core/class-member-directory.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/includes/core/class-member-directory.php b/includes/core/class-member-directory.php index 46cbf63a..198ae7dc 100644 --- a/includes/core/class-member-directory.php +++ b/includes/core/class-member-directory.php @@ -1710,7 +1710,8 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) { $regexp_map = array( '/select(.*?)from/im', '/select(.*?)sleep/im', - "/sleep\(\d+\)/im", // avoid any sleep injections + "/sleep\(\s*[\"'`]?\s*\d+\s*[\"'`]?\s*\)/im", // avoid any sleep injections + "/benchmark\(\s*[\"'`]?\s*\d+\s*[\"'`]?\s*,.*\)/im", // avoid any benchmark injections '/select(.*?)database/im', '/select(.*?)where/im', '/update(.*?)set/im', From 51dc3b5747501d8123132d08862a60d08c340ab3 Mon Sep 17 00:00:00 2001 From: Mykyta Synelnikov Date: Fri, 7 Mar 2025 10:51:48 +0200 Subject: [PATCH 2/7] Refine regex patterns to improve injection protection. Simplified the regex patterns for detecting and preventing `sleep` and `benchmark` injections. This improves the accuracy and maintainability of the security checks in the member directory code. --- includes/core/class-member-directory.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/core/class-member-directory.php b/includes/core/class-member-directory.php index 198ae7dc..0135a85f 100644 --- a/includes/core/class-member-directory.php +++ b/includes/core/class-member-directory.php @@ -1710,8 +1710,8 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) { $regexp_map = array( '/select(.*?)from/im', '/select(.*?)sleep/im', - "/sleep\(\s*[\"'`]?\s*\d+\s*[\"'`]?\s*\)/im", // avoid any sleep injections - "/benchmark\(\s*[\"'`]?\s*\d+\s*[\"'`]?\s*,.*\)/im", // avoid any benchmark injections + "/sleep\([^)]+\)/im", // avoid any sleep injections + "/benchmark\([^)]+\)/im", // avoid any benchmark injections '/select(.*?)database/im', '/select(.*?)where/im', '/update(.*?)set/im', From 7f586ef47e48f06c455fc230d82f8d8b8be1c402 Mon Sep 17 00:00:00 2001 From: Mykyta Synelnikov Date: Tue, 18 Mar 2025 17:28:15 +0200 Subject: [PATCH 3/7] Update Ultimate Member to version 2.10.2 with filesystem fixes Introduced `maybe_init_wp_filesystem` for better WP_Filesystem initialization and added new utility methods like `remove_dir`. Improved cache handling and documentation annotations for several methods. These changes enhance file management and ensure smoother integration. --- includes/common/class-filesystem.php | 74 +++++++++++++++++++++++----- includes/common/class-users.php | 2 + ultimate-member.php | 2 +- 3 files changed, 65 insertions(+), 13 deletions(-) diff --git a/includes/common/class-filesystem.php b/includes/common/class-filesystem.php index b5b092d0..08893460 100644 --- a/includes/common/class-filesystem.php +++ b/includes/common/class-filesystem.php @@ -59,6 +59,13 @@ class Filesystem { $this->temp_upload_url = $this->get_upload_url( 'ultimatemember/temp' ); } + /** + * Files Age in the temp folder. By default, it's 24 hours. + * + * @return int Temp file age in seconds. + * @since 2.8.7 + * + */ public function files_age() { /** * Filters the maximum file age in the temp folder. By default, it's 24 hours. @@ -73,6 +80,17 @@ class Filesystem { return apply_filters( 'um_filesystem_max_file_age', 24 * HOUR_IN_SECONDS ); // Temp file age in seconds } + /** + * Image MIME Types + * + * Retrieves a list of image MIME types based on the context. + * + * @param string $context The context in which the MIME types are needed ('list' or 'allowed'). + * + * @return array List of image MIME types based on the context. + * + * @since 2.8.7 + */ public static function image_mimes( $context = 'list' ) { $mimes = array(); @@ -148,12 +166,7 @@ class Filesystem { public function clear_temp_dir() { global $wp_filesystem; - if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) { - require_once ABSPATH . 'wp-admin/includes/file.php'; - - $credentials = request_filesystem_credentials( site_url() ); - WP_Filesystem( $credentials ); - } + self::maybe_init_wp_filesystem(); if ( ! $wp_filesystem->is_dir( $this->temp_upload_dir ) ) { return; @@ -197,12 +210,7 @@ class Filesystem { // Please add define('FS_METHOD', 'direct'); to avoid question about FTP. global $wp_filesystem; - if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) { - require_once ABSPATH . 'wp-admin/includes/file.php'; - - $credentials = request_filesystem_credentials( site_url() ); - WP_Filesystem( $credentials ); - } + self::maybe_init_wp_filesystem(); if ( ! $blog_id ) { $blog_id = get_current_blog_id(); @@ -290,4 +298,46 @@ class Filesystem { return ''; } + + /** + * Probably define global $wp_filesystem. + * + * @since 2.10.2 + * + * @return void + */ + public static function maybe_init_wp_filesystem() { + global $wp_filesystem; + + // If you need to fix this issue on the localhost + // https://stackoverflow.com/questions/30688431/wordpress-needs-the-ftp-credentials-to-update-plugins + // Please add define('FS_METHOD', 'direct'); to avoid question about FTP. + if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + + $credentials = request_filesystem_credentials( site_url() ); + WP_Filesystem( $credentials ); + } + } + + /** + * Remove a directory using WP Filesystem. + * + * @param string $dir The directory path to be removed. Should end with DIRECTORY_SEPARATOR. + * + * @return bool True on success, false on failure or if directory doesn't exist. + * + * @since 2.10.2 + */ + public static function remove_dir( $dir ) { + global $wp_filesystem; + + self::maybe_init_wp_filesystem(); + + if ( ! $wp_filesystem->is_dir( $dir ) ) { + return false; + } + + return $wp_filesystem->delete( $dir, true ); + } } diff --git a/includes/common/class-users.php b/includes/common/class-users.php index b3c4a207..e6fcaa5e 100644 --- a/includes/common/class-users.php +++ b/includes/common/class-users.php @@ -216,6 +216,8 @@ class Users { /** * Reset User cache * + * @since 2.8.7 + * * @param int $user_id User ID. */ public function remove_cache( $user_id ) { diff --git a/ultimate-member.php b/ultimate-member.php index 54da7c77..960538ca 100644 --- a/ultimate-member.php +++ b/ultimate-member.php @@ -3,7 +3,7 @@ * Plugin Name: Ultimate Member * Plugin URI: http://ultimatemember.com/ * Description: The easiest way to create powerful online communities and beautiful user profiles with WordPress - * Version: 2.10.1 + * Version: 2.10.2 * Author: Ultimate Member * Author URI: http://ultimatemember.com/ * Text Domain: ultimate-member From 77f6a1373093c267b5b6462da160b62dcf3f132f Mon Sep 17 00:00:00 2001 From: Mykyta Synelnikov Date: Tue, 25 Mar 2025 23:31:13 +0200 Subject: [PATCH 4/7] Refactor and enhance member directory search functionality. Replaced deprecated and complex search query methods with a more streamlined and flexible implementation. Introduced improved handling of core and custom search fields, optimized search logic, and added better filter support. Incremented plugin version to 2.10.2 to reflect these changes. --- includes/admin/core/class-admin-settings.php | 2 +- includes/core/class-member-directory.php | 303 ++++++++----------- ultimate-member.php | 2 +- 3 files changed, 127 insertions(+), 180 deletions(-) diff --git a/includes/admin/core/class-admin-settings.php b/includes/admin/core/class-admin-settings.php index 122310c7..deb4f727 100644 --- a/includes/admin/core/class-admin-settings.php +++ b/includes/admin/core/class-admin-settings.php @@ -151,7 +151,7 @@ if ( ! class_exists( 'um\admin\core\Admin_Settings' ) ) { } $skip_fields = UM()->builtin()->get_fields_without_metakey(); - $skip_fields = array_merge( $skip_fields, UM()->member_directory()->core_search_fields ); + $skip_fields = array_merge( $skip_fields, UM()->member_directory()::$core_search_fields ); $real_usermeta = $wpdb->get_col( "SELECT DISTINCT meta_key FROM {$wpdb->usermeta}" ); $real_usermeta = ! empty( $real_usermeta ) ? $real_usermeta : array(); diff --git a/includes/core/class-member-directory.php b/includes/core/class-member-directory.php index 0135a85f..b0bb59b2 100644 --- a/includes/core/class-member-directory.php +++ b/includes/core/class-member-directory.php @@ -1,6 +1,7 @@ core_search_fields ); + return apply_filters( 'um_member_directory_core_search_fields', $search_columns ); } /** @@ -456,7 +483,7 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) { if ( ! empty( UM()->builtin()->all_user_fields() ) ) { foreach ( UM()->builtin()->all_user_fields() as $key => $data ) { if ( in_array( $key, $core_search_keys, true ) ) { - if ( isset( $data['title'] ) && array_search( $data['title'], $this->searching_fields, true ) !== false ) { + if ( isset( $data['title'] ) && in_array( $data['title'], $this->searching_fields, true ) ) { $data['title'] = $data['title'] . ' (' . $key . ')'; } @@ -1705,28 +1732,7 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) { if ( empty( $search ) ) { return ''; } - - // Make the search line empty if it contains the mySQL query statements. - $regexp_map = array( - '/select(.*?)from/im', - '/select(.*?)sleep/im', - "/sleep\([^)]+\)/im", // avoid any sleep injections - "/benchmark\([^)]+\)/im", // avoid any benchmark injections - '/select(.*?)database/im', - '/select(.*?)where/im', - '/update(.*?)set/im', - '/delete(.*?)from/im', - ); - - foreach ( $regexp_map as $regexp ) { - preg_match( $regexp, $search, $matches ); - if ( ! empty( $matches ) ) { - $search = ''; - break; - } - } - // Early escape of the search line. The same as `$wpdb->prepare()`. - return esc_sql( $search ); + return $search; } /** @@ -1738,155 +1744,18 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) { // complex using with change_meta_sql function $search = $this->prepare_search( $_POST['search'] ); if ( ! empty( $search ) ) { - $meta_query = array( - 'relation' => 'OR', - array( - 'value' => $search, - 'compare' => '=', - ), - array( - 'value' => $search, - 'compare' => 'LIKE', - ), - array( - 'value' => serialize( (string) $search ), - 'compare' => 'LIKE', - ), - ); - + $meta_query = array(); $meta_query = apply_filters( 'um_member_directory_general_search_meta_query', $meta_query, $search ); - $this->query_args['meta_query'][] = $meta_query; + if ( ! empty( $meta_query ) ) { + $this->query_args['meta_query'][] = $meta_query; + } $this->is_search = true; } } } - /** - * Change mySQL meta query join attribute - * for search only by UM user meta fields and WP core fields in WP Users table - * - * @param array $sql Array containing the query's JOIN and WHERE clauses. - * @param $queries - * @param $type - * @param $primary_table - * @param $primary_id_column - * @param WP_User_Query $context - * - * @return array - */ - public function change_meta_sql( $sql, $queries, $type, $primary_table, $primary_id_column, $context ) { - if ( ! empty( $_POST['search'] ) ) { - $search = $this->prepare_search( $_POST['search'] ); - if ( ! empty( $search ) ) { - global $wpdb; - - $meta_value = '%' . $wpdb->esc_like( $search ) . '%'; - $search_meta = $wpdb->prepare( '%s', $meta_value ); - - preg_match( '~(?<=\{)(.*?)(?=\})~', $search_meta, $matches, PREG_OFFSET_CAPTURE, 0 ); - - // workaround for standard mySQL hashes which are used by $wpdb->prepare instead of the %symbol - // sometimes it breaks error for strings like that wp_postmeta.meta_value LIKE '{12f209b48a89eeab33424902879d05d503f251ca8812dde03b59484a2991dc74}AMS{12f209b48a89eeab33424902879d05d503f251ca8812dde03b59484a2991dc74}' - // {12f209b48a89eeab33424902879d05d503f251ca8812dde03b59484a2991dc74} isn't applied by the `preg_replace()` below - if ( $matches[0][0] ) { - $search_meta = str_replace( '{' . $matches[0][0] . '}', '#%&', $search_meta ); - $sql['where'] = str_replace( '{' . $matches[0][0] . '}', '#%&', $sql['where'] ); - } - - // str_replace( '/', '\/', wp_slash( $search_meta ) ) means that we add backslashes to special symbols + add backslash to slash(/) symbol for proper regular pattern. - preg_match( - '/^(.*).meta_value LIKE ' . str_replace( '/', '\/', wp_slash( $search_meta ) ) . '[^\)]/im', - $sql['where'], - $join_matches - ); - - $sql['where'] = str_replace( '#%&', '{' . $matches[0][0] . '}', $sql['where'] ); - - $directory_id = $this->get_directory_by_hash( sanitize_key( $_POST['directory_id'] ) ); - $exclude_fields = get_post_meta( $directory_id, '_um_search_exclude_fields', true ); - $include_fields = get_post_meta( $directory_id, '_um_search_include_fields', true ); - - if ( isset( $join_matches[1] ) ) { - $meta_join_for_search = trim( $join_matches[1] ); - - // skip private invisible fields - $custom_fields = array(); - if ( empty( $include_fields ) ) { - foreach ( array_keys( UM()->builtin()->all_user_fields ) as $field_key ) { - if ( empty( $field_key ) ) { - continue; - } - - $data = UM()->fields()->get_field( $field_key ); - if ( ! um_can_view_field( $data ) ) { - continue; - } - - $custom_fields[] = $field_key; - } - } else { - foreach ( $include_fields as $field_key ) { - if ( empty( $field_key ) ) { - continue; - } - - $data = UM()->fields()->get_field( $field_key ); - if ( ! um_can_view_field( $data ) ) { - continue; - } - - $custom_fields[] = $field_key; - } - } - - $custom_fields = apply_filters( 'um_general_search_custom_fields', $custom_fields ); - - if ( ! empty( $custom_fields ) ) { - if ( ! empty( $exclude_fields ) ) { - $custom_fields = array_diff( $custom_fields, $exclude_fields ); - } - - $sql['join'] = preg_replace( - '/(' . $meta_join_for_search . ' ON \( ' . $wpdb->users . '\.ID = ' . $meta_join_for_search . '\.user_id )(\))/im', - "$1 AND " . $meta_join_for_search . ".meta_key IN( '" . implode( "','", $custom_fields ) . "' ) $2", - $sql['join'] - ); - } - } - - $core_search = $this->get_core_search_fields(); - if ( ! empty( $include_fields ) ) { - $core_search = array_intersect( $core_search, $include_fields ); - } - if ( ! empty( $exclude_fields ) ) { - $core_search = array_diff( $core_search, $exclude_fields ); - } - - if ( ! empty( $core_search ) ) { - // Add OR instead AND to search in WP core fields user_email, user_login, user_display_name - $search_where = $context->get_search_sql( $search, $core_search, 'both' ); - - $search_where = preg_replace( '/ AND \((.*?)\)/im', "$1 OR", $search_where ); - - // str_replace( '/', '\/', wp_slash( $search ) ) means that we add backslashes to special symbols + add backslash to slash(/) symbol for proper regular pattern. - $pattern = $wpdb->prepare( $meta_join_for_search . '.meta_value = %s', $search ); - $pattern = '/(' . str_replace( '/', '\/', wp_slash( $pattern ) ) . ')/im'; - - $sql['where'] = preg_replace( - $pattern, - trim( $search_where ) . " $1", - $sql['where'], - 1 - ); - } - } - } - - return $sql; - } - /** * Handle filters request */ @@ -2779,16 +2648,15 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) { return $data_array; } - /** * Update limit query * - * @param $user_query + * @param WP_User_Query $user_query */ - function pagination_changes( $user_query ) { + public function pagination_changes( $user_query ) { global $wpdb; - $directory_id = $this->get_directory_by_hash( sanitize_key( $_POST['directory_id'] ) ); + $directory_id = $this->get_directory_by_hash( sanitize_key( $_POST['directory_id'] ) ); $directory_data = UM()->query()->post_data( $directory_id ); $qv = $user_query->query_vars; @@ -2808,6 +2676,88 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) { } } + /** + * Update search query + * + * @param WP_User_Query $user_query + * + * @throws Exception + */ + public function search_changes( $user_query ) { + global $wpdb; + + if ( ! empty( $_POST['search'] ) ) { + $directory_id = $this->get_directory_by_hash( sanitize_key( $_POST['directory_id'] ) ); + $qv = $user_query->query_vars; + + $search = $this->prepare_search( $_POST['search'] ); + if ( empty( $search ) ) { + return; + } + + $exclude_fields = get_post_meta( $directory_id, '_um_search_exclude_fields', true ); + $exclude_fields = ! empty( $exclude_fields ) && is_array( $exclude_fields ) ? $exclude_fields : array(); + $include_fields = get_post_meta( $directory_id, '_um_search_include_fields', true ); + $include_fields = ! empty( $include_fields ) && is_array( $include_fields ) ? $include_fields : array_keys( UM()->builtin()->all_user_fields ); + + $custom_fields = array(); + foreach ( $include_fields as $field_key ) { + if ( empty( $field_key ) ) { + continue; + } + + $data = UM()->fields()->get_field( $field_key ); + if ( isset( $data['type'] ) && 'password' === $data['type'] ) { + continue; + } + if ( isset( $data['metakey'] ) && in_array( $data['metakey'], array( 'role_select', 'role_radio' ), true ) ) { + continue; + } + if ( ! empty( $data['account_only'] ) ) { + continue; + } + + if ( ! um_can_view_field( $data ) ) { + continue; + } + + $custom_fields[] = $field_key; + } + + if ( ! empty( $custom_fields ) && ! empty( $exclude_fields ) ) { + $custom_fields = array_diff( $custom_fields, $exclude_fields ); + } + + $custom_fields = apply_filters( 'um_general_search_custom_fields', $custom_fields ); + $custom_fields = array_values( array_unique( $custom_fields ) ); + + $search_columns = $this->get_core_search_fields( $qv, $search ); + $include_fields = get_post_meta( $directory_id, '_um_search_include_fields', true ); + if ( ! empty( $include_fields ) ) { + $search_columns = array_intersect( $search_columns, $include_fields ); + } + if ( ! empty( $exclude_fields ) ) { + $search_columns = array_diff( $search_columns, $exclude_fields ); + } + + if ( ! empty( $custom_fields ) ) { + $search_columns[] = 'um_search.meta_value'; + $user_query->query_from .= " INNER JOIN {$wpdb->usermeta} AS um_search ON ( {$wpdb->users}.ID = um_search.user_id AND um_search.meta_key IN('" . implode( "','", $custom_fields ) . "'))"; + } + + if ( ! empty( $search_columns ) ) { + $search_where = $user_query->get_search_sql( $search, $search_columns, 'both' ); + $search_where = apply_filters( 'um_general_search_custom_search_where', $search_where, $user_query, $search ); + $user_query->query_where .= $search_where; + } + + // Required `DISTINCT` if not exists, because we add `OR` condition manually. + if ( ( ! empty( $custom_fields ) || ! empty( $search_columns ) ) && false === strpos( $user_query->query_fields, 'DISTINCT' ) ) { + $user_query->query_fields = 'DISTINCT ' . $user_query->query_fields; + } + } + } + function predefined_no_caps( $directory_data ) { //predefined result for user without capabilities to see other members @@ -2940,16 +2890,13 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) { */ do_action( 'um_user_before_query', $this->query_args, $this ); - add_filter( 'get_meta_sql', array( &$this, 'change_meta_sql' ), 10, 6 ); - - add_filter( 'pre_user_query', array( &$this, 'pagination_changes' ), 10, 1 ); + add_action( 'pre_user_query', array( &$this, 'search_changes' ) ); + add_action( 'pre_user_query', array( &$this, 'pagination_changes' ) ); $user_query = new WP_User_Query( $this->query_args ); - remove_filter( 'pre_user_query', array( &$this, 'pagination_changes' ), 10 ); - - remove_filter( 'get_meta_sql', array( &$this, 'change_meta_sql' ), 10 ); - + remove_action( 'pre_user_query', array( &$this, 'search_changes' ) ); + remove_action( 'pre_user_query', array( &$this, 'pagination_changes' ) ); /** * Fires just after the users query for getting users in member directory. * diff --git a/ultimate-member.php b/ultimate-member.php index 54da7c77..960538ca 100644 --- a/ultimate-member.php +++ b/ultimate-member.php @@ -3,7 +3,7 @@ * Plugin Name: Ultimate Member * Plugin URI: http://ultimatemember.com/ * Description: The easiest way to create powerful online communities and beautiful user profiles with WordPress - * Version: 2.10.1 + * Version: 2.10.2 * Author: Ultimate Member * Author URI: http://ultimatemember.com/ * Text Domain: ultimate-member From 6987660e6200c0fcbb26f6c1f0f7504b9a3f5ef5 Mon Sep 17 00:00:00 2001 From: Mykyta Synelnikov Date: Wed, 26 Mar 2025 00:18:52 +0200 Subject: [PATCH 5/7] Refactor member directory search to handle missing directory_id. Ensure proper validation for `directory_id` when processing search queries. Default include and exclude fields are applied if `directory_id` is absent, preserving functionality and preventing errors. --- includes/core/class-member-directory.php | 30 +++++++++++++++++------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/includes/core/class-member-directory.php b/includes/core/class-member-directory.php index b0bb59b2..9eb772da 100644 --- a/includes/core/class-member-directory.php +++ b/includes/core/class-member-directory.php @@ -2687,18 +2687,27 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) { global $wpdb; if ( ! empty( $_POST['search'] ) ) { - $directory_id = $this->get_directory_by_hash( sanitize_key( $_POST['directory_id'] ) ); - $qv = $user_query->query_vars; + $directory_id = null; + if ( ! empty( $_POST['directory_id'] ) ) { + $directory_id = $this->get_directory_by_hash( sanitize_key( $_POST['directory_id'] ) ); + } + + $qv = $user_query->query_vars; $search = $this->prepare_search( $_POST['search'] ); if ( empty( $search ) ) { return; } - $exclude_fields = get_post_meta( $directory_id, '_um_search_exclude_fields', true ); - $exclude_fields = ! empty( $exclude_fields ) && is_array( $exclude_fields ) ? $exclude_fields : array(); - $include_fields = get_post_meta( $directory_id, '_um_search_include_fields', true ); - $include_fields = ! empty( $include_fields ) && is_array( $include_fields ) ? $include_fields : array_keys( UM()->builtin()->all_user_fields ); + $exclude_fields = array(); + $include_fields = array_keys( UM()->builtin()->all_user_fields ); + if ( ! empty( $directory_id ) ) { + $exclude_fields = get_post_meta( $directory_id, '_um_search_exclude_fields', true ); + $include_fields = get_post_meta( $directory_id, '_um_search_include_fields', true ); + + $exclude_fields = ! empty( $exclude_fields ) && is_array( $exclude_fields ) ? $exclude_fields : array(); + $include_fields = ! empty( $include_fields ) && is_array( $include_fields ) ? $include_fields : array_keys( UM()->builtin()->all_user_fields ); + } $custom_fields = array(); foreach ( $include_fields as $field_key ) { @@ -2732,9 +2741,12 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) { $custom_fields = array_values( array_unique( $custom_fields ) ); $search_columns = $this->get_core_search_fields( $qv, $search ); - $include_fields = get_post_meta( $directory_id, '_um_search_include_fields', true ); - if ( ! empty( $include_fields ) ) { - $search_columns = array_intersect( $search_columns, $include_fields ); + + if ( ! empty( $directory_id ) ) { + $include_fields = get_post_meta( $directory_id, '_um_search_include_fields', true ); + if ( ! empty( $include_fields ) ) { + $search_columns = array_intersect( $search_columns, $include_fields ); + } } if ( ! empty( $exclude_fields ) ) { $search_columns = array_diff( $search_columns, $exclude_fields ); From 8c7d7cadecf0b6ab8ea44db93dab0cb27c485fff Mon Sep 17 00:00:00 2001 From: Mykyta Synelnikov Date: Wed, 26 Mar 2025 00:20:45 +0200 Subject: [PATCH 6/7] Refactor nonce verification handling in search processing Improved code readability by consolidating and consistently ignoring nonce verification warnings where it has already been verified. This simplifies the logic and ensures clearer handling of `$_POST` variables in the member directory search functionality. --- includes/core/class-member-directory.php | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/includes/core/class-member-directory.php b/includes/core/class-member-directory.php index 9eb772da..d1ca1d31 100644 --- a/includes/core/class-member-directory.php +++ b/includes/core/class-member-directory.php @@ -2686,19 +2686,19 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) { public function search_changes( $user_query ) { global $wpdb; - if ( ! empty( $_POST['search'] ) ) { - $directory_id = null; - if ( ! empty( $_POST['directory_id'] ) ) { - $directory_id = $this->get_directory_by_hash( sanitize_key( $_POST['directory_id'] ) ); - } - - $qv = $user_query->query_vars; - - $search = $this->prepare_search( $_POST['search'] ); + if ( ! empty( $_POST['search'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification -- already verified here + $search = $this->prepare_search( $_POST['search'] ); // phpcs:ignore WordPress.Security.NonceVerification -- already verified here if ( empty( $search ) ) { return; } + $directory_id = null; + if ( ! empty( $_POST['directory_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification -- already verified here + $directory_id = $this->get_directory_by_hash( sanitize_key( $_POST['directory_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification -- already verified here + } + + $qv = $user_query->query_vars; + $exclude_fields = array(); $include_fields = array_keys( UM()->builtin()->all_user_fields ); if ( ! empty( $directory_id ) ) { @@ -2754,12 +2754,14 @@ if ( ! class_exists( 'um\core\Member_Directory' ) ) { if ( ! empty( $custom_fields ) ) { $search_columns[] = 'um_search.meta_value'; + $user_query->query_from .= " INNER JOIN {$wpdb->usermeta} AS um_search ON ( {$wpdb->users}.ID = um_search.user_id AND um_search.meta_key IN('" . implode( "','", $custom_fields ) . "'))"; } if ( ! empty( $search_columns ) ) { $search_where = $user_query->get_search_sql( $search, $search_columns, 'both' ); $search_where = apply_filters( 'um_general_search_custom_search_where', $search_where, $user_query, $search ); + $user_query->query_where .= $search_where; } From f346020d319738f16a4a01626dc9774989838d28 Mon Sep 17 00:00:00 2001 From: Mykyta Synelnikov Date: Wed, 2 Apr 2025 01:38:16 +0300 Subject: [PATCH 7/7] Update Ultimate Member plugin to version 2.10.2 This release addresses a critical security vulnerability (CVE-2025-1702) by improving query handling with `$wpdb->prepare()`. It also introduces new filesystem utility methods (`maybe_init_wp_filesystem`, `remove_dir`) and updates documentation accordingly. Users are strongly advised to update immediately. --- .wordpress-org/blueprints/blueprint.json | 2 +- README.md | 2 +- changelog.txt | 11 +++++++++++ readme.txt | 16 +++++++++++++++- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/.wordpress-org/blueprints/blueprint.json b/.wordpress-org/blueprints/blueprint.json index 9a52be57..0d2c64e4 100644 --- a/.wordpress-org/blueprints/blueprint.json +++ b/.wordpress-org/blueprints/blueprint.json @@ -15,7 +15,7 @@ "step": "installPlugin", "pluginZipFile": { "resource": "url", - "url": "https:\/\/downloads.wordpress.org\/plugin\/ultimate-member.2.10.1.zip" + "url": "https:\/\/downloads.wordpress.org\/plugin\/ultimate-member.2.10.2.zip" }, "options": { "activate": true diff --git a/README.md b/README.md index 50eba8e8..9412b326 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ GNU Version 2 or Any Later Version ### IMPORTANT: PLEASE UPDATE THE PLUGIN TO AT LEAST VERSION 2.6.7 IMMEDIATELY. VERSION 2.6.7 PATCHES SECURITY PRIVILEGE ESCALATION VULNERABILITY. PLEASE SEE [THIS ARTICLE](https://docs.ultimatemember.com/article/1866-security-incident-update-and-recommended-actions) FOR MORE INFORMATION -[Official Release Version: 2.10.1](https://github.com/ultimatemember/ultimatemember/releases/tag/2.10.1). +[Official Release Version: 2.10.2](https://github.com/ultimatemember/ultimatemember/releases/tag/2.10.2). ## Changelog diff --git a/changelog.txt b/changelog.txt index 581b13b4..de589987 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,16 @@ == Changelog == += 2.10.2 April 02, 2025 = + +* Enhancements: + + - Added: `UM()->common()-filesystem()::maybe_init_wp_filesystem();` method. + - Added: `UM()->common()-filesystem()::remove_dir();` method. + +* Bugfixes: + + - Fixed: Security issue CVE ID: CVE-2025-1702. Reviewed general search scripts and suggested another solution that uses only `$wpdb->prepare()`. + = 2.10.1 March 03, 2025 = * Bugfixes: diff --git a/readme.txt b/readme.txt index 495a0a0f..ab90339a 100644 --- a/readme.txt +++ b/readme.txt @@ -6,7 +6,7 @@ Tags: community, member, membership, user-profile, user-registration Requires PHP: 7.0 Requires at least: 6.2 Tested up to: 6.7 -Stable tag: 2.10.1 +Stable tag: 2.10.2 License: GPLv3 License URI: http://www.gnu.org/licenses/gpl-3.0.txt @@ -167,6 +167,17 @@ No specific extensions are needed. But we highly recommended keep active these P IMPORTANT: PLEASE UPDATE THE PLUGIN TO AT LEAST VERSION 2.6.7 IMMEDIATELY. VERSION 2.6.7 PATCHES SECURITY PRIVILEGE ESCALATION VULNERABILITY. PLEASE SEE [THIS ARTICLE](https://docs.ultimatemember.com/article/1866-security-incident-update-and-recommended-actions) FOR MORE INFORMATION += 2.10.2 2025-04-02 = + +**Enhancements** + +* Added: `UM()->common()-filesystem()::maybe_init_wp_filesystem();` method. +* Added: `UM()->common()-filesystem()::remove_dir();` method. + +**Bugfixes** + +* Fixed: Security issue CVE ID: CVE-2025-1702. Reviewed general search scripts and suggested another solution that uses only `$wpdb->prepare()`. + = 2.10.1 2025-03-03 = **Bugfixes** @@ -278,6 +289,9 @@ IMPORTANT: PLEASE UPDATE THE PLUGIN TO AT LEAST VERSION 2.6.7 IMMEDIATELY. VERSI == Upgrade Notice == += 2.10.2 = +This version fixes a security related bug. Upgrade immediately. + = 2.10.1 = This version fixes a security related bug. Upgrade immediately.