When developing a Wordpress plugin/theme, is_admin()
is probably one of the most used functions to check whether the current code is running under the admin section. Unfortunately, most developers tend to believe that this is enough to make sure THAT code is run by an user who HAS privileges AND the right access. Of course, this is all wrong.
The door to Hell
On 21 March, Wordfence Security Team disclosed two vulnerabilities in Social Warfare, a very popular plugin in the WordPress ecosystem. One was a Stored Cross-site Scripting Attack (XSS) and the other a remote code execution (RCE), both tracked by CVE-2019-9978.
Both vulnerabilities has something in common: the misuse of the is_admin()
function in WordPress. Let's see why:
<?php
/**
* Migrates options from $_GET['swp_url'] to the current site.
*
* @since 3.4.2
*/
if ( true == SWP_Utility::debug('load_options') ) {
if (!is_admin()) { // THE DOOR TO HELL
wp_die('You do not have authorization to view this page.');
}
$options = file_get_contents($_GET['swp_url'] . '?swp_debug=get_user_options');
BLA BLA code
<?php
$options = str_replace('<pre>', '', $options);
$cutoff = strpos($options, '</pre>');
$options = substr($options, 0, $cutoff);
$array = 'return ' . $options . ';';
try {
$fetched_options = eval( $array ); // WELCOME TO HELL
}
Those above are snippets from the Social Warfare plugin. The code reported has the intent to parse a remote text document into a key->value array of options for the plugin to use. However, instead of using a typical data storage format like a JSON, the plugin generated options as an actual block of PHP code, which is crunched by eval()
into an array.
Eventually, the Wordfence guys explain this all fair well:
When a malicious injection matches the format used by the plugin normally, as in the attack campaigns running currently, an XSS payload can be injected into one or more of those settings. However, the contents of
<pre>
tags in the remote file are arbitrarily passed toeval()
, meaning the code within is executed directly as PHP.
So, enough for now. Let's get back to the basics.
is_admin() purpose
As mentioned in the Wordpress Codex:
is_admin() is not intended to be used for security checks. It will return true whenever the current URL is for a page on the admin side of WordPress. It does not check if the user is logged in, nor if the user even has access to the page being requested. It is a convenience function for plugins and themes to use for various purposes, but it is not suitable for validating secured requests.
Does this mean i should not ever use is_admin()
? Nope. At least, not only.
This function is crucial when writing code that is needed only in the Wordpress backend admin area, but it should always be the first step to do. After that, eveything you write must be encapsulated under a security check layer which basically takes into account 2 things: User session and User capabilities. We can then determine a simple progressive security path like the following:
- Is the user viewing an admin section?
- If yes, is the user really logged in (valid session)?
- If yes, has the current logged in user the capability to view this section?
So, before going further, let me tell you a couple of things.
User session
Wordpress is an advanced system which takes security very seriously. When developing within its infrastructure, we have all the necessary tools to write good and secure code.
is_user_logged_in()
is the first function you always need to use when developing under the admin area. It just does what its name is: checks if the user is logged or not. Although this might seem a simple task, when used along with is_admin()
, grants a first layer of security based on sessions, nonce tokens and so on.
User capabilities in Wordpress
Let's blockquote the Wordpress Codex again:
WordPress uses a concept of Roles, designed to give the site owner the ability to control what users can and cannot do within the site. A site owner can manage the user access to such tasks as writing and editing posts, creating Pages, defining links, creating categories, moderating comments, managing plugins, managing themes, and managing other users, by assigning a specific role to each of the users.
Infact, not every part of the admin area should be available to all logged-in users. At the same time, you might want not only admins to get access to that part, so there an handy tool Wordpress puts on the table: the magic user_can( $user, $capability )
This function takes the User ID as first parameter and a specific 'capability' as second. If the current logged-in user has this one, the function returns true.
<?php
if ( ! is_admin() ) {
wp_die( 'This code is for admin area only' );
}
if ( ! is_user_logged_in() ) {
wp_die( 'You shall not pass!' );
}
if ( ! user_can( $id_user, 'my_capability' ) {
wp_die( 'Yes, you too... shall not pass.' );
}
// Do your stuff
Wordpress has a long list of user roles and capabilities, so it's just up to you to pick up the right choice.
The use of these three functions all together is a good start in writing secure code that, at least, can prevent most of the common vulnerabilities that may affect Wordpress plugins or themes.
Coding pill: Paranoia is Security's best friend
Found a typo? Make a PR
Cover image by Alena Aenami
Top comments (10)
We are actually the ones that disclosed the vulnerability in Social Warfare on March 21, which Wordfence, as they often do, copied from us without credit.
The confusion over is_admin() was warned about before it reached a production release of WordPress, but the WordPress developers apparently think it is "pointless" to address it.
Nice one, thanks for the clarification, couldn't understand why
is_admin()
was false all the time for me on a frontend page!One thing maybe extra to note is that if you're writing a plugin, the functions you suggest using may not even exist until a certain point in the boot sequence. I've found it's safe in the
plugins_loaded
action.Great writeup. 1 small thing: in the code sample you use
is_user_loggedin()
but you missed the last underscore in the function. Maybe that changed in some version of WordPress itself?developer.wordpress.org/reference/...
You're right. Thanks for reporting this.
And this is why being vague and unclear is horrible for security. But it's also the developer's fault for not checking the docs.
You're right, that's a common mistake. Commenting code should be a priority when deploying to the public, especially when building plugins.
Yes, you're right at this point. I've created this website wowshoots.com/ but i just make it secure.
Great article!
In my plugins there are often options that some admins only want available in the back-end, while front-end submission & editing is on the rise. A rather simple check for /wp-admin/ (excluding ajax.php) in the url suffices for me. If it's there, the user has to be logged in, be in the back-end and then current_user_can() will decide to yes/no show the rest.
Hi !
You don´t mention the current_user_can() function I think it grants much better validation.
Nice post. Thanks