The WordPress.org website holds the repositories for all plugins and themes that are used by all WordPress sites. Furthermore, it manages the accounts that developers use to edit the code of their themes and plugins. In this blog post, we investigate a critical stored XSS vulnerability on the WordPress.org website we have reported to the WordPress security team in May 2018.

Introduction

Finding a critical vulnerability in one popular WordPress plugin and exploiting it in the wild could allow attackers to easily hijack thousands to millions of websites. An example of this could be observed lately in the case of the popular plugin WP GDPR Compliance. One plugin thus represents a single point of failure for all the websites using it. However, in matters of risk to the WordPress ecosystem, there is something more outreaching than the security of popular plugins: the security of WordPress.org.

In May of this year, we have notified the WordPress security team about a critical Stored XSS vulnerability on the website. The vulnerability was present in the display of plugin version numbers in the repository. Thus, any user having a plugin, could have injected arbitrary JavaScript code into the WordPress.org website. The vulnerability was detected during our development of the coderisk.com website for which we had to sort the available versions for every plugin on the repository. The latter, due to the absence of a unified versioning scheme, turned out to be more challenging than expected. The over 50,000 plugins revealed to us many “creative” versioning schemes. One of which was the use of an image instead of a version number in a plugin that was last updated 10 years ago. Visiting the plugin’s page in the repository affirmed that the image was in fact being displayed. This, of course, sets alarm bells ringing in the mind of security researchers. A quick verification with a plugin we had access to confirmed that it was possible to inject arbitrary JavaScript in that location. In the following, we outline the technical details that lead to the vulnerability and explain how it could have been exploited to hijack plugins of other authors.

Technical Details

The source code of the WordPress.org website is openly available as part of the WordPress meta environment. Thus, we can investigate which part of the code made the vulnerability possible.

How Plugins are Stored

The WordPress.org website is built, of course, using the WordPress CMS. The plugins as presented in the plugin repository are merely posts of a dedicated post-type that are displayed with a special template.

To change files, upload new versions, and so on, developers do not interact with the website, but make use of the version control system Subversion. Once a developer gets a plugin into the WordPress repository, he is granted access to the Subversion server in which he can make changes to the hosted plugin from that time on. The authentication credentials are the same as those of the WordPress.org user account of the developer. Data such as plugin name, description, version and so on, are retrieved from special headers in the readme.txt and main PHP file of the plugin. The plugin repository on WordPress.org watches the Subversion server for changes and updates the data it has saved about plugins whenever necessary.

The Point of Injection

The vulnerability we have detected occured because of insufficient sanitization of the plugin version numbers before they get displayed on the corresponding plugin page in the repository.

The listing below shows the code responsible for displaying the version number.

wordpress.org/public_html/wp-content/plugins/plugin-directory/widgets/class-meta.php

43 44 < li > <?php printf ( __ ( 'Version: %s' , 'wporg-plugins' ), '<strong>' . get_post_meta ( $post -> ID , 'version' , true ) . '</strong>' ); ?> </ li >

As explained above, in the repository, plugins are represented by a dedicated post-type. Additional information about the plugin, such as the version, is added as meta data to the representative post. The last listing shows that the version number is retrieved from the database and printed to the output without any validation or escaping. At this point, if the version number is not sanitized before being stored in the database, the stored XSS is given.

The version number of plugins is retrieved out of the special header in the main PHP file.

wordpress.org/public_html/wp-content/plugins/plugin-directory/cli/class-import.php

