Membership Plugin – Restrict Content <= 3.2.24 - Unvalidated Redirect in Password Reset Flow via rcp_redirect
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:NTechnical Details
<=3.2.24What Changed in the Fix
Changes introduced in v3.2.25
Source Code
WordPress.org SVN# 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-noncemust be obtained.
3. Code Flow
- Entry Point:
core/includes/login-functions.phpregistersrcp_process_lostpassword_form()on theinithook. - Check: The function checks if
$_POST['rcp_action']islostpasswordand verifies the noncercp-lostpassword-nonce. - Logic: It calls
rcp_retrieve_password(). If the user exists and the reset email is "sent" successfully (return value is not aWP_Error), the flow continues. - Vulnerable Sink:
The// 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(); }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.
- 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. - Shortcode Identification: The shortcode for the lost password form in Restrict Content is usually
[rcp_lost_password]. - Setup:
- Use
wp post createto create a page with the content[rcp_lost_password].
- Use
- Acquisition:
- Navigate to the newly created page using the browser.
- Use
browser_evalto 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
- Information Gathering: Identify a valid user (e.g., the default admin or a created test user).
- Nonce Extraction: Extract
rcp_lostpassword_noncefrom the form page. - 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
- URL:
- Execution: Submit the request using the
http_requesttool. - Observation: Check the response headers for a
302 Foundstatus and aLocationheader starting withhttps://google.com.
6. Test Data Setup
- Create User: Ensure a user exists to receive the reset request.
wp user create victim victim@example.com --role=subscriber
- 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]'
- Identify URL: Note the permalink of the created page.
7. Expected Results
- The server should return an HTTP 302 redirect.
- The
Locationheader should be:https://google.com?rcp_action=lostpassword_checkemail. - This confirms that the
rcp_redirectparameter was used inwp_redirect()without domain validation.
8. Verification Steps
- Automated Check: Use
http_requestto send the payload and inspect theheadersproperty of the response. - 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 incore/includes/registration-functions.php, though not provided) uses a similar unvalidatedrcp_redirectafter successful registration. - Login Flow Bypass: Although
rcp_process_login_formuseswp_safe_redirect, check if any filters likercp_login_redirect_urlare misconfigured or ifesc_url_rawis used in a way thatwp_safe_redirectmight be bypassed (unlikely, but worth checking). - Shortcode Variants: If
[rcp_lost_password]is not the correct shortcode, grep the plugin directory foradd_shortcodeto find the correct one. Specifically:grep -r "add_shortcode" .inside the plugin folder.
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
@@ -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.