Locking Down a WordPress 4.* Blog Installation
As is the case for Drupal, WordPress at version 4.* arrives out of the box with a great many default functions, bells, and whistles turned on. If you are a causal user then it is very possible that your blog is even now serving pages and exposing access points that you have no idea even exist. It is rarely the case that you in fact want all of this functionality to be accessible, especially given that WordPress is a prime target for automated attacks. One part of hardening any web application is to shut down or block access to every unneeded component and subsystem, thereby reducing the attack surface.
A Starting Point
There is a useful semi-official guide on hardening WordPress that provides a good starting point for reading around the topic. It doesn't really talk as much about shutting off core functionality as it might, however. You should nonetheless still read it and follow its advice.Close the Server Firewall Prior to Launch
Configuring a new WordPress installation can be a fairly manual process, depending on the sophistication of your organization. Even if you have images and deployment scripts, it is probably the case that you'll make the last edits and tests by hand, and that could easily take a couple of hours. While you are doing all of this your installation is less than secure. Thus before deploying anything ensure that the destination server firewall prevents access by anyone other than members of your own organization. Only open the firewall to public web traffic once you are ready.
Serve All Content Over SSL
All of your site content should be served encrypted over SSL connections. There really is no excuse for doing otherwise these days. Buy an SSL certificate and set this up before you even start on the process of launching a new site.
Points of Intervention
The following places are where alterations are best made to shut down code functionality and otherwise harden a WordPress installation:
- The addition of plugins that shut down access to specific functionality.
- The wp-config.php file, where numerous switches are by default set to less optimal values.
- The .htaccess file, to deny or redirect requests to specific URLs.
- The arrangement of templates in your theme, as this can also be used to shut off access to functionality.
- The functions.php file in your theme, where most aspects of WordPress can be altered via the API functions.
- The webserver itself, Apache or Nginx, and its security functions.
Plugins to Gain a Head Start on the Lockdown
I've found the following plugins to be helpful, depending on your goals and which functionality you wish to remove.
Anti-spam implements the simplest of approaches to comment spam, nothing more than requiring the submitter to run Javascript. This nonetheless excludes the vast majority of spam bots, as they are not that sophisticated.
Disable Comments. Why bother with the hassle of a comment system if you don't need one? This plugin shuts down the WordPress comment system.
Disable Search. Do you really need search on your site, or is it good enough to have a form that redirects to Google, or even nothing at all?
Simple Trackback Disabler. This does exactly what it says on the label. You enable it, use it to adjust all of the necessary settings to turn off trackbacks and pingbacks, and then disable it. Since trackbacks are nothing more than spam channels these days, it is well worth doing.
Edits to wp-config.php
WordPress ships with a number of settings that default to "silly" rather than "sensible". Make the following changes:
/** * WordPress Database Table prefix. * * You can have multiple installations in one database if you give each a unique * prefix. Only numbers, letters, and underscores please! * * Changing the prefix to something other than 'wp_' will prevent some drive-by * SQL injection attacks from taking root. It can't hurt. */ $table_prefix = 'not_wp_'; /** * Make WordPress use SSL in places where it will try not to. Regardless of this * setting, .htaccess rules should be used to redirect all requests to SSL. */ define('FORCE_SSL_LOGIN', true); define('FORCE_SSL_ADMIN', true); /** * We don't want WordPress running its own hack on cron, events running when * people hit pages. Use the real crontab instead. E.g. Set up the following * in your server root crontab: * * /15 * * * * curl -sS http://localhost/wp-cron.php?local >/dev/null 2>&1 * * Also remember to disallow all access to /wp-cron.php from any location * other than the local server. */ define('DISABLE_WP_CRON', true); /** * This turns off the ability to edit PHP files via the administrative interface, * which is a terrible potential security hole to have enabled by default. */ define('DISALLOW_FILE_EDIT', true);
Edits to .htaccess
The top level .htaccess file is where most of the work takes place to lock down access to varied bits and pieces of WordPress that are not needed.
RewriteEngine On # Ensure no directory browsing. Options All -Indexes # Redirect non-SSL requests to SSL. RewriteCond %{HTTPS} !=on RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [R,L] # ---------------------------------------------------------------------------- # No request should ever access any of these files directly. # ---------------------------------------------------------------------------- RewriteRule ^\.htaccess - [F] RewriteRule ^install.php - [F] RewriteRule ^readme.html - [F] RewriteRule ^readme.txt - [F] RewriteRule ^wp-activate - [F] # This one blocks uploads that might be executed as PHP, as well as a range of # other undesirable situations. RewriteRule ^wp-content/.*\.php - [F] RewriteRule ^wp-config - [F] RewriteRule ^wp-includes/.*\.php - [F] RewriteRule ^wp-load - [F] # If you don't know what this is then you don't need it. RewriteRule ^wp-links-opml - [F] RewriteRule ^wp-settings - [F] RewriteRule ^wp-signup - [F] # Might be used for some tools, but too dangerous to allow global access. # If you don't know what this is, then you don't need it. RewriteRule ^xmlrpc - [F] # ---------------------------------------------------------------------------- # Removing access to optional functions. # ---------------------------------------------------------------------------- # Uncomment these blocking directives as you see fit to remove access to various # functions of WordPress that you may or may not be using. # Do you use attachments? If not then block them. #RewriteRule .*attachment_id= - [F] # Posts by author. #RewriteRule ^author/ - [F] # Posts by category. #RewriteRule ^category/ - [F] # Posts by tag. #RewriteRule ^tag/ - [F] # Posts by term. #RewriteRule ^term/ - [F] # If you are not using comments, you can block access to comment # submission completely. #RewriteRule ^wp-comments-post - [F] # If you are not using post by mail, then remove access to this. #RewriteRule ^wp-mail - [F] # Using trackbacks seems fairly pointless these days. So why let # all the spambots use it at all? #RewriteRule ^wp-trackback - [F] # ---------------------------------------------------------------------------- # Restrict access by IP address. # ---------------------------------------------------------------------------- # It is highly advisable to lock down wp-cron.php, wp-admin, and wp-login.php # (which includes the often targeted password reset functionality). Leaving it # open to the world is just encouraging attacks. Restricting access by IP # address is a good approach. # If you must leave it open, then use one of the professional security plugins # that works to block malicious login attempts. # Restrict cron to being accessed by the local machine, i.e. by a local crontab # job that runs it every so often. <FilesMatch "^wp-cron.php"> Order deny,allow Deny from all Allow from 127.0.0.1 </FilesMatch> # Uncomment and replace IP_ADDRESS_HERE with your IP address to restrict admin # and login access. #<FilesMatch "^(wp-admin|wp-login)"> # Order deny,allow # Deny from all # Allow from IP_ADDRESS_HERE #</FilesMatch> # ---------------------------------------------------------------------------- # Rules cribbed from various security plugins. # ---------------------------------------------------------------------------- # Rules to block unneeded HTTP methods RewriteCond %{REQUEST_METHOD} ^(TRACE|DELETE|TRACK) [NC] RewriteRule ^(.*)$ - [F] # Rules to block suspicious requests. RewriteCond %{QUERY_STRING} \.\.\/ [NC,OR] RewriteCond %{QUERY_STRING} ^.*\.(bash|git|hg|log|svn|swp|cvs) [NC,OR] RewriteCond %{QUERY_STRING} etc/passwd [NC,OR] RewriteCond %{QUERY_STRING} boot\.ini [NC,OR] RewriteCond %{QUERY_STRING} ftp\: [NC,OR] RewriteCond %{QUERY_STRING} http\: [NC,OR] RewriteCond %{QUERY_STRING} https\: [NC,OR] RewriteCond %{QUERY_STRING} (\<|%3C).*script.*(\>|%3E) [NC,OR] RewriteCond %{QUERY_STRING} mosConfig_[a-zA-Z_]{1,21}(=|%3D) [NC,OR] RewriteCond %{QUERY_STRING} base64_encode.*\(.*\) [NC,OR] RewriteCond %{QUERY_STRING} ^.*(%24&x).* [NC,OR] RewriteCond %{QUERY_STRING} ^.*(127\.0).* [NC,OR] RewriteCond %{QUERY_STRING} ^.*(globals|encode|localhost|loopback).* [NC,OR] RewriteCond %{QUERY_STRING} ^.*(request|concat|insert|union|declare).* [NC] RewriteCond %{QUERY_STRING} !^loggedout=true RewriteCond %{QUERY_STRING} !^action=jetpack-sso RewriteCond %{QUERY_STRING} !^action=rp RewriteCond %{HTTP_COOKIE} !^.*wordpress_logged_in_.*$ RewriteCond %{HTTP_REFERER} !^http://maps\.googleapis\.com(.*)$ RewriteRule ^(.*)$ - [F] # Rules to block foreign characters in URLs. RewriteCond %{QUERY_STRING} ^.*(%0|%A|%B|%C|%D|%E|%F).* [NC] RewriteRule ^(.*)$ - [F]
Clever Arrangement of Theme Templates
When rendering content WordPress looks for theme templates in order from a cascading list. If it can't find the most specific template for the type of post, page, or archive being rendered, then it will pick the next most general template, and so on all the way back to the required index.php template. If you create an index.php template that does nothing but redirect visitors to the home page, then only content with a defined theme template file will work. All others will use index.php and redirect to the home page. So you can block search, category pages, author archives, archives by tag, and so forth, just by omitting the relevant templates from the theme.
<?php /** * The main template file. * * This is the most generic template file in a WordPress theme * and one of the two required files for a theme (the other being style.css). * It is used to display a page when nothing more specific matches a query. * e.g., it puts together the home page when no home.php file exists. * * This one defaults to the home page, so every type of page that doesn't * have a template in this theme will use this template and redirect to the * home page. */ if (!preg_match("/^\/(index\.php)?$|^\/(index\.php)?\?/", $_SERVER['REQUEST_URI'])) { wp_redirect(home_url()); exit; } // If the theme defines a home.php template then that will always be used in place // of this one, but better safe than sorry. get_template_part('home');
Edits to functions.php
Add the following to function.php in your theme to shut down or remove the various noted items.
/** * Remove unhelpful junk from the dashboard. */ function remove_dashboard_meta() { // Dead weight. remove_meta_box('dashboard_primary', 'dashboard', 'normal'); remove_meta_box('dashboard_secondary', 'dashboard', 'normal'); // Things worth keeping, but noted here in case you want to remove // them. //remove_meta_box('dashboard_incoming_links', 'dashboard', 'normal'); //remove_meta_box('dashboard_plugins', 'dashboard', 'normal'); //remove_meta_box('dashboard_quick_press', 'dashboard', 'side'); //remove_meta_box('dashboard_recent_drafts', 'dashboard', 'side'); //remove_meta_box('dashboard_recent_comments', 'dashboard', 'normal'); //remove_meta_box('dashboard_right_now', 'dashboard', 'normal'); } add_action('admin_init', 'remove_dashboard_meta'); /** * Disable post-by-email: removes the configuration section and disables the * wp-mail.php file. */ add_filter('enable_post_by_email_configuration', '__return_false'); /** * Remove the meta tag for generator version. Why advertise to attackers? */ remove_action('wp_head', 'wp_generator');
Hardening Apache and Nginx
As mentioned earlier, you should be serving all content over SSL. Improving the security of the webserver beyond this is somewhat beyond the scope of this post. There is a lot you can be doing, however. In particular, you might look at setting up a Web Application Firewall such as mod_security for Apache. This is good for blocking SQL injection attacks and similar issues with characteristic signatures.
Other Odds and Ends
It is apparently considered a good idea to remove the default user called "admin" with user ID 1 and replace it with another administrative user with a different ID. This will block some crude but widespread attacks. This is easily done: log in as "admin", create another administrator user, log out of "admin", log in as the other user, and then delete "admin".