Five Commandments Of Secure Plugin Development

If you purchase through a link on our site, we may earn a commission. Learn more.

Table of Contents
WP Engine High Performance Hosting
BionicWP Hosting

In the last couple of years WordPress has grown into the biggest publishing platform on the web. With more and more users, security has become a real challenge. I’d like to be clear; WordPress core is one of the most secure software packages out there. Most of the security issues with WordPress either come from bad written plugins, not updating, not setting strong passwords or bad hosting providers.

As a plugin developer, there’s nothing much you can do about the host or the password a user chooses, but you can do a lot about your plugins to prevent a hack. If you’ll follow these five commandments, you’ll keep a lot of people a lot happier. Plus; you won’t have to deal with support requests about hacked WordPress installs! That’s a win-win in my book!

1. Thou shalt prepare() every query

SQL Injection is still one of the biggest security problems in any web application. It basically means that your input-fields are being hijacked to run queries in your database. This could go from stealing user-data to erasing the entire database. When this happens in a WordPress install, the cause of it is usually lack of data validation. Without cleaning your queries, who knows what kind of nastiness an attacker is able to achieve?

All of WordPress’ database functions use the $wpdb class. Most of the methods in this class (and all of the core WordPress methods like get_posts(), for that matter) will use $wpdb’s prepare() function to validate there queries and make sure that nothing bad can come through. So if you’re using default WordPress methods, you’re probably secure. If you’re using $wpdb->query() directly (or even worse; php’s mysql functions) though; you’re probably not secure.

This is when we use prepare(). The prepare function in the $wpdb class filters out any MySQL reference and cleans up the string that you’re passing on to your database. If you’re already familiar with preventing SQL injections in PHP, you are probably aware that there’s a whole array of methods available already, so why use prepare()? Well, prepare gets updated when WordPress gets updated. The WordPress core team has become very good at flushing out the latest tricks hackers might try… trust WordPress core and your plugin will be fine.

2. Thou shalt use nonces on forms and urls

Nonce stands for ‘number used only once’. It’s used against unexpected or duplicate requests that could cause undesired permanent or irreversible changes to a WordPress install and particularly to it’s database. It basically checks the intention of the user.

We can use nonces in forms and urls. A nonce is first tied to a variable or a hidden input-field. When we handle the request we validate the nonce before performing any task. A nonce in WordPress is generated by a simple string. Usually this is the name of the task you are performing. If you want to make it more secure you can think about adding the current date to the string or changing the string to an md5 hash.

Nonces in forms can be generated with the following core WordPress function:


