The Exploit
A Subscriber-level WordPress user (the lowest authenticated role) can read arbitrary files from the server by sending an unauthenticated AJAX request to the plugin's file-read handlers.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.local
Content-Type: application/x-www-form-urlencoded
Cookie: wordpress_logged_in_<hash>=subscriber_session_token
action=GOTMLS_ajax_View_Quarantine&alter=../../../../../../etc/passwd&_wpnonce=<valid_nonce>
The attacker observes the contents of /etc/passwd (or any readable file on the system) echoed directly in the AJAX response. No admin capability check blocks the request; the GOTMLS_kill_invalid_user() call is missing from the handler.
What the Patch Did
Before:
if (GOTMLS_get_nonce() && isset($_REQUEST["alter"]) && isset($action[$_REQUEST["alter"]])) {
$cmd = $action[$_REQUEST["alter"]];
call_user_func($cmd);
}
After:
GOTMLS_kill_invalid_user();
if (GOTMLS_get_nonce("empty_trash") && isset($_REQUEST["alter"]) && isset($action[$_REQUEST["alter"]])) {
$cmd = $action[$_REQUEST["alter"]];
call_user_func($cmd);
}
The patch added a call to GOTMLS_kill_invalid_user() at the entry point of the affected AJAX handlers (GOTMLS_ajax_View_Quarantine, GOTMLS_ajax_empty_trash, GOTMLS_ajax_whitelist, GOTMLS_ajax_fix, GOTMLS_ajax_scan). This function terminates execution immediately unless the current user holds the required WordPress capability (inferred to be manage_options or equivalent admin-level access). Additionally, nonce validation was tightened by replacing generic function-name-based nonces with specific, named nonces like "empty_trash", preventing nonce reuse across different AJAX actions.
Root Cause
CWE-862: Missing Authorization Check combined with CWE-20: Improper Input Validation.
The vulnerable AJAX handlers accepted the alter parameter from $_REQUEST and used it to dispatch to functions within the $action array. Although the plugin performed nonce validation (GOTMLS_get_nonce()), it did not verify that the calling user possessed the capability to invoke these privileged operations. The missing GOTMLS_kill_invalid_user() call allowed any authenticated user—including Subscribers with no special permissions—to invoke admin-only file operations. The alter parameter itself was subject to path traversal (e.g., ../../etc/passwd), because no canonicalization or confinement was applied before file operations consumed it.
Why It Works
The load-bearing line is GOTMLS_kill_invalid_user();. Without it, the nonce check alone does not establish authorization; a valid nonce can be obtained by any authenticated user in the same WordPress session, because the nonce is issued globally. The function call performs a capability check (likely current_user_can('manage_options')) and terminates execution with an HTTP 403 or 401 response if the user lacks permission.
The engineer added the tightened nonce validation (GOTMLS_get_nonce("empty_trash")) as defence-in-depth: named nonces prevent an attacker from reusing a nonce leaked from a legitimate admin action in a different AJAX context. However, this alone would not fix the vulnerability if a Subscriber could somehow obtain a valid nonce for their own session. The capability check is the true gate; the nonce validation reinforces it by ensuring the nonce is action-specific and fresh.
Hardening Checklist
-
Add
current_user_can()checks at the entry point of every AJAX handler. Before any request parameter is processed, verify the user possesses the required capability usingcurrent_user_can('manage_options')or a role-specific capability constant. This is the WordPress standard for privilege enforcement. -
Use
wp_verify_nonce()with action-specific identifiers. Replace generic nonce validation withwp_verify_nonce($_REQUEST['_wpnonce'], 'action_name')where the second argument uniquely identifies the AJAX action, preventing nonce reuse across handlers. -
Canonicalize and confine file paths before use. When the
alterparameter refers to a file path, pass it throughrealpath()and verify the result is within a whitelist of permitted directories. Do not allow path traversal sequences like../to escape a base directory. -
Avoid
call_user_func()on user-supplied action names. Instead of dispatching based on$_REQUEST["alter"], use a hardcoded switch statement or array of allowed handlers. This prevents unexpected functions from being invoked through parameter injection. -
Separate admin and public AJAX handlers. Register admin-only AJAX actions with
add_action('wp_ajax_action_name', ...)(without thenoprivvariant), which WordPress automatically gates to authenticated users. Public AJAX should useadd_action('wp_ajax_nopriv_action_name', ...)and include its own authorization logic.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-11705