CVE-2026-4136

Membership Plugin – Restrict Content <= 3.2.24 - Unvalidated Redirect in Password Reset Flow via rcp_redirect

mediumWeak Password Recovery Mechanism for Forgotten Password
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
3.2.25
Patched in
1d
Time to patch

Description

The Membership Plugin – Restrict Content plugin for WordPress is vulnerable to Unvalidated Redirect in all versions up to, and including, 3.2.24. This is due to insufficient validation on the redirect url supplied via the 'rcp_redirect' parameter. This makes it possible for unauthenticated attackers to redirect users with the password reset email to potentially malicious sites if they can successfully trick them into performing an action.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=3.2.24
PublishedMarch 19, 2026
Last updatedMarch 20, 2026
Affected pluginrestrict-content

What Changed in the Fix

Changes introduced in v3.2.25

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-4136 - Unvalidated Redirect in Restrict Content ## 1. Vulnerability Summary The **Membership Plugin – Restrict Content** plugin (up to version 3.2.24) contains an unvalidated redirect vulnerability within its password recovery mechanism. The vulnerability exis…

Show full research plan

Exploitation Research Plan: CVE-2026-4136 - Unvalidated Redirect in Restrict Content

1. Vulnerability Summary

The Membership Plugin – Restrict Content plugin (up to version 3.2.24) contains an unvalidated redirect vulnerability within its password recovery mechanism. The vulnerability exists because the function rcp_process_lostpassword_form uses wp_redirect() on user-supplied input from the rcp_redirect parameter without verifying if the destination is local. Unlike wp_safe_redirect(), wp_redirect() allows arbitrary external URLs.

2. Attack Vector Analysis

  • Endpoint: Any frontend page, as the vulnerable function is hooked to init. Typically, the attack targets the page containing the "Lost Password" form.
  • HTTP Method: POST
  • Action Trigger: rcp_action=lostpassword
  • Vulnerable Parameter: rcp_redirect
  • Authentication: Unauthenticated (anyone can initiate a password reset for a known user).
  • Preconditions:
    • A valid WordPress username or email must be known to trigger the success path.
    • A valid WordPress nonce for the action rcp-lostpassword-nonce must be obtained.

3. Code Flow

  1. Entry Point: core/includes/login-functions.php registers rcp_process_lostpassword_form() on the init hook.
  2. Check: The function checks if $_POST['rcp_action'] is lostpassword and verifies the nonce rcp-lostpassword-nonce.
  3. Logic: It calls rcp_retrieve_password(). If the user exists and the reset email is "sent" successfully (return value is not a WP_Error), the flow continues.
  4. Vulnerable Sink:
    // core/includes/login-functions.php:200
    if ( ! is_wp_error( $errors ) ) {
        $redirect_to = esc_url($_POST['rcp_redirect']) . '?rcp_action=lostpassword_checkemail';
        wp_redirect( $redirect_to ); // <--- Vulnerable Sink
        exit();
    }
    
    The esc_url() function sanitizes the URL but does not prevent cross-domain redirection. wp_redirect() executes the redirect to the malicious site.

4. Nonce Acquisition Strategy

The nonce is required for the rcp_process_lostpassword_form function to proceed.

  1. Locate Form: The "Lost Password" form is typically rendered via a shortcode. Based on RCP documentation and core/includes/admin/admin-actions.php, the plugin manages several frontend pages.
  2. Shortcode Identification: The shortcode for the lost password form in Restrict Content is usually [rcp_lost_password].
  3. Setup:
    • Use wp post create to create a page with the content [rcp_lost_password].
  4. Acquisition:
    • Navigate to the newly created page using the browser.
    • Use browser_eval to extract the nonce from the hidden input field in the form.
    • Form Field: <input type="hidden" name="rcp_lostpassword_nonce" value="...">
    • Command: browser_eval('document.querySelector(\'input[name="rcp_lostpassword_nonce"]\').value')

5. Exploitation Strategy

  1. Information Gathering: Identify a valid user (e.g., the default admin or a created test user).
  2. Nonce Extraction: Extract rcp_lostpassword_nonce from the form page.
  3. Craft Payload:
    • URL: http://localhost:8080/ (or the specific page with the form)
    • Method: POST
    • Headers: Content-Type: application/x-www-form-urlencoded
    • Body:
      rcp_action=lostpassword&rcp_user_login=admin&rcp_lostpassword_nonce=[NONCE]&rcp_redirect=https://google.com
      
  4. Execution: Submit the request using the http_request tool.
  5. Observation: Check the response headers for a 302 Found status and a Location header starting with https://google.com.

6. Test Data Setup

  1. Create User: Ensure a user exists to receive the reset request.
    • wp user create victim victim@example.com --role=subscriber
  2. Create Page: Place the RCP shortcode on a public page.
    • wp post create --post_type=page --post_title="Forgot Password" --post_status=publish --post_content='[rcp_lost_password]'
  3. Identify URL: Note the permalink of the created page.

7. Expected Results

  • The server should return an HTTP 302 redirect.
  • The Location header should be: https://google.com?rcp_action=lostpassword_checkemail.
  • This confirms that the rcp_redirect parameter was used in wp_redirect() without domain validation.

8. Verification Steps

  1. Automated Check: Use http_request to send the payload and inspect the headers property of the response.
  2. Manual Check:
    const response = await http_request({
      url: 'http://localhost:8080/forgot-password/',
      method: 'POST',
      body: 'rcp_action=lostpassword&rcp_user_login=victim&rcp_lostpassword_nonce=' + nonce + '&rcp_redirect=https://google.com'
    });
    console.log(response.status); // Should be 302
    console.log(response.headers.location); // Should contain google.com
    

