The Exploit
An authenticated attacker with contributor-level access can inject arbitrary JavaScript into a WordPress page via a malicious iframeBox shortcode. The payload persists in the post content and executes in the browsers of all users who visit the page.
POST /wp-admin/post.php HTTP/1.1
Host: target.local
Content-Type: application/x-www-form-urlencoded
Cookie: wordpress_logged_in=<valid_contributor_session>
post_type=post&post_title=Innocent+Post&content=[iframeBox+width="400"+height="300"+link="https://example.com"+attr="onload=alert('XSS')"]&action=post&_wpnonce=<valid_nonce>
When any user visits the published post, the browser renders the iframe with an onload event handler that executes the attacker's script. The attacker observes the payload stored in the database (visible in post revisions and raw database dumps) and confirms execution when visiting the page in a browser, seeing the alert dialog fire.
The same attack works with other event handlers: onerror, onmouseover, and any attribute not explicitly whitelisted. Because wp_kses_post() allows broad HTML, the vulnerability is not limited to iframe attributes—attackers can inject additional HTML tags into the attr parameter.
What the Patch Did
Before:
$iframe = '<iframe width="' . esc_attr( $atts['width'] ) . '" height="' . esc_attr( $atts['height'] ) . '" src="' . esc_url( $atts['link'] ) . '" ' . wp_kses_post( $atts['attr'] ) . '></iframe>';
After:
$allowed_attrs = array();
if ( ! empty( $atts['attr'] ) ) {
preg_match_all( '/(\w+)=["\']?([^"\'>\s]+)["\']?/', $atts['attr'], $matches, PREG_SET_ORDER );
foreach ( $matches as $match ) {
$attr_name = strtolower( $match[1] );
$attr_value = $match[2];
if ( in_array( $attr_name, array( 'title', 'frameborder', 'allowfullscreen', 'loading', 'name', 'class', 'id' ), true ) ) {
$allowed_attrs[ $attr_name ] = esc_attr( $attr_value );
}
}
}
$attr_string = '';
foreach ( $allowed_attrs as $name => $value ) {
$attr_string .= ' ' . $name . '="' . $value . '"';
}
$iframe = sprintf(
'<iframe width="%s" height="%s" src="%s"%s></iframe>',
esc_attr( $atts['width'] ),
esc_attr( $atts['height'] ),
esc_url( $atts['link'] ),
$attr_string
);
The patch replaced a permissive sanitization strategy (wp_kses_post()) with a whitelist-based allowlist that parses the attr parameter, extracts only safe attribute names from a predefined list (title, frameborder, allowfullscreen, loading, name, class, id), and reconstructs the iframe string using parameterized output escaping via sprintf(). Event handlers like onload are silently dropped because they do not appear in the allowlist. All remaining attribute values are escaped with esc_attr() before insertion into HTML context.
Root Cause
CWE-79 (Improper Neutralization of Input During Web Page Generation) and CWE-400 (Uncontrolled Resource Consumption).
The attr shortcode parameter flows directly from post content (user input) into the iframe HTML string. The original code delegated sanitization to wp_kses_post(), which applies a broad allowlist suitable for post body content but unsuitable for restricting iframe attributes. wp_kses_post() permits HTML entities, inline styles, and event handlers if they are already written in valid HTML syntax. An attacker-controlled string like onload="alert(1)" passes wp_kses_post() untouched because it is syntactically valid HTML. The value then lands unescaped into the iframe element, crossing the trust boundary from attacker input to trusted rendered HTML.
Why It Works
The load-bearing line is the in_array() whitelist check:
if ( in_array( $attr_name, array( 'title', 'frameborder', 'allowfullscreen', 'loading', 'name', 'class', 'id' ), true ) ) {
Remove this line and the bug resurfaces: every attribute would be retained. The surrounding regex parsing and esc_attr() escaping are defence-in-depth. The regex extracts attribute names and values in a structured way, preventing attribute name confusion or value breakout. The esc_attr() call ensures that any remaining values cannot contain quotes or entities that re-open HTML context—but this is redundant if the whitelist is enforced, because only safe attributes reach that code path. The engineer added the parsing and escaping to ensure that even whitelisted attributes cannot be abused via encoding tricks (e.g., a class name containing "> followed by an event handler). The whitelist is the primary control; the escaping is the insurance policy.
Hardening Checklist
- Use
sanitize_key()or strict regex on all attribute names before accepting them, never rely onwp_kses_post()for attribute-level filtering. Apply a whitelist of known-safe attribute names, not tag names. - Rebuild HTML strings using parameterized functions like
sprintf()andwp_kses_attr(), not string concatenation with permissive filters. Assign each HTML context (attribute value, tag content, href) its own escaping function:esc_attr(),esc_html(),esc_url(). - Test shortcode attributes by injecting event handlers (
onload,onerror,onclick) and encoded payloads (javascript:) during code review. If the shortcode accepts anattrparameter, assume it will be exploited. - Audit all uses of
wp_kses_post()in shortcodes; it is designed for post body content, not for sanitizing fragments that will be embedded into HTML attributes. Usewp_kses_post()only when you intend to allow rich HTML; for attributes, usewp_kses_attr()or reject the parameter entirely. - Document the allowed attributes in inline comments and update the whitelist only when a business requirement mandates a new attribute. Keep the list as small as possible; every attribute in scope is a potential attack surface.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-12122