wp_nonce_field( md5( 'my_plugin_nonce' );

In links this is a bit more complicated; we need to wrap the url in that function as well:


$url = 'https://wpmayor.com';
$wp_nonce_url( $url, md5( 'my_plugin_nonce' ) );

Both nonces can be validated with the simple


wp_verify_nonce( md5( 'my_plugin_nonce' ) );

The wp_verify_nonce() function returns a simple TRUE / FALSE statement, so you can use it directly in a condition.

3. Thou shalt check user roles

First of all; if all the users in your WordPress install are administrators, you’re probably doing something wrong. Clients can come a long way with being ‘just’ editor. If your client is using a weak password and it gets cracked, at least the attacker won’t have access to plugins, other user-data or the theme editor (another tip; disable the theme editor in any case, chance is you’re not using it.).

User roles are an important part of any web application. Say you’ve created a photo album plugin; then i’m betting you wouldn’t want subscribers adding or deleting photo’s to that album. You’d probably only want editors and administrators to be able to do that… Well, you can do that very easily in WordPress by setting a capability to a role and then checking if the current user has that capability with the brilliantly named function current_user_can( ‘capability-name’ ), which returns true or false.



    add_action( 'init', 'my_photo_album_init' );

    function my_photo_album_init() {
        global $wp_roles;
        $wp_roles->add_cap( 'edit_my_photo_album', 'editor' ); 
    }

    //a simple delete function:
    function my_photo_album_delete_pic( $id ){

        if( !current_user_can( 'edit_my_photo_album' ) 
            return false

        // delete the picture...
    }

Long story short; user roles are there for a reason and WordPress makes it very easy to create new roles and verify request for certain user roles; use ’em.

4. Thou shalt escape() every output

Escaping is used to make sure our database output doesn’t break the site. It ensures that strings are valid and that they will not mess up the html of your document. WordPress offers us some nice functions for this;

esc_url()
Encodes and validates a url.

esc_html()
Makes sure a string doesn’t break your html document with extra brackets and (double)-quotes

esc_attr()
Almost the same as esc_html(), but used for escaping attributes of html-tags.

You can find all of these functions in the WordPress Codex
Escaping is a very simple thing to do. Ideally you do it right before output. So

<h2 class="<?php echo $class;?>">
   <a href="<?php echo $url;?>">
      <?php echo $title;?>
   </a>
</h2>

should become

<h2 class="<?php echo esc_attr( $class );?>">
   <a href="<?php echo esc_url( $url );?>">
      <?php echo esc_html( $title );?>
   </a>
</h2>

5. Thou shalt set WP_DEBUG to true and check for errors

This might be a no-brainer for some people, but errors are bad. Even if they don’t break your site. With WP_DEBUG in your wp-config.php file set to “true” you’ll get to see all errors your plugin is causing. This will first make you a better programmer; WordPress and PHP give you constant feedback about how you can do things better. Next to that; you’ll be able to see if a function is deprecated. WordPress tries to be as backwards compatible as possible, so a deprecated function will usually just give out a warning that it’s deprecated and that you should use something else but it won’t fire a site-breaking error. This is a good thing for updating purposes because WordPress usually won’t break when updating, but it’s still VERY smart to listen to WordPress and use the newer alternatives WordPress offers.

Another thing to consider is when your plugin is throwing errors; there will still be sites in production running your plugin with WP_Debug switched on. Which means people are going to see these errors. If an attacker gets to see them, you’ve conveniently given him the document root, which is never a good thing.

Next to not using deprecated functions and preventing the document root being spelled out for everybody it’s just plain ugly to have a plugin that’s causing warnings and errors… Don’t be that guy.

Bonus: Thou shalt prefix every function and use classes

WordPress plugins are actually pretty creepy little buggers; they’re allowed to do literally anything in and to the system. They are also loaded in completely from the start. Instead of theme files, which are loaded in contextually based on the url and the post that’s being queried, plugin files are loaded in right away.
And there are a lot of ’em. I. MEAN. LOADS.

With 26800 plugins and counting in the public repository alone it’s kind of hard to make sure everything keeps working for everybody. Especially since you don’t know which of the other 26799 plugins are being loaded. An overlapping functionname is bound to happen somewhere. And then it will generate a fatal error giving the user (who propably hasn’t got WP_DEBUG set to ‘true’) the nice little task of figuring out why his site is returning a blank document.

You can easily prevent this by prefixing all of your functions or nesting ’em in well-named classes. You can also use function_exists() everywhere, but frankly; who has the time to write that conditional every time you start a function?

Using these tips will not prevent everything, but it will prevent most of the WordPress-hack-madness. The general thing to remember here is; WordPress core is secure. If you’re ever in doubt; check the WordPress codex. Trusting in the platform is the most important part of being a WordPress (plugin) developer.

If you enjoyed this post, make sure to subscribe to WP Mayor’s RSS feed.

Luc Princen

Luc Princen is a developer and designer from The Netherlands. He’s co-founder and technical lead at Chef du Web where he specializes in pushing WordPress to the limit.

Discover more from our archives ↓

Popular articles ↓

2 Responses

  1. You’re right. If there are no arguments, prepare() is pretty much pointless. But saying it’s pointless in situation X might make some developers forget to use it in situations where it does apply. Like you said; it can’t really hurt.

  2. There is any reason for using $wpdb-prepare() when the query has no arguments to be passed?

    I’ve seen this often in plugins.
    Besides the fact that $wpdb-prepare() expects a second argument - it won’t break the code not passing it, but code inspectors see these as potential error -, generally speaking, I think that there is no point of using it, but I might be missing something.

Share Your Thoughts

Your email address will not be published. Required fields are marked *

Claim Your Free Website Tip 👇

Leave your name, email and website URL below to receive one actionable improvement tip tailored for your website within the next 24 hours.

"They identified areas for improvement that we had not previously considered." - Elliot

By providing your information, you'll also be subscribing to our weekly newsletter packed with exclusive content and insights. You can unsubscribe at any time with just one click.