Tag Archives: Security

Arbitrary SQL and PHP execution in WordPress plugin via shortcode

I keep an eye on vulnerability reports for WordPress plugins, always trying to learn more about plugin security. As a plugin developer myself, it is important to learn about pitfalls so that I don't make the same mistakes in my own plugins.

A while ago I saw a report for a plugin that provided a shortcode for executing SQL queries. Providing a feature like that in a shortcode is problematic, because any user can execute a shortcode on the site, as long as they are logged in.

The details of why that is possible were first explained (to my knowledge) in this vulnerability report put out by Securi. The short story is that WordPress core includes an Ajax callback for the parse-media-shortcode action that does just what its name sounds like: it parses shortcodes (and not just media shortcodes either). Because that action is not protected by any capability checks, it is possible for any logged in user to pass a shortcode to it in the shortcode parameter, and it will be executed.

In the case of the plugin offering a shortcode that ran an SQL query, this meant that any user of the site could run any SQL query that they wanted to. Like this, for example:

[[sql]SELECT user_email, user_pass FROM wp_users[/sql]]

For some reason, I clicked through the link in that report to see if the plugin was still available on WordPress.org. It wasn't, but the plugin directory automatically provided helpful results for similar plugins.

Plugin directory search results for "sql-shortcode".

The first result was the EZ SQL Reports Shortcode Widget and DB Backup plugin. A look at the plugin's page revealed this:

There is also an shortcode for the wpdb::get_var function that you can use to display a single value from your database. For example, this will display the number of users on your site:
[sqlgetvar]SELECT COUNT(*) FROM wp_users[/sqlgetvar]

Uh oh. Here we have another plugin that allows arbitrary SQL to be run via a shortcode.

To make sure that things were actually as bad as they seemed, I took a look at the code. As it turns out, things weren't as bad as they seemed. They were worse.

