CVE-2025-14844

Membership Plugin – Restrict Content <= 3.2.16 - Missing Authentication to Insecure Direct Object Reference and Sensitive Information Exposure

highAuthorization Bypass Through User-Controlled Key
8.2
CVSS Score
8.2
CVSS Score
high
Severity
3.2.17
Patched in
1d
Time to patch

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:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
High
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=3.2.16
PublishedJanuary 15, 2026
Last updatedJanuary 16, 2026
Affected pluginrestrict-content

Source Code

WordPress.org SVN
Research Plan
Unverified

# 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_card
    • wp_ajax_rcp_stripe_create_setup_intent_for_saved_card
  • Parameters:
    • action: rcp_stripe_create_setup_intent_for_saved_card
    • membership_id (inferred): The ID of the membership to target.
    • nonce: A WordPress nonce for verification (likely required by check_ajax_referer).
  • Authentication: Unauthenticated (via nopriv hook).
  • Preconditions: The plugin must have Stripe Pro/Stripe integration enabled and at least one membership must exist in the system.

3. Code Flow

  1. Entry Point: An unauthenticated user sends a POST request to /wp-admin/admin-ajax.php with the action rcp_stripe_create_setup_intent_for_saved_card.
  2. AJAX Dispatch: WordPress matches the action to the registered wp_ajax_nopriv_... hook, which calls the function rcp_stripe_create_setup_intent_for_saved_card.
  3. Missing Cap Check: Inside rcp_stripe_create_setup_intent_for_saved_card, there is no call to current_user_can() or any check to see if a user is logged in.
  4. IDOR: The function likely retrieves a membership_id from the $_POST or $_REQUEST array. It proceeds to interact with the Stripe API to create a SetupIntent for the account/customer associated with that specific membership ID without verifying if the requester owns that membership.
  5. Information Leak: The function returns a JSON response containing the client_secret of the newly created SetupIntent.

4. Nonce Acquisition Strategy

Restrict Content typically localizes its script data, including nonces, for use in the registration or payment forms.

  1. Shortcode Identification: The scripts are likely enqueued on pages containing the registration form: [register_form].
  2. 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]'
    
  3. Extraction:
    • Use browser_navigate to visit the newly created page.
    • Use browser_eval to search for the localized JS object. Common object names in Restrict Content are rcp_script_options or rcp_vars.
    • Target Variable (Inferred): window.rcp_script_options?.nonce or window.rcp_stripe_vars?.nonce.
    • Verification: If the nonce is not found in a specific variable, search the HTML source for nonce inside <script> tags.

5. Exploitation Strategy

The goal is to leak a client_secret for a membership ID that we do not own.

  1. Discovery: Identify a valid membership_id. In a test environment, this can be done via WP-CLI.
  2. 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>
      
  3. Execution: Send the request using the http_request tool.
  4. Analysis: Check if the response status is 200 OK and if the body contains a JSON object with a success: true key and a client_secret string.

6. Test Data Setup

  1. Enable Stripe: Ensure the plugin is configured to use Stripe (even in Test Mode).
  2. 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.
    
  3. Create a Target User and Membership:
    • Create a "victim" user.
    • Assign them a membership. Note the membership_id.
  4. 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_secret is returned despite the request being unauthenticated and not belonging to the owner of the membership_id.

8. Verification Steps

  1. Verify Unauthenticated Access: Repeat the http_request without any cookies.
  2. Verify IDOR: Attempt to retrieve the secret for a membership_id associated with User A while logged in as User B (or unauthenticated).
  3. Database Check: Confirm the membership_id used exists in the wp_rcp_memberships (or equivalent) table.

9. Alternative Approaches

  • Missing Nonce: If no nonce is found, try the request without the nonce parameter. The vulnerability description mentions "Missing Authentication," which often implies the nopriv handler lacks a check_ajax_referer call entirely.
  • Parameter Brute Force: If membership_id is not the correct parameter name, try id, subscription_id, or rcp_membership_id.
  • REST API: Check if the function is also registered as a REST route under the rcp/v1 namespace, which might also lack a permission_callback.
Research Findings
Static analysis — not yet PoC-verified

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

--- a/includes/gateways/stripe/includes/ajax-actions.php
+++ b/includes/gateways/stripe/includes/ajax-actions.php
@@ -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.