The Exploit
Attacker needs a valid WordPress account at Subscriber level or higher.
curl -i -s -X POST "https://TARGET/wp-admin/admin-ajax.php" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Cookie: wordpress_logged_in_ABC=..." \
--data "action=wpgmza_process_background_action&relay=engine&engine=google&mode=global&wpgmza_security=VALID_NONCE"
The request is accepted by the plugin because processBackgroundAction() validates only the nonce parameter wpgmza_security and does not check whether the caller has edit permissions. The server responds with a success-like payload and the plugin's global map engine configuration is modified as if an administrator had changed it.
What the Patch Did
Before:
if (empty($_POST['slug']) || empty($_POST['wpgmza_security']) || !wp_verify_nonce($_POST['wpgmza_security'], 'wpgmza_ajaxnonce')) {
if (empty($_POST['relay']) || empty($_POST['wpgmza_security']) || !wp_verify_nonce($_POST['wpgmza_security'], 'wpgmza_ajaxnonce')) {
After:
global $wpgmza;
if (empty($_POST['slug']) || empty($_POST['wpgmza_security']) || !wp_verify_nonce($_POST['wpgmza_security'], 'wpgmza_ajaxnonce') || !$wpgmza->isUserAllowedToEdit()) {
global $wpgmza;
if (empty($_POST['relay']) || empty($_POST['wpgmza_security']) || !wp_verify_nonce($_POST['wpgmza_security'], 'wpgmza_ajaxnonce') || !$wpgmza->isUserAllowedToEdit()) {
The patch added an explicit authorization check via the plugin’s permission wrapper, $wpgmza->isUserAllowedToEdit(), on AJAX handlers that modify plugin data. The request still requires a valid wpgmza_security nonce, but now also rejects authenticated users without edit privileges.
Root Cause
This is a classic authorization bypass (CWE-863). The processBackgroundAction() code accepted POST requests containing relay and wpgmza_security, verified only the nonce with wp_verify_nonce(), and then proceeded to modify background settings. The trust boundary violated is the user capability boundary: the request came from an authenticated user, but the function never checked whether that user was allowed to edit plugin settings. relay is attacker-controlled input, passed into the background action sink without a required capability check.
Why It Works
The single load-bearing fix is the added || !$wpgmza->isUserAllowedToEdit() clause. Without that guard, the nonce validation alone cannot stop a Subscriber from invoking the same backend code as an administrator. The global $wpgmza; line is required only so the handler can call the plugin’s authorization helper. The original empty() and wp_verify_nonce() checks remain useful for request sanity and CSRF protection, but they do not substitute for a role/capability check.
Hardening Checklist
- Use
current_user_can()or a plugin-specific wrapper likeisUserAllowedToEdit()in every state-changing AJAX handler. - Validate AJAX nonces with
check_ajax_referer('wpgmza_ajaxnonce','wpgmza_security')rather than relying on rawwp_verify_nonce()alone. - Register privileged handlers only under
wp_ajax_...and avoid exposing sensitive actions to unauthenticated users viawp_ajax_nopriv_.... - Keep authorization separate from request validation: check capabilities before executing business logic.
- Audit all admin-facing endpoints for missing capability checks when they modify options or global settings.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-0593