The Exploit
Unauthenticated attackers can retrieve member directory data by calling the Ultimate Member AJAX endpoint with a predictable directory_id.
curl -s -X POST "https://<TARGET>/wp-admin/admin-ajax.php" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "X-Requested-With: XMLHttpRequest" \
--data "action=ajax_get_members&directory_id=5f4d8"
The response returns JSON containing member records such as user_login, display_name, role, url, and user_id for the directory identified by that token. Because directory_id is derived from substr(md5($post_id), 10, 5), an attacker can enumerate or brute force the 1,048,576 possible values without authentication.
What the Patch Did
Before:
function get_directory_hash( $id ) {
$hash = substr( md5( $id ), 10, 5 );
return $hash;
}
After:
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;
}
The patch replaced the deterministic MD5-derived directory token with an unpredictable token generated by wp_generate_password() and persisted in post meta via update_post_meta()/get_post_meta(). This changes the control from a predictable enumeration key to a stored random token.
Root Cause
This is CWE-330: Insecure Randomness coupled with missing authorization on a public AJAX endpoint. The endpoint accepts an unauthenticated POST to wp-admin/admin-ajax.php with action=ajax_get_members and the attacker-controlled directory_id parameter. The plugin derived directory_id from substr(md5($post_id), 10, 5), meaning the value is predictable from a public post ID and can be enumerated or brute-forced across the entire 16^5 namespace.
Why It Works
The load-bearing fix is the new wp_generate_password( 5, false ) call in set_directory_hash(). That line is what turns the directory identifier into a random token instead of a deterministic MD5 substring. The surrounding get_post_meta()/update_post_meta() logic is necessary to persist the token and maintain compatibility, but without the random token source the endpoint would still expose the same predictable IDs. The fallback to the old MD5 value keeps upgrades working if token generation fails, but the security improvement hinges on generating and storing a real secret token.
Hardening Checklist
- Use
check_ajax_referer()orcurrent_user_can()on AJAX handlers that return user profile or membership data. - Do not derive access tokens from public IDs; use
wp_generate_password(5, false)orwp_rand()for secret tokens. - Persist authorization tokens in
post_metaoruser_metawithupdate_post_meta()/get_post_meta()rather than recomputing them from visible values. - Sanitize AJAX inputs such as
directory_idwithsanitize_text_field()before use. - Return member data via
wp_send_json_success()and avoid exposing raw user metadata.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-12492