Discovering Vulnerabilities in WordPress Plugins at Scale

Discovering Vulnerabilities in WordPress Plugins at Scale

Author: Luke (@hakluke) Stephens

It always blows me away to think that WordPress runs 43% of all websites, including those without a content management system (CMS) 🤯. A single open source project is responsible for such a huge part of the internet! It’s interesting to think about what might happen if a severe vulnerability was discovered in WordPress core. Thankfully, the fact that WordPress is so prevalent means that it also gets a lot of ongoing attention from security researchers around the world – helping to maintain security posture.

WordPress plugins though? Not so much. There is some guidance on how to create secure plugins in the official documentation but the details are a bit light – and too often they are not followed.

I spent much of my life as a developer writing WordPress plugins, and I have since moved into the wonderful world of application security; appsec for short. Because of my experience with WordPress, I am occasionally asked to perform secure code reviews of WordPress plugins. A few weeks ago I was doing exactly this when I thought “wouldn’t it be cool to perform this type of security testing on all WordPress plugins, or at least the most popular ones”! The most popular WordPress plugins have well over 5 million active installations, so finding a vulnerability in any of these would have a huge impact.

Naturally, I started thinking about how this might be achieved. The first step would surely be to download all of the most popular WordPress plugins so that we can analyze them manually. 

Downloading The 1000 Most Popular WordPress Plugins

I navigated over to the official WordPress plugin repository page, to the “popular” category. There are 49 pages with 20 plugins on each page, downloading them all manually would be a real chore, but never fear, bash is here:

for i in $(seq 1 100); do curl -s "$i/" | grep "" | grep "<h3 class=\"entry-title\">" | sed "s/.*a href=\"//g" | sed "s/\" rel.*//g"; done | tee plugin-urls.txt | parallel -j 20 "curl -s {} | grep downloadUrl | sed 's/.*: \"//g' | sed 's/\".*//g'" |tee plugin-download-urls.txt | parallel -j 20 "wget -P ./plugins "; ls ./plugins | parallel -j 20 "unzip ./plugins/{} -d ./unzipped-plugins/"

Okay okay, it’s not pretty. I wouldn’t say it is the finest thing I have ever coded, but it gets the job done. Running this script will download all of the plugins from the “popular” category into ./plugins, and then unzip them into ./unzipped-plugins.

It looks like we have 958 plugins in total:

number of wordpress plugins

Your unzipped plugins folder should look something like this:

unzipped wordpress plugins

Now, we can start sifting through the code for vulnerabilities. Checking them all manually would take far too long though, so we’re going to use some linux utilities (mostly grep) and some clever regular expressions to discover some low-hanging fruit.

Hunting for XSS Using Regular Expressions

First let’s take a look at a really basic one-line XSS vulnerability in PHP:

echo $_GET['name'];

There are infinite variations of this, for example:

echo "Hello " . $_GET['name'];

But we can catch a decent amount of them by using the following regular expression:


Let’s try it! Note that we could use grep for this, but personally I prefer to use ripgrep because it is much faster when searching over large sets of data. In this case, we can just run rg –no-heading “echo.*\\\$_GET” which will recursively search all files for this regular expression:

ripgrep for wordpress vulnerabilities

We can immediately see 2 problems. The first is that a lot of lines match this pattern, and the vast majority are not vulnerable because they have some kind of XSS mitigation, for example:

echo htmlspecialchars($_GET['name'])

The second is that we are showing up some minified client-side JavaScript files, which is spamming the terminal with huge chunks of text that we don’t care to see.

As we come up against these cases, we can filter them out using grep, for example, to ensure that we only return PHP files we could do something like this:

rg --no-heading "echo.*\\\$_GET" | grep "\.php:"

And to also remove any lines that utilize htmlspecialchars() we could do something like this:

rg --no-heading "echo.*\\\$_GET" | grep "\.php:" | grep -v htmlspecialchars