9. Alternative Approaches

  • Registration Flow: Check if rcp_process_registration() (likely in core/includes/registration-functions.php, though not provided) uses a similar unvalidated rcp_redirect after successful registration.
  • Login Flow Bypass: Although rcp_process_login_form uses wp_safe_redirect, check if any filters like rcp_login_redirect_url are misconfigured or if esc_url_raw is used in a way that wp_safe_redirect might be bypassed (unlikely, but worth checking).
  • Shortcode Variants: If [rcp_lost_password] is not the correct shortcode, grep the plugin directory for add_shortcode to find the correct one. Specifically: grep -r "add_shortcode" . inside the plugin folder.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Restrict Content plugin for WordPress is vulnerable to an unvalidated redirect due to the use of wp_redirect() instead of wp_safe_redirect() in its password recovery flow. An unauthenticated attacker can exploit this by supplying a malicious external URL in the 'rcp_redirect' parameter, which is then used to redirect the victim after submitting a password reset request or embedded into the password reset email itself.

Vulnerable Code

// core/includes/login-functions.php line 192
function rcp_process_lostpassword_form() {

	if( ! isset( $_POST['rcp_action'] ) || 'lostpassword' != $_POST['rcp_action'] ) {
		return;
	}

	if( ! isset( $_POST['rcp_lostpassword_nonce'] ) || ! wp_verify_nonce( $_POST['rcp_lostpassword_nonce'], 'rcp-lostpassword-nonce' ) ) {
		return;
	}

	$errors = rcp_retrieve_password();

	if ( ! is_wp_error( $errors ) ) {
		$redirect_to = esc_url($_POST['rcp_redirect']) . '?rcp_action=lostpassword_checkemail';
		wp_redirect( $redirect_to );
		exit();
	}
}

---

// core/includes/login-functions.php line 270 (within rcp_retrieve_password logic)
	$message .= __('To reset your password, visit the following address:', 'rcp') . "\r\n\r\n";
	$message .= esc_url_raw( add_query_arg( array( 'rcp_action' => 'lostpassword_reset', 'key' => $key, 'login' => rawurlencode( $user_login ) ), $_POST['rcp_redirect'] ) ) . "\r\n";

Security Fix

--- /home/deploy/wp-safety.org/data/plugin-versions/restrict-content/3.2.24/core/includes/login-functions.php	2025-12-15 16:48:48.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/restrict-content/3.2.25/core/includes/login-functions.php	2026-03-18 22:49:00.000000000 +0000
@@ -199,8 +201,8 @@
 	$errors = rcp_retrieve_password();
 
 	if ( ! is_wp_error( $errors ) ) {
-		$redirect_to = esc_url($_POST['rcp_redirect']) . '?rcp_action=lostpassword_checkemail';
-		wp_redirect( $redirect_to );
+		$redirect_to = wp_validate_redirect( isset( $_POST['rcp_redirect'] ) ? sanitize_url( wp_unslash( $_POST['rcp_redirect'] ) ) : '', home_url() ); // phpcs:ignore WordPress.WP.DeprecatedFunctions.sanitize_urlFound, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+		wp_safe_redirect( add_query_arg( 'rcp_action', 'lostpassword_checkemail', $redirect_to ) );
 		exit();
 	}
 }
@@ -267,7 +271,17 @@
 	$message .= sprintf(__('Username: %s', 'rcp'), $user_login) . "\r\n\r\n";
 	$message .= __('If this was a mistake, just ignore this email and nothing will happen.', 'rcp') . "\r\n\r\n";
 	$message .= __('To reset your password, visit the following address:', 'rcp') . "\r\n\r\n";
-	$message .= esc_url_raw( add_query_arg( array( 'rcp_action' => 'lostpassword_reset', 'key' => $key, 'login' => rawurlencode( $user_login ) ), $_POST['rcp_redirect'] ) ) . "\r\n";
+	$redirect_base = wp_validate_redirect( isset( $_POST['rcp_redirect'] ) ? sanitize_url( wp_unslash( $_POST['rcp_redirect'] ) ) : '', home_url() ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.WP.DeprecatedFunctions.sanitize_urlFound, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+	$message .= esc_url_raw(
+		add_query_arg(
+			array(
+				'rcp_action' => 'lostpassword_reset',
+				'key'        => $key,
+				'login'      => rawurlencode( $user_login ),
+			),
+			$redirect_base
+		)
+	) . "\r\n";

Exploit Outline

1. Locate the password reset page containing the Restrict Content lost password form (typically via the [rcp_lost_password] shortcode). 2. Extract the required nonce from the hidden input field named 'rcp_lostpassword_nonce'. 3. Identify a valid username or email address registered on the target WordPress site. 4. Craft a POST request to the application root or the form's action URL with the following parameters: 'rcp_action=lostpassword', 'rcp_user_login' (the victim's username), 'rcp_lostpassword_nonce' (the extracted nonce), and 'rcp_redirect' set to a malicious external URL (e.g., https://attacker-controlled.com). 5. Upon submission, the server will issue an HTTP 302 redirect to the malicious URL. Additionally, the password reset email sent to the victim will contain a reset link using the attacker-supplied URL as its base, leading the victim to a phishing or malware site when they attempt to reset their password.

Check if your site is affected.

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