CVE-2026-4977

UsersWP <= 1.2.58 - Authenticated (Subscriber+) Restricted Usermeta Modification via 'htmlvar' Parameter

mediumMissing Authorization
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
1.2.59
Patched in
1d
Time to patch

Description

The UsersWP – Front-end login form, User Registration, User Profile & Members Directory plugin for WordPress is vulnerable to Improper Access Control in all versions up to, and including, 1.2.58 This is due to insufficient field-level permission validation in the upload_file_remove() AJAX handler where the $htmlvar parameter is not validated against a whitelist of allowed fields or checked against the field's for_admin_use property. This makes it possible for authenticated attackers, with subscriber-level access and above, to clear or reset any restricted usermeta column for their own user record, including fields marked as "For admin use only", bypassing intended field-level access restrictions.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
None
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=1.2.58
PublishedApril 9, 2026
Last updatedApril 10, 2026
Affected pluginuserswp

What Changed in the Fix

Changes introduced in v1.2.59

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Research Plan: CVE-2026-4977 - Authenticated Usermeta Modification in UsersWP ## 1. Vulnerability Summary The **UsersWP** plugin (<= 1.2.58) contains a flaw in its AJAX handling logic where an authenticated user (Subscriber level or higher) can clear or reset arbitrary `usermeta` fields for their…

Show full research plan

Research Plan: CVE-2026-4977 - Authenticated Usermeta Modification in UsersWP

1. Vulnerability Summary

The UsersWP plugin (<= 1.2.58) contains a flaw in its AJAX handling logic where an authenticated user (Subscriber level or higher) can clear or reset arbitrary usermeta fields for their own account.

The vulnerability exists in the uwp_upload_file_remove AJAX action (handled by UsersWP_Ajax::upload_file_remove()). The function accepts a user-controlled parameter htmlvar representing a metadata key. It fails to validate this key against an allowlist of permitted "file" fields or check if the field is flagged for administrative use only (for_admin_use). Consequently, an attacker can supply any meta key (e.g., wp_capabilities) to delete or reset its value.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: uwp_upload_file_remove
  • Vulnerable Parameter: htmlvar (carries the target usermeta key)
  • Authentication: Required (Subscriber level is sufficient)
  • Nonce Requirement: Yes, the action requires a nonce typically localized as uwp_localize_data.basicNonce.
  • Preconditions: The attacker must be logged in and able to access a page where the UsersWP scripts are enqueued to retrieve the nonce.

3. Code Flow

  1. Entry Point: The client sends a POST request to admin-ajax.php with action=uwp_upload_file_remove.
  2. Hook Registration: In includes/class-userswp.php, the UsersWP_Ajax class is initialized. This class registers the AJAX handler:
    add_action( 'wp_ajax_uwp_upload_file_remove', array( $this, 'upload_file_remove' ) );
  3. Handler Execution (upload_file_remove):
    • It retrieves security (nonce), htmlvar (meta key), and uid (user ID) from $_POST.
    • It verifies the nonce (likely against the action uwp_basic_nonce).
    • It checks if the uid matches the current user or if the user has admin privileges.
    • Sink: It proceeds to call update_user_meta($uid, $htmlvar, '') or delete_user_meta($uid, $htmlvar) without verifying if $htmlvar is a restricted field.
  4. Impact: The metadata field for the user is cleared.

4. Nonce Acquisition Strategy

The nonce is localized in the front-end using wp_localize_script under the object uwp_localize_data.

  1. Shortcode Identification: The plugin uses the [uwp_profile] or [uwp_account] shortcodes to render user-facing forms where users-wp.js is loaded.
  2. Page Creation: Create a page containing the [uwp_profile] shortcode.
  3. Extraction:
    • Navigate to the page as a logged-in Subscriber.
    • Execute JavaScript via browser_eval to retrieve the nonce:
      browser_eval("uwp_localize_data.basicNonce")
  4. JS Variable Verbatim: uwp_localize_data.basicNonce (found in assets/js/users-wp.js).

5. Exploitation Strategy