function ELISQLREPORTS_get_var($attr, $SQL = "") {
global $wpdb;
if (!is_array($attr)) {
if (strlen($attr) > 0 && strlen($SQL) == 0)
$SQL = $attr;
$attr = array("column_offset"=>0, "row_offset"=>0);
} elseif (isset($attr["query"]))
$SQL = $attr["query"];
if (!(isset($attr["column_offset"]) && is_numeric($attr["column_offset"])))
$attr["column_offset"] = 0;
if (!(isset($attr["row_offset"]) && is_numeric($attr["row_offset"])))
$attr["row_offset"] = 0;
$var = $wpdb->get_var(ELISQLREPORTS_eval($SQL), $attr["column_offset"], $attr["row_offset"]);
if (isset($_GET["get_var"]) && ($_GET["get_var"] == "debug") && !$var && $wpdb->last_error)
return $wpdb->last_error;
return $var;
add_shortcode("sqlgetvar", "ELISQLREPORTS_get_var");

Yes, the ELISQLREPORTS_eval() function is as bad as it sounds.

function ELISQLREPORTS_eval($SQL) {
global $current_user, $wpdb;
if (@preg_match_all('/<\?php[\s]*(.+?)[\s]*\?>/i', $SQL, $found)) {
if (isset($found[1]) && is_array($found[1]) && count($found[1])) {
foreach ($found[1] as $php_code)
eval("\$found[2][] = $php_code;");
$SQL = $wpdb->prepare(preg_replace('/<\?php[\s]*(.+?)[\s]*\?>/i', '%s', $SQL), $found[2]);
return $SQL;

Not only is there the potential for arbitrary SQL execution, but the shortcode also provides another special feature: it allows arbitrary PHP to be embedded as well! (Although notice the care to try and prevent SQLi by using $wpdb->prepare() 😃.)

Remember, shortcodes can be executed not just by anybody who can edit a post (which would include Contributors), but also by even Subscriber-level users using the admin-ajax.php trick explained out above. This means that any user on a site can use the shortcode provided by this plugin to execute arbitrary SQL and PHP in the context of the site.

A PoC using the example shortcode:

<form action="http://src.wordpress-develop.dev/wp-admin/admin-ajax.php" method="post">
<input name="action" value="parse-media-shortcode"/>
<input name="shortcode" value="[sqlgetvar]SELECT COUNT(*) FROM wp_users[/sqlgetvar]"/>
<input type="submit" value="Execute SQL!"/>

Log in as a subscriber, submit the form, and viola!

The user count from my test site returned with the normal results of the parse-media-shortcode action.

The next step was to try and contact the plugin developer. First, I looked at the plugin's page in the plugin directory, to see if there was a contact method mentioned anywhere. I then went to the plugin author's profile to see if they had a contact listed there. They did have a website listed, but I couldn't find any means to contact them through that.

The only remaining option appeared to be to attempt to contact them through a direct message on WordPress's Slack. When that didn't get a response, I later remembered to also check in the plugin's code, as often an email address is included in the copyright notice. Fortunately, that was the case, so I sent an email to that address.

Within 24 hours I received a response, and soon after version 4.17.38 was released. It mitigates the issue by only allowing SQL to be used in the shortcode that matches one of the SQL reports created using the report creation feature offered by the plugin. A note regarding this was added to the plugin readme:

 Note: because of a recently discovered exploit in the WordPress shortcode functionality it is now required that an admin user create an SQL Report with the exact query that will be used in the sqlgetvar shotcode, otherwise any subscriber could white their own shortcode query.

This means that you can no longer execute arbitrary SQL. If you provide a query not already available as an SQL report, you'll just get the error "This SQL Query has not been allowed by an Administrator."

Attempting to execute arbitrary SQL now results in an error message.

Because SQL reports can only be created by a user with the 'activate_plugins' capability, this should sufficiently mitigate the issue.

The big takeaway here is that you need to be very careful what you make possible via a shortcode. Plugin authors have been able to get by with some things in shortcodes that shouldn't really be there, because historically at least shortcode execution was usually only possible for Contributor-level users and up. But with the introduction of wp_ajax_parse_media_shortcode() in WordPress 4.0, vulnerabilities in shortcodes are now exposed to even Subscriber-level users, making any site that allows user registration immediately exploitable if a shortcode is vulnerable.

This is a point where the plugin community needs some education, and where perhaps core should even consider placing more restrictions on this Ajax action to mitigate these issues (though vulnerabilities like these in shortcodes would still be exposed to anybody who could edit posts). I did bring this up with the security team, and they said that they do have plans to add more restrictions to the parse-media-shortcode Ajax callback in the future.

But until then, remember: Anything that a shortcode can do, any logged in user can do.

Unserializing Safely

Avoid object injection with this:

    if ( 1 !== preg_match( '~O:\d~', $maybe_dangerous ) ) {
        // All is well, this is safe to unserialize.

For security it is best if you use json_* or something else instead. But if you are working with a project built by someone else and you have no choice, this will keep you from unserializing any objects.

Improving Plugin Security One Day at a Time

As a security-conscious developer, I like to inspect a WordPress plugin’s source code before I install it on one of my sites. I didn’t do this, formerly. But after the first time I did (and found a vulnerability), I believe more strongly in its importance.

I also keep tabs on WordPress related security reports on sites like Packet Storm, Secunia, Exploit Database, and Bugtraq. I use IFTTT for this:

IFTTT Recipe: Email me WordPress security reports from Packet Storm connects feed to email

IFTTT Recipe: Email me WordPress security reports on Secunia connects feed to email

IFTTT Recipe: Email me WordPress security reports on Exploit DB connects feed to email

IFTTT Recipe: Email me WordPress security reports from Bugtraq connects feed to email

Over the last year I’ve come to realize something: almost every plugin has vulnerabilities. Okay, all code has vulnerabilities. But almost every plugin has glaringly obvious vulnerabilities, or at least that can be found without a great deal of effort.

That seems a little bit scary. Of course, many of these aren’t particularly serious. But they demonstrate that the people creating the plugins often don’t understand basic WordPress security.

I know I’m not the only one who realizes this. Probably, most experienced WordPress developers come to this realization sooner or later. But just realizing it and thinking, “I wish it weren’t so,” isn’t going to make things any better. So I’ve decided to do something about it. I’m going to improve plugin security one day at a time. I’m going to try to do something, every day, that will make WordPress plugins more secure.

How will I do this? In many ways. I said that I’m not the only one who understands the situation, and I’m not the only one who’s doing things about it either. The folks on the plugin review team for WordPress.org try to catch the vulnerabilities when a plugin is first submitted to the repo. They also handle security reports and make sure they make it to the plugin authors. There are also the great folks on the WordPress docs team contributing to the plugin developer handbook. Hopefully the security-related things in there will help to educate the next generation of plugin developers about these issues better right from the start.

One of the greatest things I can do is to try to help educate plugin devs about security. I’ll also try to make sure that reports of vulnerabilities make their way to the plugin developer. And I’ll continue to review plugins that I use, and report vulnerabilities that I find. I might then move on to investigating other popular plugins as well. I have even pondered creating a PHP source code security scanner, but that would be quite a project. (There are many of these out there, but none of them are intelligent enough for me.)

Regardless of how, I want to try to do a little something every day to improve WordPress plugin security. If just a few folks did this, how different might things look in a few years? We’ll just have to wait and see.

A Week with HackerOne

About three months ago I signed up with HackerOne, and created a bug bounty program for my WordPoints plugin. I’m writing this post to document my experience with HackerOne, for anyone else who may be thinking of using it.

When you first create your program, it is private. This gives you time to tweak things and gain some familiarity with the system. You also have the opportunity to invite up to 100 of the top hackers to participate and the private pre-launch program. I did invite all 100 (though not all at once), but there wasn’t any activity. That is probably because I hadn’t set a minimum bounty amount yet.

Last Friday I decided to make the program public. This timing roughly corresponded with the release of WordPoints 1.7.0, which included some security fixes that I’d discovered on my own.

What should you expect when you launch publicly? I got 15 bug reports in the first 24 hours, and about a third of them were probably in the first couple hours after launching.

The reason for the immediate spike in activity (of which there had been none previously), is probably due at least in part to my having set a minimum bounty (though this was only $25).

Of those 15 reports, most of them were low quality. The reporter obviously hadn’t read the program description, and didn’t know what kind of bugs I was looking for and what sort of vulnerabilities I would consider invalid. Of those 15, only two were vulnerabilities that actually needed fixing. I’ve received 4 more reports this week, but none of them have been valid source code vulnerabilities either.

So, now you know what you can expect with your first week after launching a bug bounty program on HackerOne. I suspect that if you wanted to avoid the first-minute slew of reports, you could wait until later to set a minimum bounty amount.

All in all, I am very pleased with HackerOne. The UI is great and has the tools you need to respond quickly. I think also that report quality will probably increase as better researchers join in in an unhurried manner. Well, at least if I decide to increase the bounty in the future. :-)

WordPress Core and PHP version support

WordPress core has supported PHP versions >= 5.2.17 for a long time. And there’s been many a discussion by developers and contributors about dropping support for 5.2, making the minimum requirement 5.3. But the lead developers are standing against this move as long as 5.2 makes up a significant part of the user base. And less than a year ago it made up about 50%.

We know the approximate proportions of which versions are being used because this information is available through the stats page on wordpress.org. I’ve been watching this page for a while, and I’ve caught a definite trend away from PHP 5.2 toward newer versions, especially 5.3.

Today, David Anderson posted this to the wp-hackers mailing list:

Rejoice: PHP 5.3 is, at last, on the threshold of overtaking PHP 5.2 as the most common version that WordPress is being run on…


… just in time for the PHP 5.3 end-of-life! http://marc.info/?l=php-internals&m=140605526629324&w=2 (though for many, there will continue to be security patches for several more years, since PHP 5.3 was part of RHEL 5 and hence Cent OS 5).

From the end of this month, around 21.5% of WordPress installations will be running on non-EOL-ed PHP versions.

Best wishes,

You can see this for yourself:

PHP versions WordPress is running on

If you want to know where my sites are on the graph, they make up a part of that green streak for 5.5. :-)

As you can see, this is a mixture of good and bad news. The good news is that people are moving away from 5.2, which no longer recieves security patches from PHP. The bad news is that the majority of these folks, who are moving to 5.3, will just be back in the same boat at the end of the month. And so will WordPress.

It’s not that WordPress needs to drop support for 5.2 so that it can introduce cool new features or improve its performance. Sure, there may be a few ways in which 5.2 is holding us back, but they really aren’t that significant. It’s mostly that the grass is always greener on the other side of the fence.

The thing that really strikes me here is how openly vulnerable software is dominating the market. It isn’t just the PHP versions WordPress is running on, it’s WordPress itself:

Versions of WordPress Pie Chart

What has always seemed strange to me is that only a fifth of sites are running the latest major version of WordPress (3.9). It makes me wonder what the breakdown of PHP versions is relative to WordPress versions, but if I remember correctly, we’ve been told that it’s pretty much the same as overall.

The maintained WordPress versions (3.7, 3.8, 3.9), make up only about 38% of the live websites it’s running on out there. In other words, 62% of WordPress sites are vulnerable right off the bat. And, what really strikes me, is that WordPress 3.0 continues to maintain a larger portion of the market than any other version.

Why is this? Why do folks continue to run old versions of WordPress? I assume it has much to do with a perceived difficulty in upgrading, whether that difficulty is real or imagined. I’m guessing people think of it like this:

  1. Oh, there’s a new version of WordPress.
  2. Updating takes time though, and what if something goes wrong?
  3. I hear there are new features, but do I really need them?
  4. Nah, my site is running fine, it isn’t worth the trouble.


updates = (new features) - time - (the chance your site will break)

It seems that many people think that

new features < time + (the chance your site will break)

And in that case, it really doesn’t make sense to update.

But, of course, the equation is wrong. The truth is that

updates = (new features) + (bug fixes) + (security patches) - time - (the chance your site will break)

It’s those security patches that push updates from the trivial to the must.

That’s why I wrote a post asking plugin developers not to add backward compatibility code to support older WordPress versions. Basically, nobody should ever be running those versions.

Which brings us back around to the unmaintained PHP versions that most sites are running on. Should WordPress continue to support these versions? You might think I’d have to say “No”. But in this case, it’s a little more complicated. People know they are running WordPress (at least most of them do), and they either know or could easily find out what version of it they are running. But PHP, well, most people don’t know what on earth that is. And even those that do often have no control over what version they are running. In other words, it is up to the web host.

That doesn’t mean WordPress couldn’t drop an older version of PHP. But it means that if it did, people who use WordPress would panic, because they don’t really understand what is happening, why it is happening. And most of them have no control over what version of PHP they are running. So, you’d have to tell them to contact their host. But their host may or may not be able to just provide a newer version of PHP with a snap of the fingers. In other words, dropping a PHP version that 40% of sites are running on would probably alienate a lot of users, who are using WordPress precisely because they don’t have to worry about all of that technical stuff. But does that mean that we have to put up with hosts running dead PHP versions “for the sake of our users”, when that is possibly making those same users’ sites more vulnerable?

Let’s go back to what I said about WordPress’ lead developers reluctance to drop support for 5.2:

It’s not that WordPress needs to drop support for 5.2 so that it can introduce cool new features or improve its performance. Sure, there may be a few ways in which 5.2 is holding us back, but they really aren’t that significant. It’s mostly that the grass is always greener on the other side of the fence.

In other words, it’s the same false argument being used by folks who don’t feel like updating WordPress: I don’t really need the features, so it isn’t worth the trouble.

Of course, there is a difference. WordPress isn’t responsible for whether a user’s hosting is secure, the host is. And, as David Anderson pointed out, many OS’s and hosts will be providing their own security patches for PHP. In other words, the equation is much harder to solve than it looks. Offering security updates for WordPress, and hoping hosts patch their PHP, may indeed be the best course of action.