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.
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.
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.
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.
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.
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
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.
|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.|
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.