//nefariousplan

Nonce Is Not Auth

CSRF tokens mistaken for authentication. Valid nonce does not equal authenticated caller.

A nonce tells you the request is fresh. That is not the same as telling you the requester is authorized. The first is about replay. The second is about identity.

WordPress plugin code, year after year, treats these two as interchangeable. A handler calls wp_verify_nonce() at the top, returns early on failure, and then proceeds into privileged operations: file uploads, option writes, user role changes. The nonce check succeeds. The nonce was valid. The caller was allowed to fetch one via an unauthenticated endpoint. Valid nonce, unauthenticated caller, privileged action. Three facts that should not coexist, coexisting because the gate that was supposed to check the third was never written.

The pattern sits in the gap between what wp_verify_nonce() promises and what the developer assumed it promised.

Mechanism

WordPress nonces are single-use tokens bound to a user session + action name + time window. They exist to prevent CSRF: an attacker who can trick a logged-in user into submitting a forged request cannot supply a valid nonce, because the attacker does not know the current nonce value. The wp_verify_nonce() call returns true only when the caller presents a token the server previously minted for that specific action + user + time slice.

What wp_verify_nonce() does not verify is who the caller is. The check tells you the token is fresh for its scope. It does not tell you that scope is any particular identity, nor that the identity has permission for the action the handler is about to perform. WordPress provides separate functions for those: is_user_logged_in() for presence, current_user_can() for permission. Nonce + identity + permission are three independent gates. Missing any one is a different class of bug.

The pattern lands when handler code treats nonce verification as a sufficient gate. The shape is consistent: a handler registered via wp_ajax_pix_upload and wp_ajax_nopriv_pix_upload, the nopriv registration making the endpoint callable without authentication. The handler's first line is check_ajax_referer('pix_action') or wp_verify_nonce on a POST field. The referrer check passes because the attacker harvested the nonce from a peer unauthenticated endpoint (every plugin has one). Then the handler performs the privileged operation: writes a file to the plugin's upload dir, flips a config option, creates a user. current_user_can() is never called. Identity was never verified. The nonce made the developer feel safe.

Exhibits

CVE-2026-5718: The Drag-and-Drop CF7 Nonce Mint Blocks One Caller, And Its Name Is curl. codedropz registered _wpcf7_check_nonce on wp_ajax_nopriv_ and gated it behind strpos($_SERVER['HTTP_USER_AGENT'], 'curl') === false. Anyone whose user-agent string omits the substring "curl" gets a freshly minted upload nonce on demand. The upload handler then dutifully calls check_ajax_referer against that mint and treats the pass as authorization. The nonce isn't auth — it's a token the server hands to whoever asks politely. Patch 1.3.9.7 left the mint byte-identical.

CVE-2026-1306: midi-Synth's Empty masterAPIkey Is the Payload. midi-Synth's midiSynth_export handler is registered for both wp_ajax_export and wp_ajax_nopriv_export. Its only identity gate is a string compare against wp_create_nonce("midiSynth"). The same nonce is printed inline on every public page that loads the plugin's scripts, so an unauthenticated visitor harvests it from the <script> block, sends it back, and passes the gate. There is no current_user_can, no is_user_logged_in. The post traces the nonce from the wp_enqueue_scripts callback that prints it, to the nopriv registration that exposes the handler, to the handler's lone gate, and shows the same shape repeats across five other handlers in the codebase. The nonce confirms the caller can read the page. The handler reads it as confirming who the caller is.

CVE-2026-6279: Avada Builder's call_user_func Has No Allowlist. PHP Has No Conditional-Tag Table.. CVE-2026-6279 is the Avada Builder instance of the pattern, with the privileged operation behind the nonce being call_user_func against an attacker-named PHP function. Prior exhibits in the catalog landed on file-upload sinks (Pix for WooCommerce), Zoom SDK signature issuance (vczapi), administrator account creation (WP Maps Pro), and password reset key delivery (Kirki). The Avada exhibit adds a runtime-PHP-callable sink: the handler dispatches system, passthru, shell_exec, exec, file_get_contents, or assert based on the JSON the caller posted. The nonce mint runs in the public-page render path for whatever user is rendering the page, and the page rendering the mint is the page anonymous visitors load. fusionPostCardsVars.nonce is the JavaScript variable. check_ajax_referer is the gate. current_user_can is not called.

CVE-2026-8732: WP Maps Pro's Temp-Access Endpoint Creates Administrators. The Vendor's Email Is Hardcoded.. Eighth exhibit on the pattern, with the intent of the privileged operation made literal in the source. The wpgmp_temp_access_ajax_callback handler is registered via wp_ajax_nopriv_wpgmp_temp_access_ajax and gated by check_ajax_referer( 'fc-call-nonce', 'nonce' ). The nonce is enqueued into every public page through wp_localize_script as the nonce field of the wpgmp_local JavaScript object. The handler's body invokes WPGMP_Temp_Access::wpgmp_temp_access_support, whose check_temp=false branch hardcodes 'role' => 'administrator' and 'email' => '[email protected]' into a wp_insert_user call and returns a magic URL that calls wp_set_auth_cookie on redemption. Prior exhibits on the pattern landed on handlers whose privileged operation (a certificate upload, an SDK signature mint) was incidental to the developer's stated intent; the WP Maps Pro handler's stated intent is administrator account creation, so the CSRF token is gating the exact privilege the handler is designed to grant.

