Securing wordpress

File Permissions

Some neat features of WordPress come from allowing various files to be writable by the web server. However, allowing write access to your files is potentially dangerous, particularly in a shared hosting environment.

It is best to lock down your file permissions as much as possible and to loosen those restrictions on the occasions that you need to allow write access, or to create specific folders with less restrictions for the purpose of doing things like uploading files.

Here is one possible permission scheme.

All files should be owned by your user account, and should be writable by you. Any file that needs write access from WordPress should be group-owned by the user account used by the web server.

The root WordPress directory: all files should be writable only by your user account, except .htaccessif you want WordPress to automatically generate rewrite rules for you.
The WordPress administration area: all files should be writable only by your user account.
The bulk of WordPress application logic: all files should be writable only by your user account.
User-supplied content: intended to be completely writable by all users (owner/user, group, and public).

Within /wp-content/you will find:

Theme files. If you want to use the built-in theme editor, all files need to be group writable. If you do not want to use the built-in theme editor, all files can be writable only by your user account.
Plugin files: all files should be writable only by your user account.

Other directories that may be present with /wp-content/should be documented by whichever plugin or theme requires them. Permissions may vary.

Changing file permissions

If you have shell access to your server, you can change file permissions recursively with the following command:

For Directories:

find /path/to/your/wordpress/install/ -type d -exec chmod 755 {} \;

For Files:

find /path/to/your/wordpress/install/ -type f -exec chmod 644 {} \;

Regarding Automatic Updates

When you tell WordPress to perform an automatic update, all file operations are performed as the user that owns the files, not as the web server's user. All files are set to 0644 and all directories are set to 0755, and writable by only the user and readable by everyone else, including the web server.

Database Security

If you run multiple blogs on the same server, it is wise to consider keeping them in separate databases each managed by a different user. This is best accomplished when performing the initial WordPress installation. This is a containment strategy: if an intruder successfully cracks one WordPress installation, this makes it that much harder to alter your other blogs.

If you administer MySQL yourself, ensure that you understand your MySQL configuration and that unneeded features (such as accepting remote TCP connections) are disabled. See Secure MySQL Database Design for a nice introduction.

Securing wp-admin

Adding server-side password protection (such as BasicAuth) to /wp-admin/adds a second layer of protection around your blog's admin area, the login screen, and your files. This forces an attacker or bot to attack this second layer of protection instead of your actual admin files. Many WordPress attacks are carried out autonomously by malicious software bots.

Simply securing the wp-admin/directory might also break some WordPress functionality, such as the AJAX handler at wp-admin/admin-ajax.php. See the Resources section for more documentation on how to password protect your wp-admin/directory properly.

The most common attacks against a WordPress blog usually fall into two categories.

  1. Sending specially-crafted HTTP requests to your server with specific exploit payloads for specific vulnerabilities. These include old/outdated plugins and software.
  2. Attempting to gain access to your blog by using "brute-force" password guessing.

The ultimate implementation of this "second layer" password protection is to require an HTTPS SSL encrypted connection for administration, so that all communication and sensitive data is encrypted. See Administration Over SSL.

Securing wp-includes

A second layer of protection can be added where scripts are generally not intended to be accessed by any user. One way to do that is to block those scripts using mod_rewrite in the .htaccess file. Note: to ensure the code below is not overwritten by WordPress, place it outside the # BEGIN WordPress and # END WordPress tags in the .htaccess file. WordPress can overwrite anything between these tags.

# Block the include-only files.
RewriteEngine On
RewriteBase /
RewriteRule ^wp-admin/includes/ - [F,L]
RewriteRule !^wp-includes/ - [S=3]
RewriteRule ^wp-includes/[^/]+\.php$ - [F,L]
RewriteRule ^wp-includes/js/tinymce/langs/.+\.php - [F,L]
RewriteRule ^wp-includes/theme-compat/ - [F,L]

