Published : August 5, 2026

CVE-2026-18501: UsersWP <= 1.2.69 Authenticated (Subscriber+) Stored Cross-Site Scripting via Badge Widget Variable Substitution PoC, Patch Analysis & Rule

Plugin userswp
Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 1.2.69
Patched Version 1.2.70
Disclosed August 5, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-18501:

UsersWP versions 1.2.69 and earlier contain a Stored Cross-Site Scripting (XSS) vulnerability. The flaw resides in the Badge Widget variable substitution functionality, where untrusted user data is substituted into template strings without proper output escaping. This allows authenticated attackers with subscriber-level access to inject arbitrary web scripts that execute when an administrator or other user views the affected page. The CVSS score is 6.4.

Root Cause: The core issue is a failure to sanitize or escape user-provided data before it is inserted into HTML output. In the Badge Widget template processing within includes/helpers/pages.php, the `str_replace` function substitutes the `%%input%%` and `%%profile_url%%` placeholders with values derived from user profile fields. Specifically, the code at lines 384 and 387 replaced these placeholders with the unescaped `$match_value` and `uwp_build_profile_tab_url($user_id)` values. Furthermore, the `uwp_replace_variables` function, which processes `%%variable%%` placeholders with user data, lacked output escaping. This allowed an attacker to store malicious HTML and JavaScript within their profile fields, which is then injected into template output without neutralization.

Exploitation: An attacker with subscriber-level access can exploit this by populating a profile field (e.g., ‘First Name’, ‘Last Name’, or ‘Bio’) with a crafted XSS payload. For instance, setting `first_name` to `alert(document.cookie)`. When an administrator or any user views a page that includes a UsersWP badge with a custom template using the `%%first_name%%` variable, the stored script executes. The payload is stored in the database upon profile update or via the import feature, which lacks proper sanitization for these fields. The vulnerable code path processes the profile data and directly injects it into the badge output.

Patch Analysis: The patch introduces output escaping at multiple critical points. In helpers/pages.php, the `$badge` template is first decoded with `wp_specialchars_decode` to ensure the admin-authored template is represented correctly. Following this, values substituted in via `str_replace` are wrapped in escaping functions: `esc_html` for the `%%input%%` value and `esc_url` for the `%%profile_url%%` value. The `uwp_replace_variables` function now applies `esc_html` to any scalar value before substitution. Additionally, the patch adds sanitization to the import functionality in includes/class-import-export.php, using functions like `sanitize_user`, `sanitize_email`, `sanitize_text_field`, and `sanitize_textarea_field` to clean data on import. This defense-in-depth approach ensures that malicious payloads are neutralized both at the point of storage and at the point of output.

Impact: Successful exploitation allows for Stored Cross-Site Scripting. An attacker can inject malicious scripts that run in the context of any user viewing the compromised page. This can lead to session hijacking, credential theft, defacement of the page, or redirecting users to phishing sites. The impact is limited to users with administrative access who view the badge, as subscribers may not have the same privileges, but a compromised administrator session can lead to full site compromise.

Differential between vulnerable and patched code

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

Code Diff
--- a/userswp/includes/class-import-export.php
+++ b/userswp/includes/class-import-export.php
@@ -631,12 +631,12 @@
                     continue;
                 }

-                $username = isset($row['username']) ? $row['username'] : '';
-                $email = isset($row['email']) ? $row['email'] : '';
-                $first_name = isset($row['first_name']) ? $row['first_name'] : '';
-                $last_name = isset($row['last_name']) ? $row['last_name'] : '';
-                $bio = isset($row['bio']) ? $row['bio'] : '';
-                $display_name = isset($row['display_name']) ? $row['display_name'] : '';
+                $username = isset($row['username']) ? sanitize_user($row['username']) : '';
+                $email = isset($row['email']) ? sanitize_email($row['email']) : '';
+                $first_name = isset($row['first_name']) ? sanitize_text_field($row['first_name']) : '';
+                $last_name = isset($row['last_name']) ? sanitize_text_field($row['last_name']) : '';
+                $bio = isset($row['bio']) ? sanitize_textarea_field($row['bio']) : '';
+                $display_name = isset($row['display_name']) ? sanitize_text_field($row['display_name']) : '';
                 $password = wp_generate_password();
                 $exclude = array('user_id');
                 $exclude = apply_filters('uwp_import_exclude_columns', $exclude, $row);
