How to keep your WordPress website secure?
If you are developing WordPress plugins (or themes) for distribution via WordPress.org, or for client projects, it should be a no-brainer that writing insecure code can lead to severe consequences.
Having your plugin pulled from the repository, seeing a loss in respect and end-user confidence, or even worse, seeing users fall victim to easily preventable attacks are all possibilities if plugin security is not taken seriously.
With this in mind you would think WP developers would take extra care to avoid common exploits, however the research seems to indicate that insecure code is still rife in plugins and themes.
Taking the whole WordPress environment into consideration (plugins, theme, host, end-user), of the 170,000 sites hacked in 2012 this infographic suggests that around 22% were due to plugin vulnerabilities and 29% themes.
A second infographic puts forward the perfect storm scenario in which a combination of plugins, themes and legacy hosting providers are all to blame for hacks.
Most shockingly back in June, Checkmarx, an application security company, published a study analysing the most popular WordPress plugins in the official WordPress plugin repository for security issues and found that:
20% of the 50 most popular WordPress plugins are vulnerable to common Web attacks. This amounts to nearly 8 million downloads of vulnerable plugins.
One-fifth… and thats not including the less popular plugins.
Lets also not forget that It’s not just newbies to the platform at fault; even experienced developers sometimes make mistakes, heck, even I’ll admit to letting issues slip through the net on occasion. And even though this doesn’t mean all plugins are insecure nor receive patches (plugins get security patches all the time remember) it still highlights the fact that as developers we could do much better when it comes to security.
So what can we do? There are several easy to implement rules you can follow to make your code more secure which I’ll briefly explain in this post.
1. Always develop with debugging ON
Deprecated notices, warnings, fatal errors – you don’t want these to be triggered by your plugin. If a server has error reporting turned on, these errors can reveal server information such as file paths with which an attacker can better plan attacks on your server.
Turning on debug mode will cause these errors to be visible allowing you to patch them accordingly. Add the following into your wp-config.php file to enable debugging:
define( WP_DEBUG, true );
In a production environment you’ll want these hidden to end-users but you may want them logged so you can at least track them behind the scenes. In this case you can also add:
define( 'WP_DEBUG_LOG', true ); define( 'WP_DEBUG_DISPLAY', false );
This will log them inside the wp-content/debug.log file.
2. Prevent direct access to your files
Most hosts allow direct access to files on your server, including those belonging to your plugin. Directly accessing plugin files will in most cases cause PHP errors which, like rule #1, will also lead to disclosure of your WordPress install path.
To avoid these errors you can add a simple ABSPATH
check which terminates the script if accessed outside of WordPress.
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
3. Sanitize all the things
Never trust user input, even that provided by admin users, because:
- you have no way of knowing the user is whom they say they are
- other scripts can manipulate posted data.
If you don’t do this you can be vulnerable to XSS attacks in which an attacker injects malicious code into your inputs and posts it to your server. You must sanitize all data collected from $_GET
, $_POST
and $_REQUEST
to avoid this.
A favourite function of mine is sanitize_text_field
. Not only does this function remove and encode invalid, possibly dangerous code, it also removes white space, tags and line breaks with minimum effort on your part.
There are several other more specific sanitization functions you can make use of too such as sanitize_title
and sanitize_email
. See the full list here.
In addition to sanitization you should also validate input i.e. are the values you receive from a user what you were expecting.
For example, if you have an email field in a form, after sanitizing the value, validate thats it actually is an email address using the is_email()
function, otherwise reject the request.
4. Escape when you output
When you output any data from your database or from user input its always wise to escape it so that you are sure the output data is clean and valid – like input sanitization, this also helps prevent XSS vulnerabilities. WordPress provides several functions to make this a doddle, including:
wp_kses
andwp_kses_post
which strip out untrusted HTML.esc_js
which escapes and correctly encodes characters in text strings which you intend to echo for JavaScript.esc_textarea
which encodes text for use inside textarea elements.esc_url
which correctly encodes URLs and rejects invalid URLs.esc_html
which encodes< > & " '
when outputting HTML.esc_attr
which encodes text likeesc_html
for use in HTML attributes.
For example, if your plugin output some HTML like this:
You would replace this with:
5. Nonce your forms and urls
Cross-site request forgeries (CSRF) are where an attacker can trick a user into performing actions against their will.
For example, say you have a link to delete a post from the database. I could (with malicious intent) trick you into clicking a link which deletes your posts without your consent.
Similarly, if you accidentally visited a delete URL a second time you could delete posts by accident. This is where nonces can help.
A nonce (Number used ONCE) is a unique token generated using an action name and a timestamp which you can use to verify a request. Nonces can be added to both forms (using wp_nonce_field()
) and urls (using wp_nonce_url()
).
As an example, take our delete link and nonce it:
Now when this is processed we can verify the nonce is valid using wp_verify_nonce
:
if ( ! wp_verify_nonce( $_GET['_wpnonce'], 'delete_link' ) ) die( 'Security check' );
6. $wpdb is your friend when it comes to database queries
If you are doing anything with the database, never interact with it directly – use $wpdb
. $wpdb
is WordPress’ database abstraction class and using it will help reduce the risk of SQL injection attacks.
$wpdb
provides and insert()
and update()
method which you can use to safely insert and edit data in the database – it does the escaping for you.
If you are doing a query directly using the $wpdb->query()
method, you can ensure your query is safe by using $wpdb->prepare()
which escapes your variables. Example:
$wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->posts WHERE ID = %d;", $post_id ) );
Aside from the benefit to security, $wpdb
makes your job much easier, so use it.
7. Avoid CURL when posting remotely
This is a common bugbear of mine. Using CURL
directly should, and can easily, be avoided by using WordPress’ WP_HTTP
class and wrapper functions, wp_remote_get
and wp_remote_post
being the most obvious.
Not only do these functions take care of encoding data you are posting, they also offer fallbacks for when CURL
is not available.
8. Prevent unauthorized access
If functionality inside your plugin gives access or allows modification of sensitive information, you need to prevent unauthorized users from breaking things.
To check if a user can perform an action you can use the handy current_user_can()
function which returns whether or not a user has access to a given capability.
9. Use the tools available to you and keep things lean
WordPress comes bundled with a bunch of php and javascript libraries which you are free to use. As a general rule of thumb, don’t bundle something if you can use core to do your bidding.
Take TimThumb for example, a php based image resizing library. This was used by many plugins and themes to resize images on-the-fly. This was then found to include a fairly substantial vulnerability. As a result the plugins and themes bundling it were also affected.
These plugins could have instead used WordPress’ native image resizing functionality (add_image_size()
) which would have saved bags of hurt. Moral of this story, use native WP functions where possible and keep 3rd party code (at least code you don’t fully understand) to a minimum.
Full article at mikejolley.com