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/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/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/includes/core/class-member-directory.php b/includes/core/class-member-directory.php index 46cbf63a..d1ca1d31 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,27 +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\(\d+\)/im", // avoid any sleep 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; } /** @@ -1737,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 */ @@ -2778,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; @@ -2807,6 +2676,102 @@ 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'] ) ) { // 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 ) ) { + $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 ) { + 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 ); + + 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 ); + } + + 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 @@ -2939,16 +2904,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/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. 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