# BEGIN WordPress

Note that this won't work well on Multisite, as RewriteRule ^wp-includes/[^/]+\.php$ - [F,L] would prevent the ms-files.php file from generating images. Omitting that line will allow the code to work, but offers less security.

Securing wp-config.php

You can move the wp-config.php file to the directory above your WordPress install. This means for a site installed in the root of your webspace, you can store wp-config.php outside the web-root folder.

Note: Some people assert that moving wp-config.php has minimal security benefits and, if not done carefully, may actually introduce serious vulnerabilities. Others disagree.

Note that wp-config.php can be stored ONE directory level above the WordPress (where wp-includes resides) installation. Also, make sure that only you (and the web server) can read this file (it generally means a 400 or 440 permission).

If you use a server with .htaccess, you can put this in that file (at the very top) to deny access to anyone surfing for it:

order allow,deny
deny from all

Disable File Editing

The Wordpress Dashboard by default allows administrators to edit PHP files, such as plugin and theme files. This is often the first tool an attacker will use if able to login, since it allows code execution. Wordpress has a constant to disable editing from Dashboard. Placing this line in wp-config.php is equivalent to removing the 'edit_themes', 'edit_plugins' and 'edit_files' capabilities of all users:

define('DISALLOW_FILE_EDIT', true);

This will not prevent an attacker from uploading malicious files to your site, but might stop some attacks.


First of all, make sure your plugins are always updated. Also, if you are not using a specific plugin, delete it from the system.

Firewall Plugins

There are a few plugins that purport to screen out suspicious-looking requests based on rule databases and/or whitelists. BlogSecurity's WPIDS plugin installs PHPIDS, a generic security layer for PHP applications, while WordPress Firewall uses some WordPress-tuned pre-configured rules along with a whitelist to screen out attacks without much configuration.

Plugins that need write access

If a plugin wants write access to your WordPress files and directories, please read the code to make sure it is legit or check with someone you trust. Possible places to check are the Support Forums and IRC Channel.

Code execution plugins

As we said, part of the goal of hardening WordPress is containing the damage done if there is a successful attack. Plugins which allow arbitrary PHP or other code to execute from entries in a database effectively magnify the possibility of damage in the event of a successful attack.

A way to avoid using such a plugin is to use custom page templates that call the function. Part of the security this affords is active only when you disallow file editing within WordPress.

Security through obscurity

Security through obscurity is generally an unsound primary strategy. However, there are areas in WordPress where obscuring information might help with security:

  1. Rename the administrative account: On a new install you can simply create a new Administrative account and delete the default adminaccount. On an existing WordPress install you may rename the existing account in the MySQL command-line client with a command like UPDATE wp_users SET user_login = 'newuser' WHERE user_login = 'admin';, or by using a MySQL frontend like phpMyAdmin.
  2. Change the table_prefix: Many published WordPress-specific SQL-injection attacks make the assumption that the table_prefix is wp_, the default. Changing this can block at least some SQL injection attacks.

Data Backups

Back up your data regularly, including your MySQL databases. See the main article: Backing Up Your Database.

Data integrity is critical for trusted backups. Encrypting the backup, keeping an independent record of MD5 hashes for each backup file, and/or placing backups on read-only media increases your confidence that your data has not been tampered with.

A sound backup strategy could include keeping a set of regularly-timed snapshots of your entire WordPress installation (including WordPress core files and your database) in a trusted location. Imagine a site that makes weekly snapshots. Such a strategy means that if a site is compromised on May 1st but the compromise is not detected until May 12th, the site owner will have pre-compromise backups that can help in rebuilding the site and possibly even post-compromise backups which will aid in determining how the site was compromised.


It is possible to log various requests sent to WordPress. Standard Apache logs do not offer much help with dealing with security forensics. See Mod_Security - Logs and Prevents using Apache.