56 57 58 59 60 61 62 63 64 65 66 namespace WordPressdotorg\Plugin_Directory\CLI ; ⋮ class Import { ⋮ public function import_from_svn ( $plugin_slug ) { ⋮ $data = $this -> export_and_parse_plugin ( $plugin_slug ); ⋮ $headers = $data [ 'plugin_headers' ]; ⋮ update_post_meta ( $plugin -> ID , 'version' , wp_slash ( $headers -> Version ) );

As can be seen in the listing above, the method WordPressdotorg\Plugin_Directory\CLI\Import:import_from_svn() is responsible for synchronizing the changes in the Subversion server with the data stored in the plugin repository. It extracts the information out of the plugin header section and readme.txt file with the export_and_parse_plugin() method, and saves the necessary changes. The field representing the version is saved as a meta value to the post representing the plugin. Only the wp_slash() function, which does not prevent XSS, is applied to the version between retrieval and saving.

How this could have been exploited

To exploit the found vulnerability, an attacker would have needed to possess a plugin in the WordPress.org plugin repository. Given how easy it is to contribute plugins, this is not difficult to attain.

An attacker could have hidden an arbitrary JavaScript payload in the version field of his plugin. This payload would have been executed in the context of WordPress.org each time someone would have visited the plugin’s page in the repository. Now, what possible damage the payload could have produced ?

When a plugin author is logged-in in WordPress.org, he can add other accounts as committers to his plugin. These accounts would be granted full write access to the Subversion repository of the plugin, and can thus modify the code of the plugin. Accounts granted access to the plugin on this way, also can add and remove committers. For instance, they can remove the main committer, which is the author of the plugin, without any verification or warning. This process of adding committers can be triggered with some simple JavaScript ajax requests. An attacker could have crafted an XSS payload that would silently add an account he owns as committer when a logged-in user that possesses a plugin visits the infected page. The attacker, being a committer at this point, could then hide backdoors in the hijacked plugins, and inject his payload into their version number to spread it even more. The spread could have been initiated by posting a link to the plugin containing the payload somewhere many developers exist (e.g., WordPress slack, or support forums).

Bonus: Reflected XSS in the Admin Dashboard

After finding the first vulnerability, we have decided to scan the WordPress.org codebase with the RIPS static code analyzer. This revealed another vulnerability, a reflected XSS, in the admin dashboard of WordPress.org/plugins.

wordpress.org/public_html/wp-content/plugins/plugin-directory/admin/tools/class-stats-report.php

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public function show_stats () { ⋮ if ( isset ( $_POST [ 'date' ] ) && preg_match ( '/[0-9]{4}\-[0-9]{2}\-[0-9]{2}$/' , $_POST [ 'date' ] ) ) { $args [ 'date' ] = $_POST [ 'date' ]; } else { $args [ 'date' ] = '' ; } ⋮ $stats = $this -> get_stats ( $args ); ⋮ printf ( __ ( 'Displaying stats for the %1$d days preceding %2$s (and other stats for the %3$d most recent days).' , 'wporg-plugins' ), $stats [ 'num_days' ], $stats [ 'date' ], $stats [ 'recentdays' ] );

In line 3 of the listing above, the user input received from $_POST[‘date’] is verified to have a certain pattern with a regular expression. The value retrieved from the user input is then printed without escaping in line 11 of the listing. A small pitfall in the way the regular expression is constructed made it possible to inject a payload despite that it might at first seem to verify the strict conformity of the input to the xxxx-xx-xx pattern.

Your browser does not support the video tag.

Because of the missing starting delimiter ^ in the regular expression, it matches anything that ends with a date in the desired format. Thus, a payload such as <script>alert(1)</script> 0000-00-00 would bypass the check and get executed. The RIPS static code analyzer correctly identified the regular expression as an insufficient fix and traced the input to the sink.

Time Line

What 2018/05/11 Vulnerability reported to the WordPress security team on Hackerone. 2018/05/12 The vulnerability was triaged and verified by the security team. 2018/05/12 The security team deploys a fix https://meta.trac.wordpress.org/changeset/7195.

Summary

In this blog post we have introduced two vulnerabilities we have detected on the WordPress.org website. The first, a critical stored XSS in the plugin repository, was exploitable by any user having a plugin in the repository. Big damage could have been done if malevolent attackers would have made use of it. The second, a reflected XSS in the admin dashboard of WordPress.org/plugins, demonstrates how a small pitfall in a regular expression can lead to vulnerable code.