The Exploit
An unauthenticated attacker can manipulate WordPress admin menu ordering by sending a single AJAX request to the rm_user_exists action, injecting an empty or whitespace-padded slug into the admin_order parameter. This causes the RegistrationMagic plugin to grant manage_options capability to an arbitrary role during the next admin menu build.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.local
Content-Type: application/x-www-form-urlencoded
action=rm_user_exists&rm_nonce=<valid_nonce>&admin_order=subscriber:%20&_rm_options_admin_menu_nonce=<valid_nonce>
When this request lands, the plugin processes the malformed admin_order parameter without validating that the role slug is non-empty. The AJAX handler returns success (HTTP 200), and the next time an administrator views the dashboard, the plugin's menu generation logic silently grants the subscriber role manage_options capability.
Why this still matters even though it needs a subscriber account: The exploit chain is two-stage. Stage one (the AJAX request shown above) executes unauthenticated and poisons the plugin's stored configuration. Stage two occurs when any administrator logs in and triggers menu generation—at which point the stored configuration with the empty slug is processed, causing the capability grant. A site with open user registration or pre-existing subscribers becomes a complete privilege-escalation vector for unauthenticated attackers: register a free account, call the AJAX endpoint once, wait for admin login, then log in with your subscriber account that now holds manage_options.
What the Patch Did
Before:
if ($this->mv_handler->validateForm("options_admin_menu")){
After:
if ($this->mv_handler->validateForm("options_admin_menu") && current_user_can('manage_options')) {
The patch added a current_user_can('manage_options') capability check to the entry point of the add_menu() function in the RegistrationMagic options controller. This WordPress-native authorization check ensures that only users with explicit admin capability can modify the admin menu ordering. The form validation alone (validateForm()) was insufficient because it only verified CSRF token and form structure, not that the requesting user possessed the required capability.
Additionally, the patch in class_rm_admin.php (line 488) added input validation to prevent empty role slugs from being concatenated into capability checks:
if ( ! $rm_role->has_cap( $value[0]."manage_options" ) && !empty(trim($value[0])) ) {
This ensures that whitespace-only or empty values in the $value[0] role slug field cannot produce malformed capability names that might bypass role-based security logic.
Root Cause
CWE-862: Missing Authorization Check and CWE-20: Improper Input Validation
The vulnerability chains two failures. First, the add_menu() method in class_rm_options_controller.php (line 563) trusts the presence of a valid form nonce as sufficient proof of authorization. The nonce is generated server-side and validated by validateForm(), but a nonce proves origin (same-site, not replayed), not permission. Any user capable of making an authenticated request—including unauthenticated attackers if the AJAX endpoint has no authentication guard—can submit a valid nonce and modify menu order.
Second, the admin_order POST parameter (sourced from $_POST['admin_order'] via the request shown above) is never validated to ensure role slugs are non-empty strings. The code concatenates $value[0] directly with "manage_options" at line 488 without checking that $value[0] is non-empty after trimming whitespace. An empty or whitespace-only slug produces a capability check on the string "manage_options" instead of (e.g.) "subscriber_manage_options", which evaluates differently in the role-capability system and triggers unintended privilege grants.
Why It Works
The load-bearing fix is the current_user_can('manage_options') check added at line 563 of class_rm_options_controller.php. Remove it, and the vulnerability remains fully exploitable: an unauthenticated attacker can still post to the AJAX endpoint, pass form validation, and poison the admin_order configuration. If you remove the current_user_can() check but keep the input validation (!empty(trim($value[0]))), the attack succeeds because the attacker simply submits a non-empty role slug alongside a valid nonce, and the plugin still processes it without verifying user permission.
The input validation fix (!empty(trim($value[0]))) is defence-in-depth: it prevents the secondary capability-confusion bug at line 488, where malformed role slugs could produce unexpected role-capability mappings. Together, these changes implement two complementary controls—capability-gating at the entry point and input validation at the point of use—ensuring that even if one check fails or future code refactors the call site, the other barrier blocks exploitation.
Hardening Checklist
-
Audit all AJAX handlers for
current_user_can()checks. For everyadd_action('wp_ajax_*')oradd_action('wp_ajax_nopriv_*'), verify that the callback includesif (!current_user_can('appropriate_capability')) { wp_die(); }before processing any user input. RegistrationMagic'srm_user_existsaction lacked this guard. -
Use
wp_verify_nonce()for CSRF prevention only, not authorization. A nonce proves the request came from your own site in the current session; it does not prove the user has permission. Always pair nonce validation withcurrent_user_can()for access control. -
Validate and sanitize all role and capability names before concatenation. Use
sanitize_key()on role slugs andpreg_match('/^[a-z_]+$/', $role_slug)to ensure they contain only lowercase letters and underscores, preventing collision attacks via whitespace-injection (as seen in$value[0]."manage_options"). -
Never trust form validation as a substitute for capability checks. A form handler can validate structure, CSRF tokens, and data types, but only
current_user_can()determines whether the user may perform the action. Combine both. -
Run
wp_capabilities_test(get_user_by('login', 'subscriber'), 'manage_options')in your test suite to catch silent privilege grants. The RegistrationMagic bug would have been caught by a simple unit test that confirmed unprivileged subscribers do not gain admin caps after a menu-update request.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-15403