Steve Taylor photo

WordPress hacks and tips: Security

WordPress security image based on image by Net Efekt I well and truly cut my WordPress security teeth last year when my server got hacked. I summarized my lessons learned in that post, but the post also included a lot of things specific to the attack I was subject to. I thought I’d round up my WP security measures here for easy reference.

There’s many, many things you can do to secure WP. I’ll give links for further reading at the end. Documented here are my “baseline” measures that I make sure are in every WP deployment I create.

Use strong, random passwords

The database password should be very strong, just a long string of random characters. It’s hard-coded into wp-config.php, so you don’t need to remember it.

If you have mutiple WP installations on a single shared server, try to give each WP installation—or at least, each person using WP installations—their own database user. My own server was hacked when someone I let use my server space didn’t upgrade WP very often. Their site got hacked, and because I stupidly used the same database user for all sites, all the other installations got compromised.

For admin users in WP, I try to use long, random passwords; they’re cached in the browsers I use on my own machines, and for remote use I can either store the password in an email account (itself with a memorable but secure password, and accessed via HTTPS), or temporarily change the WP admin password to something memorable but strong before setting out.

Make sure you change the security keys

These are the strings in wp-config.php that WP uses to secure logging in, form submissions, etc. Luckily you’re given a handy page to generate fresh values for you.

Change the table prefix

Before running the WP installation script (which creates the database), make sure you change the setting that defines the database table prefix in wp-config.php. Use a password generator and grab 5-10 random characters. Set these as the value for the $table_prefix (leave an underscore at the end of the value, it makes it easier to read table names).

If you’ve already installed WP and need to change the table prefix, change it in wp-config.php as above, then run some SQL with phpMyAdmin to rename the existing tables. This code assumes the current prefix is wp_:

RENAME TABLE wp_comments TO [your prefix here]_comments;
RENAME TABLE wp_links TO [your prefix here]_links;
RENAME TABLE wp_options TO [your prefix here]_options;
RENAME TABLE wp_postmeta TO [your prefix here]_postmeta;
RENAME TABLE wp_posts TO [your prefix here]_posts;
RENAME TABLE wp_terms TO [your prefix here]_terms;
RENAME TABLE wp_term_relationships TO [your prefix here]_term_relationships;
RENAME TABLE wp_term_taxonomy TO [your prefix here]_term_taxonomy;
RENAME TABLE wp_usermeta TO [your prefix here]_usermeta;
RENAME TABLE wp_users TO [your prefix here]_users;

Then, in the options table, change the option_name called wp_user_roles to have your new prefix instead of wp_. Likewise for all meta_key values in the usermeta table that start with wp_.

And make sure you use $wpdb->prefix when referencing table names in any custom theme SQL code! Some badly written plugins might fall foul of this if they’ve hard-coded table prefixes; any plugin worth its salt will be OK, but watch out.

Change the default admin account username

Don’t leave it as “admin”! Make it something unusual. Get into the database via phpMyAdmin and change the user_login field for the default account.

Don’t advertize your WP version

Of course it should go without saying that you should keep WP and all plugins as up-to-date as possible. But there’s no point in letting anyone know what WP version you’re on. The wp_head() function will include this by default through a theme’s header. To stop this, add this line to your theme’s functions.php:

remove_action( 'wp_head', 'wp_generator' );

UPDATE! I’ll leave the above here for reference, but thanks to WP-Scanner I’ve discovered that this only removes the generator tag from the page header. It’s still included in the RSS feed. To completely remove it, attack it at the source (solution thanks to follow the white rabbit). Use this instead of the above code:

function no_generator() { return ''; }
add_filter( 'the_generator', 'no_generator' );

Also, delete readme.html from the root of your WP installation. That’s got the version number in, too.

.htaccess precautions

At the very least, add these lines to .htaccess:

# Prevent directory listing
IndexIgnore *

# Protect .htaccess files
<Files .htaccess>
	order allow,deny
	deny from all
</Files>

# Protect wp-config.php
<FilesMatch ^wp-config.php$>
	deny from all
</FilesMatch>

The Perishable Press blacklists

Jeff Starr at Perishable Press has some obsessive compilations of .htaccess that should add many extra layers of security, if you need it. I’ve not used them myself, so I should note that some people seem to have experienced conflicts between Jeff’s code and legit operations in their WP installations. However, Jeff seems to be very sharp at keeping everything updated; and he’s always keen to point out that his demonstrated technique is sometimes more important than the data that the lists represent. The actual blacklist data should be tweaked according to your own experience of your access logs.