The goal is to clear a restricted usermeta field, such as wp_capabilities (to remove the user's role) or a custom UsersWP field intended for admin use.

Step-by-Step Plan:

  1. Authentication: Log in as a Subscriber user.
  2. Nonce Retrieval:
    • Navigate to /profile-page/ (created in setup).
    • Capture the value of uwp_localize_data.basicNonce.
  3. Target Selection:
    • Target: wp_capabilities (Standard WordPress restricted field).
    • Target: uwp_admin_note (Inferred UsersWP meta field).
  4. Exploit Request:
    Send a POST request to admin-ajax.php:
    • URL: http://vulnerable-wp.local/wp-admin/admin-ajax.php
    • Headers: Content-Type: application/x-www-form-urlencoded
    • Body:
      action=uwp_upload_file_remove&htmlvar=wp_capabilities&uid=[CURRENT_USER_ID]&security=[NONCE]
      
  5. Observation: The response should be a JSON object: {"success": true}.

6. Test Data Setup

  1. Target User: A user with username victim_sub and role subscriber.
  2. Landing Page:
    wp post create --post_type=page --post_title="User Account" --post_status=publish --post_content='[uwp_account]'
    
  3. Verification Meta: Ensure the user has a capability set.
    wp user meta get [USER_ID] wp_capabilities
    

7. Expected Results

  • The AJAX request returns {"success": true}.
  • The wp_capabilities meta entry for the victim_sub user is deleted or set to an empty string.
  • The user effectively loses their role and permissions on the site.

8. Verification Steps

  1. CLI Check:
    wp user meta get [USER_ID] wp_capabilities
    
    Expected Output: Empty or error indicating the key does not exist.
  2. Role Check:
    wp user get [USER_ID] --field=roles
    
    Expected Output: Empty string.

9. Alternative Approaches

If wp_capabilities is protected by internal WordPress filters that UsersWP doesn't bypass, target UsersWP-specific fields created via the Form Builder (admin/settings/class-formbuilder.php).

  1. Create a custom field uwp_verified_status and set it to 1 for the user.
  2. Use the exploit to clear uwp_verified_status.
  3. Verify the field is removed via wp user meta get.
Research Findings
Static analysis — not yet PoC-verified

Summary

The UsersWP plugin for WordPress is vulnerable to unauthorized usermeta modification due to a lack of field-level validation in the 'uwp_upload_file_remove' AJAX handler. Authenticated attackers (Subscriber and above) can clear arbitrary usermeta fields for their own account, including restricted administrative fields like 'wp_capabilities', by manipulating the 'htmlvar' parameter.

Vulnerable Code

// assets/js/users-wp.js @ 1.2.58
$( '.uwp_upload_file_remove' ).on( 'click', function( event ) {
    event.preventDefault();

    var htmlvar =  $( this ).data( 'htmlvar' );
    var uid =  $( this ).data( 'uid' );

    var data = {
        'action': 'uwp_upload_file_remove',
        'htmlvar': htmlvar,
        'uid': uid,
        'security': uwp_localize_data.basicNonce
    };

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/userswp/1.2.58/admin/settings/class-formbuilder.php /home/deploy/wp-safety.org/data/plugin-versions/userswp/1.2.59/admin/settings/class-formbuilder.php
--- /home/deploy/wp-safety.org/data/plugin-versions/userswp/1.2.58/admin/settings/class-formbuilder.php	2025-12-11 14:08:00.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/userswp/1.2.59/admin/settings/class-formbuilder.php	2026-03-30 08:27:26.000000000 +0000
@@ -1818,7 +1818,7 @@
                 'label'      => __( 'Validation Pattern', 'userswp' ) . uwp_help_tip( __( 'Enter regex expression for HTML5 pattern validation.', 'userswp' ) ),
                 'type'       => 'text',
                 'wrap_class' => uwp_advanced_toggle_class(),
-                'value'      => addslashes_gpc( $value ), // Keep slashes
+                'value'      => wp_slash( $value ), // Keep slashes
             )
         );
 
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/userswp/1.2.58/assets/js/users-wp.js /home/deploy/wp-safety.org/data/plugin-versions/userswp/1.2.59/assets/js/users-wp.js
--- /home/deploy/wp-safety.org/data/plugin-versions/userswp/1.2.58/assets/js/users-wp.js	2026-02-19 15:06:48.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/userswp/1.2.59/assets/js/users-wp.js	2026-03-30 08:27:26.000000000 +0000
@@ -106,8 +106,9 @@
         $( '.uwp_upload_file_remove' ).on( 'click', function( event ) {
             event.preventDefault();
 
-            var htmlvar =  $( this ).data( 'htmlvar' );
-            var uid =  $( this ).data( 'uid' );
+            var $this = $(this);
+            var htmlvar =  $this.data( 'htmlvar' );
+            var uid =  $this.data( 'uid' );
 
             var data = {
                 'action': 'uwp_upload_file_remove',
@@ -116,17 +117,25 @@
                 'security': uwp_localize_data.basicNonce
             };
 
+            if ($this.closest("form").find('.uwp-field-error').length) {
+                $this.closest("form").find('.uwp-field-error').remove();
+            }
+
             jQuery.ajax({
                 url: uwp_localize_data.ajaxurl,
                 type: 'POST',
                 data: data,
                 dataType: 'json'
             }).done(function(res, textStatus, jqXHR) {
-                if (typeof res == 'object' && res.success) {
-                    $("#"+htmlvar+"_row").find(".uwp_file_preview_wrap").remove();
-                    $("#"+htmlvar).closest("td").find(".uwp_file_preview_wrap").remove();
-                    if($('input[name='+htmlvar+']').data( 'is-required' )){
-                        $('input[name='+htmlvar+']').prop('required',true);
+                if (res && typeof res == 'object') {
+                    if (res.success) {
+                        $("#"+htmlvar+"_row").find(".uwp_file_preview_wrap").remove();
+                        $("#"+htmlvar).closest("td").find(".uwp_file_preview_wrap").remove();
+                        if($('input[name='+htmlvar+']').data( 'is-required' )){
+                            $('input[name='+htmlvar+']').prop('required',true);
+                        }
+                    } else if (res.data && typeof res.data == 'object' && res.data.message) {
+                        $this.parent(".uwp_file_preview_wrap").append('<div class="uwp-field-error">' + res.data.message + '</div>');
                     }
                 }
             });
... (truncated)

Exploit Outline

The exploit targets the 'uwp_upload_file_remove' AJAX action, which is intended to allow users to remove uploaded files by clearing a specific usermeta key. 1. Authentication: The attacker logs into the WordPress site with a Subscriber account. 2. Nonce Acquisition: The attacker navigates to their own profile or account page to retrieve the required AJAX nonce stored in 'uwp_localize_data.basicNonce'. 3. Payload Construction: The attacker crafts a POST request to '/wp-admin/admin-ajax.php' with the following parameters: - 'action': 'uwp_upload_file_remove' - 'htmlvar': The target usermeta key (e.g., 'wp_capabilities' or any 'for_admin_use' field). - 'uid': The attacker's own user ID. - 'security': The captured 'basicNonce'. 4. Execution: Because the server-side handler fails to validate that the provided 'htmlvar' corresponds to an actual file field or an authorized metadata key, the plugin executes 'delete_user_meta' or 'update_user_meta' on the sensitive key, effectively clearing it for the attacker's account.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.