Sometimes prevention is not enough and you may still be hacked. That's why intrusion detection/monitoring is very important. It will allow you to react faster, find out what happened and recover your site.

Monitoring your logs

If you are on a private server (where you have admin access), you have to watch your logs to detect password guessing attempts, web attacks, etc. A good open source solution to monitor your logs in real time and block the attacker is OSSEC.

Monitoring your files for changes

When an attack happens, it always leave traces. Either on the logs or on the file system (new files, modified files, etc). If you are using OSSEC for example, it will monitor your files and alert you when they change.

Monitoring your web server externally

If the attacker tries to deface your site or add malware, you can also detect these changes by using a web-based integrity monitor solution.


URL-Based Exploits

With URL-based exploits, hackers try to find weak spots on your website by making requests that would normally return an error but for some reason are completed.

The above hypothetical URL is essentially a stab in the dark by a hacker. But if the request is met, even though the URL is clearly not meant to go anywhere, the hacker might be able to make use of it.

Using .htaccess as a Firewall

One of the best methods I’ve found against this kind of threat is an .htaccess firewall. It basically consists of rules that automatically block requests based on strings in the URL.

For example, there is no good reason for an opening bracket ([) to be in a URL. If a request is made using a URL that contains a bracket, then either the user has mistyped something or someone is looking for a security hole. Either way, generating a “403 Forbidden” page is good practice in this case.

RedirectMatch 403 \[

Paste the line above in your .htaccess file to block any request that contains an opening bracket.

To guard against more than just brackets, you will need a more complex ruleset. Luckily, our awesome editor Jeff Starr has gone out of his way to create a great .htaccess ruleset. The latest iteration is called 5G Firewall and is freely available from Perishable Press for your copy-and-pasting pleasure.

The firewall is modular, so you can delete lines from it without breaking the functionality. If something goes wrong when you’re using it, you can usually track down the problem by deleting lines until it starts working again. Once you’ve found the offending line, you can delete it and paste back the rest.

Protecting Directories

On many servers, it is possible to view the contents of a directory simply by typing it in the URL bar.

Visiting this typical URL for a WordPress blog will list the contents of that directory, showing all of the uploads from August 2011. You might want this, but you can also disable or fine tune it using the good ol’ .htaccess file.

Options -Indexes

Popping the above line into your .htaccess file will disable directory listings; so, users will get a “403 Forbidden” message if they try to view a directory. While many people seem to be aware of this, far fewer know of the other options besides allowing or disallowing access. You can control which file types are listed using the IndexIgnoredirective. Take these three examples:

IndexIgnore *
IndexIgnore *.php 
indexIgnore *.jpg *.gif *.png

If directory listing is enabled, then the directory will be displayed in the first example, but no files will be listed because all will be ignored. The second example will list all files except ones with a .php extension. The third example will omit the three image types specified.

Note that some hosts (such as MediaTemple) disable directory browsing by default, so you won’t need to modify the .htaccess file. To verify this, just type a directory location in the URL bar and see what happens.

Additional Server-Level Protection

So far, the measures we have taken have nothing to do with our website’s actual code. However secure your code is, you will still need to implement something like what we did above. We don’t have time to look at all tips and tricks for .htaccess, but you can do quite a few other things:

  • Password-protect directories,
  • Use smart redirects,
  • Deny access based on IP or an IP range,
  • Force downloading of files,
  • Disable hotlinking,
  • The list goes on.

Look at the “Further Reading” section at the end of this article, and become good friends with your .htaccess file. It might seem daunting and confusing at first, but solid knowledge of how to use it will go a long way.

Protecting Against Malicious Users

The second type of problem that can arise is when someone performs an action that they are not supposed to do. This doesn’t necessarily mean that they intended to harm the website, but it could happen.

If users are listed somewhere in the admin part of your website, chances are that a link is displayed to delete each user. The link could point to the script in the following location:

This link is relatively obscure; that is, a normal user doesn’t have a good chance of stumbling on it. But if directory listings are enabled, then someone naughty could go to, see that you have a delete_user.php file there, and make various requests to try to delete a user.

If the script does not check permission or intent, then anyone who visits the link above could delete user 5.

Authority and Intent

Whenever a user initiates an action, we need to take two things into consideration. Does the user have authority to perform the action (i.e. do they have permission)? If the user does have authority, do they also intend to complete the action (i.e. do they mean to do what they’re doing)?

WordPress has functions to help you make sure that both conditions are met before an action in the script is triggered. We will look at these in detail shortly. If you are building your website from scratch, then you will need to make sure that each user has associated permissions and that you know which action can be performed under which condition.

For example, you would probably want only administrators to be able to delete content from the website. Every time a user tries to delete content, you would need to make sure that they are actually an administrator — this is the “authority” part.

Intent is best described with an example. Let’s assume you can use the following link to delete a comment:

The script itself will check that the user is currently logged in and is an administrator, so it takes care of the authority check. Could someone still wreak havoc? Sure they could! A sneaky hacker could put a link on their own website pointing to the same location:

Super-Happy Times Here!

Because everyone likes super-happy times, many users would click the link. In 99% of cases, nothing would happen, because those visitors would not be administrators of But if a logged-in administrator of mysite.comdid click on the link, then the action would execute, even though the link was actually clicked from

You might think that the odds of this happening are astronomical. In a way you’d be right, but remember that a hacker can do this extremely easily and can automate it. Millions of people get spam email saying that Bill Gates will take away the Internet unless they pay $1,000. Most recipients don’t see the email or throw it out or mark it as spam or what have you, but perhaps 1 out of every 2 million people is lured in. A thousand bucks for basically doing nothing is not bad at all. And a hacker probably wouldn’t put the link on their own website; all they would need to do is hack a big website and embed the link there without anyone noticing.

Checking For Authority In WordPress

WordPress has a permissions system built in referred to as “Roles and Permissions.” Capabilities are the basis of the whole system; roles are just a way to group a set of capabilities together.

If a user has the delete_postscapability, then they have the authority to delete posts. If a user has the edit_postscapability, then they can edit their posts. Quite a few capabilities are available, and you can even create your own.

Roles are basically groups of capabilities. A user with the role of “contributor” has three capabilities: read, delete_postsand edit_posts. These give the user the authority to read posts and to edit or delete their own posts. These capabilities could be granted individually to any user, but grouping them into frequently used bundles is much easier and more practical.

With that in mind, let’s look at how to use WordPress functions to ensure that a user has the authority to complete an action that they initiate.

if(current_user_can("delete_users")) {
else {
    die("You naughty, naughty person. Of course, you could just be logged out…");

Here, we’ve made sure that the user has the delete_userscapability before they are able to complete the action. Don’t make premature assumptions when protecting your scripts; in many cases, especially those of authority, the intent is not malicious.

The current_user_can()function takes one argument, which can be a role or a permission. We could let “editors” (who don’t normally have the delete_userscapability) delete users in the following way:

if(current_user_can("editor")) {
else {
    die("You must be an editor to delete a user");

Be careful with the method above because the roles are not inclusive. This function doesn’t require the user to be at least an editor; it requires them to be exactly an editor (if that makes sense). Because of this, I find it preferable to use capabilities, especially if I have modified the default permissions extensively.

Two other similar functions enable you to examine the capabilities of users other than the currently logged-in one.

if(user_can(5, "manage_links")) {
    echo "User 5 is allowed to manage links";
else {
    echo "Sadness! User 5 may not manage links";
if(author_can(1879, "update_themes")) {
    echo "The author of post #1879 is allowed to update themes";
else {
    echo "Oh noes, our friend, the author of post #1879 may not update themes";

The user_can()function checks whether a given user has the given capability (or role). The first argument is the user’s ID or a user object; the second argument is the name of the capability or role that we want to check for.

The author_can()function checks whether the author of a given post has the given capability (or role). The first parameter should be a post ID or a post object; the second is the capability or role that we are examining.

Checking For Intent In WordPress

Intent is a bit more difficult to check. In the good ol’ days, a check of $_SERVER['HTTP_REFERER']was the way to go. This stored the page that the user came from. If the domain name was their own, then the user was probably OK… unless, of course, someone had gotten into their files and inserted a link that deleted the user’s database if they clicked on it as an administrator.

A newer more secure method was implemented in WordPress 2.03 — quite some time ago — called nonces. Nonce stands for “number used once” and is used frequently in cryptography to secure communications. It is a number that is generated before an action is initialized, then attached to the action’s call, and then checked before the action completes.

In WordPress, you would generally use nonces in one of two places: forms and normal links. Let’s look first at how to generate a nonce in a form.

Nonces in Forms

Enter an awesome word here

This will generate a hidden input field containing your generated nonce, which will be sent along with all of the form’s other data. The wp_nonce_fieldfunction takes four parameters:

  1. The first parameter is optional, but recommended because it gives the nonce a unique identifier.
  2. The second parameter is the name of the field. This is also optional and defaults to _wpnonce.
  3. The third parameter is boolean. If set to true, it will also send the referrer for validation.
  4. The fourth parameter is also a boolean and controls whether the field is echoed right then and there.

The resulting hidden field would look something like this:

Setting all of this up won’t make a huge difference if it isn’t used when the form is actually processed. We need to check for the presence and the value of the nonce before allowing any actions to be performed. Here is one way to do that:

if (!wp_verify_nonce($_POST['_wpnonce'],'awesome_name_nonce') ) {
   die('Oops, your nonce didn\'t verify. So there.');
else {

Here, we’ve used the wp_verify_nonce()function to make sure that our nonce is correct. This function takes two parameters: the first is the value of the nonce field, and the second is the name of the action that we defined (this was the first parameter for the wp_nonce_field()function). If the nonce is verified, then the function will return true; otherwise, it will return false.

Nonces in Links

In some cases, you will want a link, instead of a form, to perform an action. This would typically look like our previous examples:

To generate a nonce for a link, we can use the following method:

$base_url = "";
$nonce_url = wp_nonce_url( $base_url, "thingdeleter_nonce");
echo "Delete that thing";

The resulting link would be something like this:

When we actually go to the script, we can check the nonce using the same method as before:

if (!wp_verify_nonce($_GET['_wpnonce'],'thingdeleter_nonce') ) {
   die('Oops, your nonce didn\'t verify. So there.');
else {

Checking Authority And Intent At The Same Time

We need to look at both aspects at once; although, now that we’ve looked at all of the components, it won’t exactly be rocket science! Let’s take a simple link that lets the user delete a comment. We would have this on the page that lists comments:

$nonce_url = wp_nonce_url("", "delete_comment_nonce");
echo "dispose of this comment";

And here is the script itself:

if (wp_verify_nonce($_GET['_wpnonce'],'delete_comment_nonce') AND current_user_can("edit_comment")) {
else {
   die('Oops, your nonce didn\'t verify, or you are not permission-endowed enough.');

Data Security

Our work might seem to be done, but if you’ve been developing for a while, then you know it never is. An additional layer of security needs to be added to stop insecure data (or erroneous data) from entering our database. Adding this additional layer is called data sanitization.

A quick clarification: data sanitization refers to the process of cleaning up our data to make sure that nothing suspicious gets sent to the database. Validation refers to all of the checks we perform on data to make sure they are the types of data we need; it is typically done to ensure that the user has entered a valid email address, a well-formed URL, etc. The two terms are sometimes used interchangeably, and other methods may be similar, but they are quite different things. For our purposes, sanitization is a bit more important, because it has more to do with security.

The main thing we are trying to protect against is SQL injection. SQL injection is a technique used by hackers to exploit a database’s weaknesses. Take the following example:

// A hacker goes to your search field and searches for elephant' - note the apostrophe at the end. In the script, the following SQL is run:
SELECT ID, post_title FROM wp_posts WHERE post_title LIKE '%elephant'%'

In the example above, the user’s search for elephant'has resulted in unclosed quotes in your script. While the hacker might not be able to do much with this, an error message would be generated, indicating to them that at the very least you are not sanitizing your data.

In some cases, the SQL itself could be harmful or could give the hacker much more information than you’d like. Take the example of an administrator being able to enter a user’s log-in name in a form and getting back the user’s email address.

SELECT user_email FROM wp_users WHERE user_login = 'danielp'

If the hacker manages to perform an SQL injection attack, they could type ' OR 1=1 'in the form, which would result in the following query:

SELECT user_email FROM wp_users WHERE user_login = '' OR 1=1 ''

This would return all email addresses in the database, because we would be retrieving all addresses for which the user’s log-in name is an empty string, or 1=1, which is always true.

There are two ways to protect against this kind of problem — implementing both is good practice. In round one, we validate the data. If an email address needs to be entered, we can filter out user data that does not conform to the format of email addresses. We simply make sure that the format is right, otherwise we redirect the user, stating that the address is invalid. If the data passes round one, we move to round two, where we remove all characters that could mess up the query. This usually entails escaping quotes so that they can be used only as actual quotes in the SQL query.

When working without a framework, you would typically use addslashes()or something similar, but WordPress offers its own solution…

Data Sanitization In WordPress

When communicating with the database in WordPress, the preferred method is to use the $wpdbclass. You can read all about this in “WordPress Essentials: Interacting With the WordPress Database.” The class offers a number of methods to alleviate your SQL injection worries.

Before jumping in, let’s look at some examples to get a basic understanding of how the class works.

// Perform any query
$wpdb->query("DELETE FROM wp_users WHERE user_id = 5");
// Get one column of data
$posts = $wpdb->get_col("SELECT post_title FROM wp_posts WHERE post_status = 'publish' ORDER BY comment_count DESC LIMIT 0,10");
// Get a row of data
$post = $wpdb->get_row("SELECT * FROM wp_posts WHERE ID = 1453");
// Get multiple rows and columns
$posts = $wpdb->get_results("SELECT ID, post_title, post_date FROM wp_posts WHERE post_type = 'publish' ORDER BY post_date DESC LIMIT 0, 12 ");
// Get a single value 
$author_id = $wpdb->get_var("SELECT post_author FROM wp_posts WHERE ID = 2563");
// Insert a record
$wpdb->insert("wp_postmeta", array("post_id" => 2323,  "meta_key" => "favorite_count", "meta_value" => 224 ), array("%d", "%s", "%d"));
// Update a record
$wpdb->update("wp_postmeta", array("meta_value" => 225), array("meta_key" => "favorite_count", "post_id" => 2323), array("%d"), array("%s", "%d"));

The insert()and update()methods are helper methods, and they’re great because, apart from modularizing your database interactions a bit, they also take care of sanitization for you. If you want to use the general query()method, though, you will need to take care of it on your own.

The easier way is just to use the escape()method:

$data = $wpdb->escape($_POST[about_me]);
$wpdb->query("UPDATE wp_usermeta SET meta_value = '$data' WHERE meta_key = 'description' AND user_id = 154  ");

A slightly harder but better way to go about this is to use the prepare()method. An example from the WordPress Codex illustrates this perfectly:

$metakey	= "Harriet's Adages";
$metavalue	= "WordPress' database interface is like Sunday Morning: Easy.";
$wpdb->query( $wpdb->prepare( 
		INSERT INTO $wpdb->postmeta
		( post_id, meta_key, meta_value )
		VALUES ( %d, %s, %s )
) );