The Exploit
An unauthenticated attacker must trick a WordPress administrator into clicking a malicious link or visiting a crafted webpage while logged in to the WordPress dashboard. The attacker sends a forged AJAX request to the Royal Elementor Addons plugin's filter handler without a valid nonce token. The server processes the request, filters grid media posts, and returns the filtered dataset—or in the case of a second AJAX endpoint, returns a filtered count—all without cryptographic proof that an admin actually intended the action.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.wordpress.local
Content-Type: application/x-www-form-urlencoded
action=wpr_filter_grid_media
&grid_settings[tax1_custom_color_field_text]=_test_color
&wpr_taxonomy=category
&wpr_filter=uncategorized
&wpr_offset=0
The response contains filtered post data in JSON. No error is raised because the plugin never calls wp_verify_nonce() on the request. An attacker embedding this request in a <form> tag on an attacker-controlled site and tricking an admin into visiting it would trigger arbitrary grid filtering—opening the door to chained attacks involving unsanitized post meta retrieval (see below).
What the Patch Did
Before:
public function __construct() {
add_action('wp_ajax_wpr_filter_grid_media', [$this, 'wpr_filter_grid_media']);
add_action('wp_ajax_nopriv_wpr_filter_grid_media', [$this, 'wpr_filter_grid_media']);
add_action('wp_ajax_wpr_get_media_filtered_count', [$this, 'wpr_get_media_filtered_count']);
add_action('wp_ajax_nopriv_wpr_get_media_filtered_count', [$this, 'wpr_get_media_filtered_count']);
}
public function get_main_query_args() {
$settings = $_POST['grid_settings'];
$taxonomy = $_POST['wpr_taxonomy'];
$term = $_POST['wpr_filter'];
// ...
if ( isset($_POST['wpr_offset']) ) {
$args['offset'] = $_POST['wpr_offset'];
}
// ...
$cfc_text = get_term_meta($term->term_id, $_POST['grid_settings']['tax1_custom_color_field_text'], true);
}
After:
public function __construct() {
add_action('wp_ajax_wpr_filter_grid_media', [$this, 'wpr_filter_grid_media']);
add_action('wp_ajax_nopriv_wpr_filter_grid_media', [$this, 'wpr_filter_grid_media']);
add_action('wp_ajax_wpr_get_media_filtered_count', [$this, 'wpr_get_media_filtered_count']);
add_action('wp_ajax_nopriv_wpr_get_media_filtered_count', [$this, 'wpr_get_media_filtered_count']);
}
public function wpr_filter_grid_media() {
check_ajax_referer('wpr_filter_grid_media_nonce');
// ... rest of handler
}
public function get_main_query_args() {
$settings = isset($_POST['grid_settings']) ? sanitize_array($_POST['grid_settings']) : [];
$taxonomy = isset($_POST['wpr_taxonomy']) ? sanitize_text_field($_POST['wpr_taxonomy']) : '';
$term = isset($_POST['wpr_filter']) ? sanitize_text_field($_POST['wpr_filter']) : '';
// ...
if ( isset($_POST['wpr_offset']) ) {
$args['offset'] = intval($_POST['wpr_offset']);
}
// ...
$field_key = isset($_POST['grid_settings']['tax1_custom_color_field_text'])
? sanitize_text_field($_POST['grid_settings']['tax1_custom_color_field_text'])
: '';
$cfc_text = get_term_meta($term->term_id, $field_key, true);
}
The patch added check_ajax_referer('wpr_filter_grid_media_nonce') to the wpr_filter_grid_media() handler method. This WordPress API function verifies that the _wpnonce parameter in the request matches a nonce previously generated for the same action, preventing attackers from forging valid requests. Additionally, the patch introduced input sanitization via sanitize_text_field() and intval() to prevent secondary injection attacks through unsanitized $_POST['grid_settings'] array values passed directly to get_term_meta().
Root Cause
CWE-352: Cross-Site Request Forgery (CSRF). The dataflow is straightforward: an attacker-controlled HTTP request carrying AJAX action parameters (action=wpr_filter_grid_media, wpr_filter, wpr_taxonomy, grid_settings) reaches the WordPress AJAX dispatcher, which routes to wpr_filter_grid_media() without cryptographic verification that a logged-in admin authorized the request. The plugin registered the AJAX handler on both wp_ajax_ (authenticated) and wp_ajax_nopriv_ (unauthenticated) hooks, making it globally callable. No nonce token was ever validated, allowing the attacker to cross the trust boundary between attacker-controlled site and WordPress admin session.
Why It Works
The load-bearing line is check_ajax_referer('wpr_filter_grid_media_nonce'). If removed, an attacker's forged POST request would still succeed because no other control in the code path verifies request origin. The sanitization (sanitize_text_field(), intval()) addresses a secondary vulnerability in the same function—unsanitized $_POST['grid_settings']['tax1_custom_color_field_text'] passed directly to get_term_meta() could be exploited to manipulate term meta lookups or leak sensitive data—but sanitization alone does not stop a CSRF attack. The nonce check is the single gating function; without it, the CSRF remains exploitable even if inputs are clean. By adding both, the engineer implemented defence-in-depth: nonce verification stops the forgery vector, and input sanitization prevents the secondary meta-injection attack that a compromised or malicious AJAX call might attempt.
Hardening Checklist
- Register all AJAX handlers on
wp_ajax_only (removewp_ajax_nopriv_) unless the action genuinely requires unauthenticated access, and in that case add explicitcheck_ajax_referer()withdie()on failure. - Call
check_ajax_referer('action_nonce_name')at the top of every AJAX handler, before any$_POSTor$_GETaccess, and usewp_nonce_field()in frontend forms andwp_localize_script()to pass nonce tokens to JavaScript. - Sanitize and validate all
$_POST,$_GET, and$_REQUESTinputs viasanitize_text_field(),sanitize_email(),intval(), orwp_parse_args()before use in database queries or meta functions. - Audit all AJAX handlers in a plugin using grep for
add_action.*wp_ajaxand verify each hascheck_ajax_referer()early and appropriate capability checks (e.g.,current_user_can('manage_options')) if admin-only. - Use WordPress escaping functions on output (
esc_attr(),esc_html(),wp_kses_post()) to prevent stored and reflected XSS even if input sanitization fails.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-0393