@@ -644,7 +644,6 @@
                 if(isset($row['username']) && username_exists($row['username'])){
                     $user = get_user_by('login', $row['username']);
                     $user_id = $user->ID;
-                    $email = $row['email'];
                     if( !empty( $email ) && $update_existing = apply_filters('uwp_import_update_users', false, $row, $user_id) ) {
                         $args = array(
                             'ID'         => $user_id,
@@ -663,7 +662,7 @@
                     $user = get_user_by('ID', $row['user_id']);
                     if(false === $user){
                         $userdata = array(
-                            'user_login'  =>  $row['username'],
+                            'user_login'  =>  $username,
                             'user_email'  =>  $email,
                             'user_pass'   =>  $password,
                             'first_name' => $first_name,
--- a/userswp/includes/helpers/pages.php
+++ b/userswp/includes/helpers/pages.php
@@ -381,10 +381,16 @@
 			if ( empty( $badge ) && empty($args['icon_class']) ) {
 				$badge = isset($field->site_title) ? $field->site_title : '';
 			}
-			if( !empty( $badge ) && $badge = str_replace("%%input%%", $match_value,$badge) ){
+			// Decode entities in the admin-authored template text now, before any
+			// untrusted user values are substituted in below. Decoding after
+			// substitution would undo the escaping applied to those values.
+			if ( ! empty( $badge ) ) {
+				$badge = wp_specialchars_decode( $badge, ENT_QUOTES );
+			}
+			if( !empty( $badge ) && $badge = str_replace("%%input%%", esc_html( (string) $match_value ), $badge) ){
 				// will be replace in condition check
 			}
-			if( !empty( $badge ) && $user_id && $badge = str_replace("%%profile_url%%", uwp_build_profile_tab_url($user_id),$badge) ){
+			if( !empty( $badge ) && $user_id && $badge = str_replace("%%profile_url%%", esc_url( uwp_build_profile_tab_url($user_id) ),$badge) ){
 				// will be replace in condition check
 			}

@@ -429,7 +435,7 @@
 				$new_window = ' target="_blank" ';
 			}

-			$badge = ! empty( $badge ) ? __( wp_specialchars_decode( $badge, ENT_QUOTES ), 'userswp' ) : '';
+			$badge = ! empty( $badge ) ? __( $badge, 'userswp' ) : '';

 			// phone & email link
 			if ( ! empty( $field ) && ! empty( $field->field_type ) && ! empty( $args['link'] ) && strpos( $args['link'], 'http' ) !== 0 ) {
@@ -596,6 +602,9 @@
 			foreach($user_data as $key => $val) {
 				if ( ! in_array( $key, $excluded_fields ) ) {
 					$val  = apply_filters( 'uwp_replace_variables_' . $key, $val, $text );
+					if ( is_scalar( $val ) ) {
+						$val = esc_html( (string) $val );
+					}
 					$text = str_replace( '%%' . $key . '%%', $val, $text );
 				}
 			}
--- a/userswp/userswp.php
+++ b/userswp/userswp.php
@@ -3,7 +3,7 @@
 Plugin Name: UsersWP
 Plugin URI: https://userswp.io/
 Description: The only lightweight user profile plugin for WordPress. UsersWP features front end user profile, users directory, a registration and a login form.
-Version: 1.2.69
+Version: 1.2.70
 Author: AyeCode Ltd
 Author URI: https://userswp.io
 License: GPL-2.0+
@@ -24,7 +24,7 @@
 }

 if ( ! defined( 'USERSWP_VERSION' ) ) {
-	define( 'USERSWP_VERSION', '1.2.69' );
+	define( 'USERSWP_VERSION', '1.2.70' );
 }

 if ( ! defined( 'USERSWP_PATH' ) ) {
--- a/userswp/vendor/ayecode/wp-super-duper/sd-plugin.php
+++ b/userswp/vendor/ayecode/wp-super-duper/sd-plugin.php
@@ -5,7 +5,7 @@
  * @wordpress-plugin
  * Plugin Name: Super Duper - Examples
  * Description: This is a Hello World test plugin for WP Super Duper Class.
- * Version: 1.2.33
+ * Version: 1.2.34
  * Author: AyeCode
  * Author URI: https://ayecode.io
  * Text Domain: super-duper
--- a/userswp/vendor/ayecode/wp-super-duper/wp-super-duper.php
+++ b/userswp/vendor/ayecode/wp-super-duper/wp-super-duper.php
@@ -5,7 +5,7 @@

 if ( ! class_exists( 'WP_Super_Duper' ) ) {

-	define( 'SUPER_DUPER_VER', '1.2.33' );
+	define( 'SUPER_DUPER_VER', '1.2.34' );

 	/**
 	 * A Class to be able to create a Widget, Shortcode or Block to be able to output content for WordPress.
--- a/userswp/vendor/composer/autoload_static.php
+++ b/userswp/vendor/composer/autoload_static.php
@@ -13,14 +13,14 @@
     );

     public static $prefixLengthsPsr4 = array (
-        'C' =>
+        'C' =>
         array (
             'Composer\Installers\' => 20,
         ),
     );

     public static $prefixDirsPsr4 = array (
-        'Composer\Installers\' =>
+        'Composer\Installers\' =>
         array (
             0 => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers',
         ),
--- a/userswp/vendor/composer/installed.php
+++ b/userswp/vendor/composer/installed.php
@@ -3,7 +3,7 @@
         'name' => 'uswerwp/userswp',
         'pretty_version' => 'dev-master',
         'version' => 'dev-master',
-        'reference' => '570126f04d37441dfc371cfa6ae4d73738ea67cf',
+        'reference' => 'e9b72c7a50c333a0d507579be24fc3cecde60197',
         'type' => 'project',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -47,9 +47,9 @@
             'dev_requirement' => false,
         ),
         'ayecode/wp-super-duper' => array(
-            'pretty_version' => '1.2.33',
-            'version' => '1.2.33.0',
-            'reference' => '08d488111ffa84911162261f4ac64986cdddb230',
+            'pretty_version' => '1.2.34',
+            'version' => '1.2.34.0',
+            'reference' => 'c8678e97ca8ad4f5f71f946b5374ebb4f3fb1ac0',
             'type' => 'library',
             'install_path' => __DIR__ . '/../ayecode/wp-super-duper',
             'aliases' => array(),
@@ -79,7 +79,7 @@
         'uswerwp/userswp' => array(
             'pretty_version' => 'dev-master',
             'version' => 'dev-master',
-            'reference' => '570126f04d37441dfc371cfa6ae4d73738ea67cf',
+            'reference' => 'e9b72c7a50c333a0d507579be24fc3cecde60197',
             'type' => 'project',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),

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.