WordPress XSS Vulnerability Can Result in Remote Code Execution (RCE)

This article discusses vulnerabilities in older versions of WordPress due to its pingback and trackback features, and flawed sanitizing mechanism. It describes how attackers can use HTML tags to bypass sanitizing and insert an XSS payload using the WordPress flaw. Finally, it concludes with advice on how to fix the vulnerability in WordPress.

WordPress XSS Vulnerability Can Result in Remote Code Execution (RCE)

On March 13, 2019, RIPS Technologies, a company specializing in static code analysis software, released details of a Cross-site Scripting (XSS) vulnerability they found in all versions of WordPress up to 5.1.1. The vulnerability had been disclosed on various websites under different categories. Some classified it as a Cross-site Request Forgery (CSRF) vulnerability, while others correctly classified it as an XSS. In this article, we will analyze this WordPress XSS vulnerability – numbered CVE-2019-9887 – that results in a Remote Code Execution (RCE) vulnerability.

WordPress XSS Vulnerability Can Result in Remote Code Execution (RCE)

The Pingback and Trackback Features of WordPress

In WordPress, a nonce value is required to prevent a CSRF attack. This is a random value that changes on each request and is only known to the server and the user’s browser. However, the comments section in WordPress has not used nonce values since 2009. That’s because the WordPress developers made two design decisions that restrict the proper implementation of a security token to prevent CSRF attacks.

The pingback and trackback features in WordPress require the absence of a nonce value in order to function properly:

  • The pingback feature notifies bloggers that their article has been linked from someone’s post. This increases the ratings of the website on any search engine using the backlink method in its SEO.
  • The trackback feature is used when bloggers link to websites that are not built with WordPress.  

The Sanitization of Comments in WordPress

When an attacker manages to redirect an authenticated WordPress admin user to access an attacker-controlled website, they can submit a form on behalf of this admin user in order to post a comment on a blog in the user’s name. It would seem that this alone would be viewed as a serious vulnerability by anyone but the WordPress developers. However, the behaviour is absolutely necessary if to avoid breaking the pingback.

In order to minimize the impact of a form submission on behalf of an administrator, they implemented a nonce that doesn’t prevent the CSRF in general. When the correct nonce is set (if the administrator used the actual form in order to post a comment), the HTML code within the comment is not as strictly sanitized as if the form submission was a result of CSRF.

So WordPress uses the nonce value ‘_wp_unfiltered_html_comment_disabled’ to check whether the comments should be sanitized from HTML tags, if the nonce token isn’t available on the comment requests made by the admin account.

Analyzing WordPress’ Flawed Sanitizing Mechanism

An engineer at RIPS Technologies spotted a flaw in the detection and sanitizing mechanism in the /wp-includes/comment.php file during static code analysis. This is the code block related to the flaw.

if ( current_user_can( 'unfiltered_html' ) ) {
   if (! wp_verify_nonce( $_POST['_wp_unfiltered_html_comment'], 'unfiltered-html-comment' )) {
       $_POST['comment'] = wp_filter_post_kses($_POST['comment']);
else {
   $_POST['comment'] = wp_filter_kses($_POST['comment']);

According to the code, if the current user doesn’t have permissions for unfiltered_html, the comment text is directed to the function wp_filter_kses to be sanitized from the HTML elements in it.

Since the attack request will be sent through the browser on which the admin account is logged in, and the admin account has permissions for unfiltered_html, the flaw on the code begins from the first if statement.

In the first conditional statement, if the nonce token isn’t verified, the comment text will go through a different function called wp_filter_post_kses, which sanitizes the input.

The main difference between the functions wp_filter_kses and wp_filter_post_kses is that wp_filter_kses removes all comments, except basic HTML tags such as the href attribute and the anchor <a> tag. The wp_filter_post_kses function sanitizes all the potentially risky HTML tags, but it’s much more permissive than the wp_filter_kses function.

Using the HTML Tags to Bypass Sanitizing

The engineer at RIPS Technologies discovered that the sanitization process of the HTML elements and attributes contained a major flaw. Once a comment is sanitized, the attributes belonging to the <a> tags are optimized for SEO purposes by parsing them to an associative array.

For example, if the attributes with the <a> tag are href="#" title="some link" rel="nofollow", the string will be parsed into an array where each attribute name is the key:

function wp_rel_nofollow_callback( $matches ) {
   $text = $matches[1];
   $atts = shortcode_parse_atts($matches[1]);

After this conversion, you can access the href feature of the anchor tag by using $atts["href"]. Next, WordPress checks whether the anchor tags in the comment has the rel attribute.

It’s important to note that the rel attribute in the anchor element can only exist if the commenter has an admin account. The wp_filter_kses function doesn’t allow this attribute; the wp_filter_post_kses function does.

  if (!empty($atts['rel'])) {
       // the processing of the 'rel' attribute happens here
       $text = '';
       foreach ($atts as $name => $value) {
           $text .= $name . '="' . $value . '" ';
   return '<a ' . $text . ' rel="' . $rel . '">';

Using the WordPress Vulnerability to Insert an XSS Payload

As illustrated by the lines of code in bold above, the concatenated string values are not sanitized in any way. If attackers add the following title attribute to the anchor tag in the comment, they can abuse the nature of this feature.

title='XSS " onmouseover=alert(1) id="'

The code line  <a title='XSS " onmouseover=evilCode() id=" '> would become the following after processing (notice the double quotes):

<a title="XSS " onmouseover=evilCode() id=" ">

Malicious users who wish to exploit this vulnerability would have to trick a user with admin privileges to visit a page that would trigger and execute the payload.

The comment request that has the XSS payload is sent just at this point.

<iframe name="hidden_iframe" id="hidden_frame" style="display:none;" >

<form target="hidden_iframe" style="display:none;" action="wp-comments-post.php" method="POST">
<input type="hidden" name="comment" value="<a title='XSS " onmouseover=evilCode() id=" '>"/>

<input type="hidden" name="comment_post_ID" value=1 />
<input type="hidden" name="comment_parent" value=0 />
<input type="submit"/>

In order to conduct the attack undercover, and avoid top level navigation while submitting the form, the response of the form is directed to the hidden iframe that has a display:none style as specified in the target attribute.

Finally, the blog post with the XSS payload comment would be viewed by the admin browser for the attack to be complete. Once the attack is executed, template files are edited through the admin panel. The attacker can then perform a PHP code injection and convert this XSS attack into a Remote Code Execution (RCE).

Fixing the Vulnerability in WordPress

If you use WordPress, the fastest and easiest way to solve this issue is to update to version 5.1.1 in which the vulnerability is fixed.

Further, Netsparker reports any scanned website that uses an out-of-date WordPress version and shows the related vulnerabilities of the current version. Here is an example of one such report.

out-of-date WordPress version

Further Reading

For further information, see the RIPS article WordPress 5.1 CSRF to Remote Code Execution.