The Exploit
An authenticated attacker with Contributor-level access or higher can store a malicious shortcode attribute and execute it whenever the page is viewed.
## store payload
curl -i -k -X POST "https://TARGET/wp-json/wp/v2/posts" \
-H "Cookie: wordpress_logged_in_example=..." \
-H "Content-Type: application/json" \
-d '{
"title": "XSS test",
"slug": "wishsuite-xss",
"content": "[wishsuite_button button_text=\"<img src=x
"status": "publish"
}'
## trigger payload
curl -i -k "https://TARGET/wishsuite-xss/"
The first request stores a post containing the wishsuite_button shortcode with attacker-controlled button_text. The second request loads the published page and returns HTML containing the raw <img src=x> payload, which would execute in a browser as a stored XSS.
What the Patch Did
// Before
$atts = shortcode_atts( $default_atts, $atts, $content );
return Manage_Wishlist::instance()->button_html( $atts );
// After
$atts = shortcode_atts( $default_atts, $atts, $content );
// Sanitize shortcode attributes to prevent XSS
// Uses custom allowed HTML that includes SVG elements for button icons
$allowed_html = $this->get_allowed_button_html();
$atts['button_text'] = wp_kses( $atts['button_text'], $allowed_html );
$atts['button_added_text'] = wp_kses( $atts['button_added_text'], $allowed_html );
$atts['button_exist_text'] = wp_kses( $atts['button_exist_text'], $allowed_html );
return Manage_Wishlist::instance()->button_html( $atts );
The patch adds wp_kses() sanitization for the shortcode attributes button_text, button_added_text, and button_exist_text before they are handed to button_html(). This is an input sanitization control using WordPress’s HTML whitelist filtering API.
Root Cause
This is CWE-79: stored cross-site scripting. User-controlled shortcode attributes enter via the wishsuite_button shortcode and are merged by shortcode_atts() into $atts. Those values are passed directly into Manage_Wishlist::instance()->button_html( $atts ) without any sanitization or escaping, so attacker-supplied HTML/JavaScript in button_text, button_added_text, or button_exist_text crosses the trust boundary from post content into rendered page markup unchecked.
Why It Works
The load-bearing line is the first wp_kses() call:
$atts['button_text'] = wp_kses( $atts['button_text'], $allowed_html );
If that line were removed, attacker-supplied HTML in button_text would still be forwarded raw into the button renderer and the XSS would remain exploitable. The additional wp_kses() calls for button_added_text and button_exist_text are necessary to harden the related button-label attributes that share the same rendering path. The get_allowed_button_html() helper defines the whitelist needed to preserve legitimate icon markup while stripping unsafe tags and attributes.
Hardening Checklist
- Sanitize shortcode attribute values before rendering with
wp_kses()when HTML is allowed. - Escape all output in rendering functions with
esc_html()oresc_attr()for text inserted into HTML or attributes. - For shortcode fields that should not contain HTML, use
sanitize_text_field()orsanitize_textarea_field(). - Keep shortcode parsing and rendering separate: validate attrs with
shortcode_atts()and then sanitize before passing them to output. - Restrict authorable content paths where possible using WordPress capabilities like
current_user_can('edit_posts')and avoid giving untrusted roles access to raw HTML fields unless absolutely necessary.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-13838