Security is a very important topic. To secure WordPress, you must have responsible users making use of an instance of WordPress that is only executing secure code (maybe helped by some extra “hardening”) on a secured server. But a compromise of any part of that can invalidate on all your work on any other part. There is no single solution to having a secure WordPress site.
What we’re going to focus on in this article is what you, as a developer, should be thinking about when writing PHP for WordPress if you want to avoid the obvious errors and security blunders. We will not be talking about securing servers (for 90% of WordPress sites, the host takes care of that) or about using WordPress securely. (In short: use good passwords, keep updated.)
This is also not an in-depth exploration of these topics. If you already recognize the top three threats: SQL injection, XSS, CSRF, you may not get a lot from this article. But if you don’t recognize them, we’ll give a thorough explanation of each—what they mean, what they are, and how to prevent them—and a few more common mistakes. Let’s get to it!
Understand the Threats
Talking about security without discussing the threat model—who and what are we worried about?—is kind of silly.
In this article, we’re assuming that you’re writing a WordPress plugin or theme and you want to make sure it is secure. To do so, you need to have a good idea of where your threats will come from. Talking about security without discussing the threat model—who and what are we worried about?—is kind of silly.
So, what are we worried about? As a developer, our enemy is generally a malicious user. A user can be malicious for a lot of reasons. So we’re mostly worrying, as WordPress developers, about people who are aiming to compromise a site who are not the primary administrators of the site. That is, we’re not really protecting from someone with an “Administrator” account on your WordPress site blowing off some steam by deleting all the themes and plugins. Protecting against that in the code you develop is both very hard, and pretty pointless. That’s a losing battle.
Instead, our focus should be on writing code that doesn’t do things people don’t expect when someone tries to trick them into doing things they don’t want to do. And we should also prevent them from accidentally doing bad things they don’t intend when they make use of edges of our software we’ve not thought they’d use that way.
SQL Injection, the Ultimate in Database Exploits
In most web applications, and WordPress is no exception, the database is the heart of where data is stored, and thus the heart of what you need to protect. It is also where some of the most damaging and common mistakes are made by new developers. A developer in a hurry or unfamiliar with the risks of SQL injection attacks is very likely to make some costly mistakes when writing database code.
For the unfamiliar, SQL stands for “structured query language”. Every form of relational database—WordPress uses one called MySQL—speaks a variant of SQL. And because of this common language, it’s pretty easy for person to “inject” bad SQL code into a database’s operation if the author of a piece of software that talks to the database isn’t careful.
Understanding an SQL Injection Attack
A very simple SQL injection attack is illustrated by the famous “Bobby Tables” comic. The basic premise is that if you as a developer has made it possible for an arbitrary form field or URL string to be put into the database without adequate protective measures, you’re in for a bad time. The joke is the that the mother who believes in software quality has tested all the databases of her son’s school by naming him in such a way that simply putting his name into the system would remove their entire list of students. She has used an SQL injection vector to drop (delete, if you don’t speak SQL) a whole table from the school’s records.
An attack as simplistic as portrayed in XKCD isn’t as easy to do in WordPress. But other ways to compromise your database are. As a proof of concept of how easily you can write vulnerable database code in WordPress, I came up with this:
add_shortcode('hack_my_db', 'hack_my_db');
function hack_my_db() {
$id = $_GET['id'];
global $wpdb;
$post_title = $wpdb->get_var('SELECT post_title from wp_posts WHERE ID='.$id);
return $post_title;
}
What does this code—which you should only run on a local test server—do? It takes an arbitrary query string (like the part after the question mark in a URL like http://wordpress.org?id=123) and does a database search with it. This would be akin to something like the order page—created via a shortcode—in an ecommerce plugin. The specific code I’ve written doesn’t require $wpdb
, but comparable code might.
And because this code is intentionally vulnerable, we’re not using a wpdb->prepare
to make the query. Using that method is one of the easy things that would have made this code safe. Because it doesn’t prepare
this code leaves the door open from someone to launch an SQL injection attack against the site.
In fact, I recorded a bonus video about exploiting this very SQL injection vulnerability using a free-and-easy tool. The “Bobby Tables” attack didn’t work for me, but I was able to exploit this code to find the usernames and passwords of all the users on the site. We need your email address to get that to you:
How to stay safe from an SQL Injection Attack
The simple thing to do is use
prepare
and tell the database the structure of your query and that you’re going to replace a part of it with a integer.
The simple thing to do is use prepare
and tell the database the structure of your query and that you’re going to replace a part of it with a integer. If you did, the DROP TABLE ___
wouldn’t run, ever, nor would a more complex attack like the one I used. In response to the comic, someone actually created bobby-tables.com, and explains prepare statements (including a WordPress example) in greater detail.
In the case of this specific code, I would also be more protected by the use of WP_Query
, which has it’s own extra layer of protection from exactly this type of issue. Because the specific database data I was getting could be gotten that way, it’d be easier and wiser. But not every use of $wpdb
can be routed around. So it’s important to know about prepare
regardless.
You can also protect against an SQL injection attack in this case with rather simple sanitization. We can make sure the value is of a type we expect, in this case an integer, by forcing it to be. In this case, we know that we want the value of $id
to be an integer. So we could simply use the PHP function intval
to make sure it is. This would prevent an SQL injection attack like I was able to carry out in video to steal credentials from a WordPress site.
With more complicated parameters to database queries, more complex sanitization may be necessary. You can’t convert an email address into a number before you put it in a database. That’s where you’ll reach for some of WordPress’s built in functions for sanitization, like sanitize_email
. Validation could also be done with something like like is_email
. It’s worth saying, users should and do prefer validation—tell them their input is wrong, don’t just change it before acting with it. But either can work to keep your database safe from a SQL injection attack.
Cross-Site Scripting (XSS)
Cross-site scripting is a class of attacks most often characterized by letting a vulnerable input field be used to write JavaScript you don’t expect to run on your site to be executed there. Though potentially you could also have executable PHP run this way, if you weren’t careful.
It is cross-site scripting that makes it necessary to escape all output
This is especially problematic because JavaScript served from the same domain as the server is likely to have more privilege than that from elsewhere. It is cross-site scripting that makes it necessary to escape all output you don’t know to be safe from poorly or maliciously written JavaScript.
It’s not impossible, or even that difficult for you to thoughtlessly fail to sanitize the data from a textarea
in a form you create, and then fail to escape it when you display it to a site’s visitors. This is the most common way that you let people attack you by triggering unexpected JavaScript that either steals data or simply annoys your visitors with unwanted alert()
s.
How to stay safe from XSS
As we suggested above, there are two lines of defense against a cross-site scripting attack. The first is to validate and/or sanitize all data you get from users before you show it back to them or save it. This is, again, a place where you might want to use the is_email
function. But really long and complex text fields will require you to reach for, for example, PHP’s strip_tags
function. If you expect that a user is giving you a large amount of plain text, you can just run it through strip_tags
and script tags and HTML markup will be removed.
On the other side, you also can prevent an XSS attack via escaping output. If a user has submitted HTML, and you need to show that HTML, you can just make sure that you don’t accidentally allow script
tags from executing by escaping the HTML. There are lots of different escaping functions in WordPress. Some of the most used and important are:
esc_html
is the WordPress function to make sure that the text you’re outputting contains no HTML. It’ll change all your angle brackets, etc into the correct HTML entities.wp_kses(_post)
is a less-used function that stops specific (classes) of HTML elements from being used. You explain what you’ll accept, and the function strips other kinds of HTML markup.esc_attr
is for things that’ll end up in HTML attribute tags. Essentially, it makes sure to prevent things from breaking out the quotes you put them in by accident or malice.
Cross-Site Request Forgery Attack
The most common defense against this is to use what is called a “nonce”—a number used once
A Cross-Site Request Forgery (CSRF) attack is the most esoteric of the three major vulnerabilities. It relies on the idea that an authorized user sent to a certain page or URL can possibly do things that they don’t realize they’ve done. When paired with a XSS attack, an example would be a “delete” action being performed by a user who simply thought they’d hit, say, a “Search” button.
The most common defense against this is to use what is called a “nonce”—a number used once—to make sure that the “delete” page doesn’t accidentally accept a “search” form request redirected to it. Each form will submit a nonce identifying that it came from a specific form, and the exploit code can’t know the nonce for the target form. So when you check the “nonce” for the user who submits your form, you protect them from accidentally doing something other than what they thought they were.
The details of using a nonce in WordPress are a little beyond the scope of this article. And we lack content explaining them on WPShout. This CSS-Tricks article can stand in until we have one for you. The short version is that you’ll add a nonce
to your forms in WordPress with a call to the wp_nonce_field()
function, and then verify that same nonce before you perform your form’s action with a call to wp_verify_nonce
. Skipping either of these steps makes the whole thing not work, as you may guess. There are other ways to do nonces, and they get a bit more complicated with AJAX requests, but that’s the basic idea of them: don’t let someone do an action unless you know they meant to do specifically that action.
Other Security Issues to Keep in Minds
The three topic headers above are the most common and dangerous vulnerabilities an unwitting developer may make available, but the aren’t the only things that might go wrong. A few others that you’ll want to keep in mind are capability issues in your code, code path errors, and exploitable redirects.
Capability Checks
This isn’t a mistake in the sense of the top three, because it requires no “attacker”.
One of the most common issues in WordPress code that seems simple is to forget to make sure that the user who’s on a page submitting a form has the privilege’s necessary to perform this action.
This isn’t a mistake in the sense of the top three, because it requires no “attacker”. Even a naive user acting alone could trigger this issue. But it’s still a very common source of security vulnerabilities. If you only want “Administrators” to delete certain data on the site, make sure that you check the user submitting the form to delete data on your site is an “Administrator.” This isn’t conceptually hard, but its very easy to forget when you’re in a hurry. current_user_can
is often the best way to implement this, as it checks user “capability” rather than roles.
Not Exiting on Guard Clauses when You Should
Like the capabilities issue listed above, failing to make sure everything stops in your code when you meant it to can cause very easy security issues, but isn’t really an attack. It’s simply about making sure that you always return
or die
in the places you mean to.
This kind of issue is a little harder to spot, because you might think that your guard clause has exited but it didn’t. Logic errors are small but can lead to important security issues. Apple’s #gotofail bug is the quintessential example.
Exploitable Redirects
The last issue to consider is that you may allow something like a query string to control which page a user is forwarded to after an action. If you do this, make sure that it’s a URL you are happy with. For exactly this reason WordPress has a function called wp_safe_redirect
. It may seem like overkill, but a clever linking to a URL that forwards in a way a user or a developer don’t expect on your WordPress site could be a potent attack vector in the hands of a clever attacker.
A Optimistic Dose of Reality
The reality is, most code I’ve written in my career as a freelance WordPress developer has run on one site. And while I’ve been working to be a better WordPress developer for years, I’m sure some code I’ve written which still runs somewhere has had an SQL injection vulnerability, cross-site scripting, or similar issues.
Even big important plugins, with millions of installs and a strong need to stay secure, do make mistakes.
But, to my knowledge, no one’s site has ever been taken over by my mistakes. Perhaps I’m tempting fate by saying this, but when code is deployed on one (small-time) site, you’re kind of protected by the small scale. Security is important, and all good WordPress developers should keep it in mind, but it’s not the case that you need to be terrified if you’ve made these mistakes in the past. Even big important plugins, with millions of installs and a strong need to stay secure, do make mistakes. And they’re often discovered before they’re heavily exploited. (This is the reason, as a WordPress administrator, you should update plugins regularly.)
As best you can, the obvious path forward is it simply not make these mistakes any more. And that does require learning. I hope this article has clarified how you think about WordPress security, but I don’t think that it’s the final answer to these questions, more learning will be necessary.
But Secure Code Still Matters. A lot.
While it’s probably the case that your first deployed insecure code will not be bad for you or your users, it does just takes one mistake, exploited opportunely, for you to be in a world a self-doubt and regret. I know for sure I wouldn’t enjoy knowledge that I broke a WooCommerce store and lost them hours (if not years) worth of data by letting an unauthorized attacker take over the site via an SQL vulnerability or CSRF attack. That’s the reason that you must always be careful and thoughtful when you write code.
Security as an afterthought is both common and problematic. Your code will never be made totally safe by advancements to WordPress core, WordPress hosting, or WordPress security plugins. The simplest defense is to be aggressive and careful in the code you write. There’s simply no substitute for that. Happy hacking!
Indeed a great list of common WordPress security mistakes. A couple of days back I faced a situation where there was some unwanted ads being displayed on my blog and that was something I did not install. When inspected I found that there was a lot of unwanted codes that were injected into the WordPress theme files and other main files. On further inspection I found out the following 3 things which were the reasons for this: 1). Not updating the other WordPress installation, plugins and themes that are being run from the same hosting account if you are using a shared hosting . 2). Optimizepress 1.0 is known to have a security issue and they have released an update to it. This doesn’t update in the normal updates from your wordpress dashboard. You might want to update it manually, if you haven’t done it yet. 3). Not Cleaning and optimizing your database periodically 4). Leaving the default themes like twentyeleven etc. as it is and not updating them. This primarily happens if you are using a different theme and these default themes just remain there. 5). Not uninstalling plugins that haven’t been updated for a long time by its… Read more »
[…] Principles of Secure WordPress Code Writing secure code is an essential part of keeping your WordPress website’s data safe. […]