The Exploit
The attacker only needs a victim who is logged in and has edit_posts capability to be tricked into loading a malicious request.
curl 'https://TARGET/wp-admin/admin-ajax.php' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-b 'wordpress_logged_in_...=VICTIM_SESSION' \
--data 'action=insert_inner_template&nonce=&template_name=CSRFed+Template&template_content=%5B%5D'
This request uses the authenticated victim session to call the vulnerable insert_inner_template AJAX action without a valid nonce. The attacker observes the same AJAX success response as a normal template insertion call, and the site ends up with an attacker-controlled Elementor inner template created in the victim’s account.
Why this still matters at admin:
A compromised admin browser session, a malicious shop manager, or a tenant in a shared hosting environment can all be coerced into executing this request. Since the endpoint only checks capability and not request origin, any user with edit_posts is enough for the exploit to succeed.
What the Patch Did
Before:
public function insert_inner_template() {
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error();
}
After:
public function insert_inner_template() {
check_ajax_referer( 'pa-templates-nonce', 'nonce' );
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error();
}
The patch adds a WordPress AJAX nonce validation check via check_ajax_referer('pa-templates-nonce', 'nonce').
Root Cause
This is a classic CWE-352 CSRF issue. The AJAX handler insert_inner_template accepts attacker-controlled POST data at wp-admin/admin-ajax.php?action=insert_inner_template and performs a state-changing operation without verifying the request origin. The only gate before the mutation is current_user_can('edit_posts'), which confirms the authenticated user has permission but does not stop a forged request sent from another site or injected link. The missing validation of the nonce parameter means an attacker can cross-site request forgery a template insertion whenever a victim with edit_posts capability visits a malicious page.
Why It Works
The load-bearing line is check_ajax_referer( 'pa-templates-nonce', 'nonce' );. If that line is removed, the bug is present again because the request still reaches the template insertion logic with only capability checking. The current_user_can('edit_posts') line is necessary for authorization, but it is not sufficient against CSRF: it only checks who the user is, not how the request was initiated. The new check_ajax_referer() call enforces the missing origin/authenticity control by requiring a valid WordPress nonce in the nonce POST field.
Hardening Checklist
- Use
check_ajax_referer( $action, $query_arg )for all AJAX handlers that mutate state. - Keep
current_user_can()checks for authorization, but never rely on them alone for request authenticity. - Generate nonces with
wp_create_nonce( 'pa-templates-nonce' )and pass them to client-side code for AJAX form submissions. - Restrict writable AJAX endpoints to POST requests and verify expected request parameters before acting.
- Avoid exposing template-creation actions to unauthenticated users; always require
is_user_logged_in()or equivalent session validation.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-14163