CVE-2026-3891: The Capability Check Is Missing Because the Nonce Check Was Never a Capability Check. This post is the catalog's first complete walk-through of the nonce-is-not-auth chain in a live CVE. The Pix for WooCommerce C6 integration registered both its certificate upload handler and its nonce generator as wp_ajax_nopriv_, making both callable without authentication. The upload handler's only security check was check_ajax_referer, which verified nonce freshness for a nonce issued by the companion nopriv endpoint to any anonymous caller. The post traces every link: the nopriv registrations in LknPaymentPixForWoocommerce.php, the generate_nonce function that calls wp_create_nonce for user ID 0 with no validation, and the upload handler that passes check_ajax_referer while never calling current_user_can. The nonce confirmed the caller was talking to the plugin. It confirmed nothing about who they were.

CVE-2026-1368: The Patch Is Two Characters. The Line Was in the File.. The patch's "fix" is check_ajax_referer on a wp_ajax_nopriv_ handler whose only purpose is to issue Zoom Meeting SDK signatures to anonymous browser-join visitors. The nonce is generated by wp_create_nonce("_nonce_zvc_security") and localized into the page that hosts the join-via-browser flow, a page the plugin product description leads with as a feature for unauthenticated participants. The post-patch attack chain is: load the join page, regex-scrape zvc_ajx.zvc_security from the inline <script>, POST it back to admin-ajax.php with action=get_auth and the chosen meeting_id. The CSRF token confirms the caller could read the page. The page exists for the caller to read. No is_user_logged_in(), no current_user_can(), before or after the patch. Same shape as the pix-woocommerce exhibit, different sink: instead of a webroot file write, the privilege at the end of the chain is the issuance of valid third-party SaaS authentication material.

CVE-2025-9209: The RestroPress JWT Is Not Forged. The Plugin Signs It For You.. Kin pattern, different mechanism. Where nonce-is-not-auth covers the case of a CSRF freshness token misread as an identity proof, this post covers the adjacent case of an HMAC signature misread as an identity proof, with the twist that the caller chose the signing key. Both patterns live in the same structural gap: a check that confirms "the caller could produce this token" is treated as if it confirmed "the caller holds the credential the token stands for." RestroPress's RP_JWT_Verifier::jwt_verify is doing exactly what the pix-woocommerce upload handler did with check_ajax_referer, one layer up in the crypto stack.

Boundaries

Not every nonce check is this pattern. A handler that calls wp_verify_nonce() AND current_user_can('edit_posts') is doing the right thing. The nonce handles CSRF; the capability check handles authorization. The pattern requires that the capability check is missing, not that the nonce check exists.

Not every CSRF gate. The pattern is specifically about confusing CSRF defense with authentication. A system with a different CSRF token mechanism (synchronizer tokens, SameSite cookies, Origin header checks) can fall into the same trap if its authors assume "token valid" means "caller trusted." The shape transfers beyond WordPress.

Not every unauthenticated endpoint. A handler deliberately scoped to anonymous users (a public contact form, a registration page) may legitimately have no auth check. The pattern is about handlers that perform PRIVILEGED operations under nopriv registration. The anonymous-by-design case is a design choice. The privileged-under-nopriv case is the bug.

Defender playbook

Grep your plugins for wp_ajax_nopriv_. Every match is a handler registered to accept unauthenticated callers. For each one, open the handler and verify it does NOT perform privileged operations. If it writes files, updates options, changes user state, or invokes do_action on sensitive hooks, the handler is a candidate for this pattern.

In the handler, audit for current_user_can(). If the function handles privileged operations and does not call current_user_can(), the nonce check is not saving you. Add the capability check, or remove the nopriv registration, or move the endpoint behind authentication. One of the three must be true for the handler to be correct.

Separate the question "is this request fresh?" from the question "is this caller permitted?" in your own code review mental model. Nonce answers the first. Identity plus permission answers the second. A handler that passes the first but not the second is running with wrong inputs.

Treat the registry of nopriv actions as an attack surface, not a feature list. Each entry is a documented path to reach server-side code without authentication. Maintaining that list deliberately, with owner notes on why each one is safe, is cheaper than discovering the unsafe ones through advisories.

For plugins you consume rather than author: check the plugin's changelog for nonce-related CVEs. Plugins that have shipped this pattern once often ship it again, because the codebase's handler-registration style did not change. The post linked below is an exemplar of the class as it lands.

Kinship

Trust Inversion. The nonce becomes an authorizer it was not designed to be. Trust inversion at the plugin-handler layer: the mechanism the handler trusts (nonce presence) is compromised in the specific sense that it never conferred the property the handler is relying on.

Design Debt Driver. WordPress's ajax-handler architecture produces this pattern recurrently across the plugin ecosystem. The API exposes nonces, nopriv registration, and capability checks as three separate knobs. Developers miss one of the three knobs in handler after handler. Each plugin CVE closes its specific instance. The substrate persists.

Unauth Write To Execution Path. Frequent co-occurrence. A nonce-is-not-auth handler that performs file uploads commonly writes into a path the webserver executes. The two patterns compose into the classical WordPress plugin pre-auth RCE shape: nopriv endpoint, harvested nonce, privileged write, PHP execution.

A valid nonce tells you the attacker knew the nonce. It does not tell you they are the user the nonce was minted for.