Anyway, the most important posts seem to be the 4G Blacklist and the related User-Agent Blacklist.

Remove or replace install.php

Jeff also recently posted a notice about a possible WP vulnerability. I’m not sure about how his situation happened, but his advice, to remove or replace wp-admin/install.php, seems worth following. It’s not needed again post-installation.

Automate data backups

I was lucky; the hack I was subject to lost me no data at all. Of course, security shouldn’t rely on luck! Install WP-DBManager and schedule regular backups.

Coding best practices

If you’re coding custom themes or plugins, always follow the WordPress Coding Standards. Of particular note, security-wise, is the syntax for making your SQL queries secure:

$var = "dangerous'"; // raw data that may or may not need to be escaped
$id  = some_foo_number(); // data we expect to be an integer, but we're not certain
$wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET post_title = %s WHERE ID = %d", $var, $id ) );

So, $wpdb->prepare() will switch in the sequence of variables you provide at the end, and you need to specify their placeholders with %s for a string and %d for an integer.

Plugins

There aren’t any plugins for specific security reasons that I currently use regularly (I’m discounting plugins to stop spam). However, here are some that are worth checking out:

WordPress Scanner
“A free online resource that blog administrators can use to provide a measure of their wordpress security level.” Involves using a plugin, but the scan itself seems to run from a remote server.
WP Security Scan
More scanning. “Scans your WordPress installation for security vulnerabilities and suggests corrective actions.”
Limit Login Attempts
I assume it does what it says on the tin.
Secure WordPress
A bundle of little security measures in one plugin.
WordPress File Monitor
“Monitors your WordPress installation for added/deleted/changed files. When a change is detected an email alert can be sent to a specified address.”

References and further reading

8 comments

  1. when I put in your code to remove the generator tags I get an error:
    Fatal error: Call to undefined function add_filter() in /—/—/—/—/functions.php on line 75.

    Am I doing something wrong.

    Really, really nice resource. Thanks

  2. Steve Taylor avatar Steve Taylor

    Hi Steve, add_filter() is a built-in WordPress function (check the Codex). If it’s generating an “undefined function” error in your theme’s functions.php file, I’m a little lost for ideas. I can’t think how I would generate that error if I wanted to! It suggests you’ve got some kind of serious problem with your theme set-up, but I can’t guess what. If your site’s otherwise working, maybe use one of the many plugins that can remove the generator tags?

  3. Thanks, Steve. I re-upped the virgin copy of functions.php form the zip file, which fixed the errors.

    I then proceeded to look for a plugin to remove the tag, first at wordpress (I trust those more) to no avail, but found a nice metatag plugin for keywords and descriptions, seo stufff.

    I then tried google and did find a couple, but in my mucking around found out that it is the theme/functions.php file that the code needs to go into.

    Working great now, rss pages too. Also added your htaccess code for wp-config and htaccess.

    This page is bookmarked for future installs.

    Steve

  4. Steve Taylor avatar Steve Taylor

    Steve, I always assume when I mention adding code to functions.php that people know I mean the theme’s functions.php. I guess you were altering /wp-includes/functions.php. Well again, I assume people know that altering core files is generally a no-no. Anyway, I’ve altered the above wording to specify that it’s the file in the theme…

  5. Thanks, Steve. I’ve been writing html from scratch for quite a few years, using wordpress is only my second of two recent “website in a box” installs. Frankly WordPress has been the harder of the 2, and I always look for paths, so functions.php vs themes/functions.php told me where to go, I thought.

    Just gotta figure out how to hack the header on this theme I’m trying to use…

    Steve

  6. Paul Bell avatar Paul Bell

    Steve, Just a note of appreciation from a newbie – sorry I can’t currently make any material contribution to your blog just yet. But as I am onto my second WordPress site now, I have to say to those people who make their knowledge public for the rest of us, deserve a big pat on the back!

  7. Allison Clark avatar Allison Clark

    I agree with Paul, people like Steve deserve a huge hug, haha. This page is great for newbies. Thanks for this guide.

  8. Graham avatar Graham

    Hi Steve, we’ve been using WordPress quite a bit, it’s such a great system. One of the big concerns though is security, specially as we build for clients. This has been really useful information – some of the things we were doing already – but some we weren’t and will certainly start doing from now on.

    Thanks again
    Graham

Leave a comment

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

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>