As we start reviewing the code, we can start seeing a lot of cases where XSS has been mitigated. I eventually landed on the following command which has some very aggressive filtering. If I wanted to find more vulnerabilities and I had more time, I wouldn’t be this aggressive, but in this case I’m looking to find a vulnerability as quickly as possible.

rg --no-heading "echo.*\\\$_GET" | grep "\.php:" | grep -v -e "(\$_GET" -e "( \$_GET" -e "esc_" -e "admin_url" -e "(int)" -e htmlentities

This returns only 17 lines, and then I deleted another 10 manually because I could see that they were not vulnerable for various reasons. The remaining 7 lines looked like this. I have edited them slightly because I don’t want to give away the plugins that they are contained in:

The First Finding

The first line looked line looked like this:

<input type="hidden" name="reset_key" value="<?php echo utils()->array_get('reset_key', $_GET); ?>" />

It appears that the reset_key parameter value is echoed straight into the hidden value of a form without any encoding. Exploiting this may be as simple as figuring out where this code reflects into the application, and then appending ?reset_key=payload to the URL.

The Second Finding

This line is from the same plugin, and the same file as the previous one – they have done exactly the same thing, but on a different parameter.

<input type="hidden" name="id" value="<?php echo utils()->array_get('user_id', $_GET); ?>" />

The Third Finding

This one is very similar, but it echo’s the return value of a sprintf command. Unfortunately the string variables in the sprintf contain $_GET[‘url’] which is not escaped at any point before being reflected back to the user.

echo sprintf( '<div class="lightbox"><div class="clearfix"><img class="image" src="%s" /><div class="actions"><a href="#" class="close">%s</a></div></div></div>', $_GET['url'], __( 'Close', 'elements' ) );

The Fourth Finding

This one really could not be more straightforward!

echo $_GET['error'];

The Fifth Finding

This one is very similar to the previous, but it is reflected within double quotes. An example payload for testing this XSS could be something like “><script>alert(1)</script>.

<form id="form1" method="post" action="<?php echo $_GET['stateid'];?>">

The Sixth Finding

This is essentially the same case as the previous finding.

echo '<a href="?take=' . $_GET['page'] . '&tab=' . $cluster['name'] . '" class="nav-tab' . ($cluster['name'] == $currentCluster['name'] ? ' nav-tab-active' : '') . '">' . $cluster['label'] . '</a>';

The Seventh Finding

And another one!

echo '<div class="table" id="info-tab-' . $_GET[ 'tab' ] . '">';

Now What?

This gives us 7 very good leads for XSS vulnerabilities in popular WordPress plugins. It’s important to note that, while these lines look vulnerable by themselves, it’s possible that they are not vulnerable within the context of the entire plugin.

As a contrived example, this looks vulnerable by itself:

echo $_GET['error'];

But when you review the file, it might look like this:

if $_GET['error'] === "static string") {
         echo $_GET['error'];

Or like this:

if False {
    echo $_GET['error'];

Or even like this:

echo $_GET['error'];

To know for sure, you would need to review the code manually, then spin up a test WordPress environment, install that plugin and build a proof of concept (PoC) to check that it works in practice.

Vulnerabilities Other Than XSS

The same concepts apply to other vulnerability types, but of course the execution will be different. For example, if you are looking for Remote Code Execution (RCE) vulnerabilities, you may be searching for terms such as “exec(” or “shell_exec(“. Or if you are looking for SQL injections, you may be trying something like “SELECT \* .*\$_”. Get creative with it!

Beyond Grep

Of course, for a more thorough analysis, you could use a proper PHP SAST solution that will analyze the sources and sinks in the code without relying on single-lines of vulnerable code. There is a huge list of PHP SAST tools here. Beware – just like our grep methods, these tools nearly always produce a lot of false positives to sift through, but if you have the patience they can be an excellent way of discovering vulnerabilities.


With half an hour and some knowledge of PHP, you can start hunting for vulnerabilities in WordPress plugins – be sure to let the plugin authors know what you find, to help create a more secure internet!

Leave a Comment

Your email address will not be published.