REPORT / 01
Analysis Report · Folder Analysis cache/ultimate-member_2.11.0 → cache/ultimate-member_2.11.1 — CVE-2025-12492
Shared security patch analysis results
02 ·
Lifecycle actions
cancel · resume · skip · regenerate
03 ·
Share this analysis
copy link · embed report
03 ·
CVE Security Analysis & Writeups
ai-generated · per cve
Comprehensive security analysis generated by AI for each confirmed CVE match. Click on a CVE to view the detailed writeup including vulnerability background, technical details, patch analysis, and PoC guide.
CVE-2025-12492
NVD
AI-Generated Analysis
05 ·
Findings
filter · search · paginate
Showing 0 to 0 of 0 results
includes/core/class-member-directory.php
AI: No vulnerabilities
CVE-2025-12492
--- cache/ultimate-member_2.11.0/includes/core/class-member-directory.php 2025-12-21 09:36:27.798829371 +0000+++ cache/ultimate-member_2.11.1/includes/core/class-member-directory.php 2025-12-21 09:36:33.923209991 +0000@@ -2,6 +2,7 @@ namespace um\core; use Exception;+use WP_Post; use WP_User_Query; if ( ! defined( 'ABSPATH' ) ) {@@ -123,11 +124,79 @@ public function __construct() { add_filter( 'init', array( &$this, 'init_variables' ) ); + add_action( 'wp_insert_post', array( &$this, 'set_token' ), 10, 3 ); add_action( 'template_redirect', array( &$this, 'access_members' ), 555 ); } /**+ * Set member directory token as soon as it's created.+ *+ * @param int $post_ID+ * @param WP_Post $post+ * @param bool $update+ *+ * @return void+ */+ public function set_token( $post_ID, $post, $update ) {+ if ( 'um_directory' === $post->post_type && ! $update ) {+ $this->set_directory_hash( $post_ID );+ }+ }++ /**+ * Check if the user can view the member directory.+ *+ * @param int $directory_id+ * @param int|null $user_id+ *+ * @return bool+ */+ public function can_view_directory( $directory_id, $user_id = null ) {+ if ( is_null( $user_id ) && is_user_logged_in() ) {+ $user_id = get_current_user_id();+ }++ $can_view = false;+ $privacy = get_post_meta( $directory_id, '_um_privacy', true );+ if ( '' === $privacy ) {+ $can_view = true;+ } else {+ $privacy = absint( $privacy );++ switch ( $privacy ) {+ case 0:+ $can_view = true;+ break;+ case 1:+ if ( ! is_user_logged_in() ) {+ $can_view = true;+ }+ break;+ case 2:+ if ( is_user_logged_in() ) {+ $can_view = true;+ }+ break;+ case 3:+ if ( is_user_logged_in() ) {+ $privacy_roles = get_post_meta( $directory_id, '_um_privacy_roles', true );+ $privacy_roles = ! empty( $privacy_roles ) && is_array( $privacy_roles ) ? $privacy_roles : array();++ $current_user_roles = um_user( 'roles' );+ if ( ! empty( $current_user_roles ) && count( array_intersect( $current_user_roles, $privacy_roles ) ) > 0 ) {+ $can_view = true;+ }+ }+ break;+ }+ }++ return apply_filters( 'um_directory_user_can_view', $can_view, $directory_id, $user_id );+ }++ /** * Get the WordPress core searching fields in wp_users query.+ * * @since 2.6.10 * @version 2.10.2 * @param array|null $qv WP_User_Query variables.@@ -202,29 +271,104 @@ * * @return bool|int */- function get_directory_by_hash( $hash ) {+ public function get_directory_by_hash( $hash ) { global $wpdb; - $directory_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE SUBSTRING( MD5( ID ), 11, 5 ) = %s", $hash ) );-+ $directory_id = $wpdb->get_var(+ $wpdb->prepare(+ "SELECT post_id+ FROM {$wpdb->postmeta}+ WHERE meta_key = '_um_directory_token' AND+ meta_value = %s+ LIMIT 1",+ $hash+ )+ ); if ( empty( $directory_id ) ) {- return false;+ // Fallback, use old value.+ $directory_id = $wpdb->get_var(+ $wpdb->prepare(+ "SELECT ID+ FROM {$wpdb->posts}+ WHERE SUBSTRING( MD5( ID ), 11, 5 ) = %s",+ $hash+ )+ );+ if ( empty( $directory_id ) ) {+ return false;+ } } return (int) $directory_id; } + /**+ * Generate a secure random token for each directory+ *+ * @param int $id+ *+ * @return false|string+ */+ public function set_directory_hash( $id ) {+ $unique_hash = wp_generate_password( 5, false );+ $result = update_post_meta( $id, '_um_directory_token', $unique_hash );+ if ( false === $result ) {+ return false;+ }+ return $unique_hash;+ } /** * @param $id * * @return bool|string */- function get_directory_hash( $id ) {- $hash = substr( md5( $id ), 10, 5 );+ public function get_directory_hash( $id ) {+ $hash = get_post_meta( $id, '_um_directory_token', true );+ if ( '' === $hash ) {+ // Set the hash if empty.+ $hash = $this->set_directory_hash( $id );+ }+ if ( empty( $hash ) ) {+ // Fallback, use old value.+ $hash = substr( md5( $id ), 10, 5 );+ } return $hash; } + /**+ * Generate a secure random token for each user card+ *+ * @param int $id+ *+ * @return false|string+ */+ public function set_user_hash( $id ) {+ $unique_hash = wp_generate_password( 5, false );+ $result = update_user_meta( $id, '_um_card_anchor_token', $unique_hash );+ if ( false === $result ) {+ return false;+ }+ return $unique_hash;+ }++ /**+ * @param $id+ *+ * @return bool|string+ */+ public function get_user_hash( $id ) {+ $hash = get_user_meta( $id, '_um_card_anchor_token', true );+ if ( '' === $hash ) {+ // Set the hash if empty.+ $hash = $this->set_user_hash( $id );+ }+ if ( empty( $hash ) ) {+ // Fallback, use old value.+ $hash = substr( md5( $id ), 10, 5 );+ }+ return $hash;+ } /** * Get view Type template@@ -673,7 +817,7 @@ */ $attrs = apply_filters( 'um_search_fields', $attrs, $field_key, $directory_data['form_id'] ); - $unique_hash = substr( md5( $directory_data['form_id'] ), 10, 5 );+ $unique_hash = $this->get_directory_hash( $directory_data['form_id'] ); ob_start(); @@ -2545,16 +2689,15 @@ $hook_after_user_name = ob_get_clean(); $data_array = array(- 'card_anchor' => esc_html( substr( md5( $user_id ), 10, 5 ) ),- 'id' => absint( $user_id ),- 'role' => esc_html( um_user( 'role' ) ),- 'account_status' => esc_html( UM()->common()->users()->get_status( $user_id ) ),- 'account_status_name' => esc_html( UM()->common()->users()->get_status( $user_id, 'formatted' ) ),+ 'card_anchor' => esc_html( $this->get_user_hash( $user_id ) ),+ 'role' => is_user_logged_in() ? esc_html( um_user( 'role' ) ) : 'undefined', // make the role hidden for the nopriv requests.+ 'account_status' => is_user_logged_in() ? esc_html( UM()->common()->users()->get_status( $user_id ) ) : 'undefined', // make the status hidden for the nopriv requests.+ 'account_status_name' => is_user_logged_in() ? esc_html( UM()->common()->users()->get_status( $user_id, 'formatted' ) ) : __( 'Undefined', 'ultimate-member' ), // make the status hidden for the nopriv requests. 'cover_photo' => wp_kses( um_user( 'cover_photo', $this->cover_size ), UM()->get_allowed_html( 'templates' ) ), 'display_name' => esc_html( um_user( 'display_name' ) ), 'profile_url' => esc_url( um_user_profile_url() ), 'can_edit' => (bool) $can_edit,- 'edit_profile_url' => esc_url( um_edit_profile_url() ),+ 'edit_profile_url' => $can_edit ? esc_url( um_edit_profile_url() ) : '', 'avatar' => wp_kses( get_avatar( $user_id, $this->avatar_size ), UM()->get_allowed_html( 'templates' ) ), 'display_name_html' => wp_kses( um_user( 'display_name', 'html' ), UM()->get_allowed_html( 'templates' ) ), 'dropdown_actions' => $dropdown_actions,@@ -2788,13 +2931,16 @@ } } - /** * Main Query function for getting members via AJAX */- function ajax_get_members() {+ public function ajax_get_members() { UM()->check_ajax_nonce(); + if ( UM()->is_rate_limited( 'member_directory' ) ) {+ wp_send_json_error( __( 'Too many requests', 'ultimate-member' ) );+ }+ global $wpdb; if ( empty( $_POST['directory_id'] ) ) {@@ -2807,6 +2953,10 @@ wp_send_json_error( __( 'Wrong member directory data', 'ultimate-member' ) ); } + if ( ! $this->can_view_directory( $directory_id ) ) {+ wp_send_json_error( __( 'You cannot see this member directory', 'ultimate-member' ) );+ }+ $directory_data = UM()->query()->post_data( $directory_id ); //predefined result for user without capabilities to see other members
AI Analysis
# Security Analysis Report
Analyzing the diff for security vulnerabilities:
---
**Vulnerability 1:**
Vulnerability Existed: **yes**
**Insecure Randomness / Predictable Hash - CWE-330** - CWE-330 - includes/core/class-member-directory.php (lines 206-208, 325-327)
**Old Code:**
```php
function get_directory_hash( $id ) {
$hash = substr( md5( $id ), 10, 5 );
return $hash;
}
```
**Fixed Code:**
```php
public function get_directory_hash( $id ) {
$hash = get_post_meta( $id, '_um_directory_token', true );
if ( '' === $hash ) {
// Set the hash if empty.
$hash = $this->set_directory_hash( $id );
}
if ( empty( $hash ) ) {
// Fallback, use old value.
$hash = substr( md5( $id ), 10, 5 );
}
return $hash;
}
public function set_directory_hash( $id ) {
$unique_hash = wp_generate_password( 5, false );
$result = update_post_meta( $id, '_um_directory_token', $unique_hash );
if ( false === $result ) {
return false;
}
return $unique_hash;
}
```
Explanation:
The original code generated directory identifiers by taking a substring of the MD5 hash of the post ID. This is cryptographically weak because: (1) MD5 is not suitable for security purposes, (2) the hash is deterministic and predictable based on the ID, and (3) only 5 characters are used from the hash, reducing entropy. The fix replaces this with `wp_generate_password()` which generates cryptographically random tokens and stores them as post metadata, making directory tokens unpredictable and non-enumerable.
---
**Vulnerability 2:**
Vulnerability Existed: **yes**
**Insecure Randomness / Predictable Hash - CWE-330** - CWE-330 - includes/core/class-member-directory.php (lines 2545, 2689-2699)
**Old Code:**
```php
'card_anchor' => esc_html( substr( md5( $user_id ), 10, 5 ) ),
```
**Fixed Code:**
```php
'card_anchor' => esc_html( $this->get_user_hash( $user_id ) ),
public function set_user_hash( $id ) {
$unique_hash = wp_generate_password( 5, false );
$result = update_user_meta( $id, '_um_card_anchor_token', $unique_hash );
if ( false === $result ) {
return false;
}
return $unique_hash;
}
public function get_user_hash( $id ) {
$hash = get_user_meta( $id, '_um_card_anchor_token', true );
if ( '' === $hash ) {
// Set the hash if empty.
$hash = $this->set_user_hash( $id );
}
if ( empty( $hash ) ) {
// Fallback, use old value.
$hash = substr( md5( $id ), 10, 5 );
}
return $hash;
}
```
Explanation:
Similar to Vulnerability 1, user card anchors were generated using MD5 substrings of user IDs, making them predictable. The fix replaces this with random token generation using `wp_generate_password()` stored in user metadata, preventing user enumeration and predictable card anchor values.
---
**Vulnerability 3:**
Vulnerability Existed: **yes**
**Improper Access Control / Authorization - CWE-639** - CWE-639 - includes/core/class-member-directory.php (lines 2931-2942, 2953-2956)
**Old Code:**
```php
function ajax_get_members() {
UM()->check_ajax_nonce();
global $wpdb;
if ( empty( $_POST['directory_id'] ) ) {
wp_send_json_error( __( 'Wrong member directory data', 'ultimate-member' ) );
}
```
**Fixed Code:**
```php
public function ajax_get_members() {
UM()->check_ajax_nonce();
if ( UM()->is_rate_limited( 'member_directory' ) ) {
wp_send_json_error( __( 'Too many requests', 'ultimate-member' ) );
}
global $wpdb;
if ( empty( $_POST['directory_id'] ) ) {
wp_send_json_error( __( 'Wrong member directory data', 'ultimate-member' ) );
}
if ( ! $this->can_view_directory( $directory_id ) ) {
wp_send_json_error( __( 'You cannot see this member directory', 'ultimate-member' ) );
}
```
Explanation:
The original code lacked proper access control checks before serving member directory data via AJAX. An authenticated user could request any directory ID without verifying permissions. The fix adds two critical controls: (1) rate limiting to prevent brute force attacks via `UM()->is_rate_limited()`, and (2) explicit permission checking via the new `can_view_directory()` method which validates privacy settings and user roles. This prevents unauthorized access to restricted member directories.
---
**Vulnerability 4:**
Vulnerability Existed: **yes**
**Improper Information Disclosure - CWE-200** - CWE-200 - includes/core/class-member-directory.php (lines 2689-2699)
**Old Code:**
```php
$data_array = array(
'card_anchor' => esc_html( substr( md5( $user_id ), 10, 5 ) ),
'id' => absint( $user_id ),
'role' => esc_html( um_user( 'role' ) ),
'account_status' => esc_html( UM()->common()->users()->get_status( $user_id ) ),
'account_status_name' => esc_html( UM()->common()->users()->get_status( $user_id, 'formatted' ) ),
```
**Fixed Code:**
```php
$data_array = array(
'card_anchor' => esc_html( $this->get_user_hash( $user_id ) ),
'role' => is_user_logged_in() ? esc_html( um_user( 'role' ) ) : 'undefined',
'account_status' => is_user_logged_in() ? esc_html( UM()->common()->users()->get_status( $user_id ) ) : 'undefined',
'account_status_name' => is_user_logged_in() ? esc_html( UM()->common()->users()->get_status( $user_id, 'formatted' ) ) : __( 'Undefined', 'ultimate-member' ),
```
Explanation:
The original code exposed sensitive user information (role, account status) to non-authenticated users via the AJAX response. This allows unauthenticated visitors to enumerate user information and determine which users have special roles or status. The fix conditionally includes this sensitive data only when the current user is logged in, preventing information disclosure to anonymous users. Additionally, the `edit_profile_url` is only included for users who can edit (line 2701).
CVE Analysis Results:
CVE-2025-12492: Yes
View CVE Description
The Ultimate Member – User Profile, Registration, Login, Member Directory, Content Restriction & Membership Plugin plugin for WordPress is vulnerable to Sensitive Information Exposure in all versions up to, and including, 2.11.0 via the ajax_get_members function. This is due to the use of a predictable low-entropy token (5 hex characters derived from md5 of post ID) to identify member directories and insufficient authorization checks on the unauthenticated AJAX endpoint. This makes it possible for unauthenticated attackers to extract sensitive data including usernames, display names, user roles (including administrator accounts), profile URLs, and user IDs by enumerating predictable directory_id values or brute-forcing the small 16^5 token space.
Showing 1 to 1 of 1 results