How to harden WordPress

As an application becomes more popular, more bugs are found as exploited. While there is no perfect way to both improve performance and keep your site secure, there are some approaches you can take to make things more difficult for potential attackers.

This guide makes the following assumptions :

  • You have secured your web server
  • You are running WordPress as a different user
  • You have disabled all unused and insecure commands in php
  • You take the time to keep plugins/themes/wordpress current

While there is a method to rename all the `wp-*` folders to obscure that WordPress is the engine driving your website, this guide will not be going into that area. Instead, we will focus on the simple things which can be done without touching WordPress itself as much as possible that can be done on most hosting providers. While some of the concepts are designed for Apache, you should easily be able to adapt the method for the webserver of your choice (NGINX, etc).


Locking down the database

WordPress doesn’t require many of the permissions that are typically assigned to the database once it is installed. As such, you should restrict the user you setup (you can find it in your wp-config.php file, to only the following permissions :

1
2
3
SELECT
INSERT
UPDATE

It is recommended that you disable automatic updates of plugins/themes/and wordpress itself (this will be part of the sample config file at the end), and instead use a different database user with the following permissions when you need to update or install a new plugin. The database user for managing updates and plugins, should have the following permissions :

1
2
3
4
5
6
7
8
SELECT
INSERT
UPDATE
DELETE
ALTER
CREATE TABLE
DROP TABLE
INDEX

While this makes things a bit more difficult to manage updates, this minimizes the damage that could be done to your database. You can also play with the permissions and lock the database user down on a table by table basis, further restricting access in the event your site was ever unfortunate enough to have had a successful breach of the database.


Restricting world access

There are a few things that WordPress does by default which, in our experience, is never or seldom used. Fortunately, there are ways to disable these features.

Block access to critical scripts

Inside /.htaccess

1
2
3
4
5
6
7
<Files xmlrpc.php>
  Deny from all
</Files>

<Files "class-wp-xmlrpc-server.php">
  Deny from all
</Files>


limit access to wp-admin and wp-login by ip

1
2
3
4
RewriteCond %{REQUEST_URI} ^(.*)?wp-login\.php(.*)$ [OR]
RewriteCond %{REQUEST_URI} ^(.*)?wp-admin$
RewriteCond %{REMOTE_ADDR} !^123\.123\.123\.123$
RewriteRule ^(.*)$ - [R=403,L]

The above code would be inserted just before RewriteCond %{REQUEST_FILENAME} !-f. Replace “123\.123\.123\.123” with the ip address you wish the IP address you wish to grant access to login.


Prevent uploads script execution

Inside : wp-content/uploads/.htaccess

1
2
3
<FilesMatch ".+\.*$">
   SetHandler !
</FilesMatch>

This will disable execution of php scripts in this folder.


Use common sense in your wp-config

prevent errors from being displayed

1
2
error_reporting(0);
@ini_set('display_errors', 0);

configure your database connectivity with a user that has SELECT, INSERT and UPDATE access

1
2
3
4
define('DB_NAME',                               '');
define('DB_USER',                               '');
define('DB_PASSWORD',                           '');
define('DB_HOST',                               '');

use a random prefix

1
2
define('WORDPRESS_TABLE_PREFIX',        'somethingrandom_');
$table_prefix  = WORDPRESS_TABLE_PREFIX;

customize the cache salt

1
define('WP_CACHE_KEY_SALT',             'SOME_RANDOM_SALT' );

bypass the database setting for your site url

1
2
define('WP_SITEURL',                            'http://www.onezeroless.com');
define('WP_HOME', WP_SITEURL);

disable wordpress’s php based cron. you should use a real cron job instead if needed

1
define('DISABLE_WP_CRON', true);

enable wordpress built-in cache

1
define('WP_CACHE', false);

only keep a couple revisions, preventing database bloat

1
define('WP_POST_REVISIONS', 2);

prevent editing of files from wp-admin

1
define('DISALLOW_FILE_EDIT',true);

set a limit on how much memory scripts can use

1
define('WP_MEMORY_LIMIT', '512M');

keep trash clean preventing database bloat

1
define('EMPTY_TRASH_DAYS', 0 );

use a modern charset for your data

1
2
define('DB_CHARSET', 'utf8');
define('DB_COLLATE', '');


Protect all files by making them read-only

Use the following commands from your wordpress root to disable write access to files, and keep write access to your uploads folder :

1
2
3
find . -type f -exec chmod 404 {} \;
find . -type d -exec chmod 515 {} \;
find ./wp-content/uploads -type d -exec chmod 755 {} \;

This will also prevent you from making changes to these files without first changing the permissions granting your user write access. [ 644 ]


WRAPPING EVERYTHING UP

/wp-content/uploads/.htaccess

1
2
3
<FilesMatch ".+\.*$">
   SetHandler !
</FilesMatch>

/.htaccess

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Options -Indexes

<Files xmlrpc.php>
  Deny from all
</Files>

<Files "class-wp-xmlrpc-server.php">
  Deny from all
</Files>

# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_URI} ^(.*)?wp-login\.php(.*)$ [OR]
RewriteCond %{REQUEST_URI} ^(.*)?wp-admin$
RewriteCond %{REMOTE_ADDR} !^123\.123\.123\.123$
RewriteRule ^(.*)$ - [R=403,L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress

/wp-content/plugins/lock-it-down.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
/*
Plugin Name: Lock It Down
Plugin URI: http://www.onezeroless.com/
Description: Assistant to help lock down WordPress by disabling some of wordpress's methods.
Author: One Zero Less
Version: 1.0
Author URI: http://www.onezeroless.com/
*/


// disable some automatic methods
$lock_these_down = array(
        // disable overwriting htaccess
        'flush_rewrite_rules_hard',

        // disable automatic plugin updates
        'auto_update_plugin',

        // disable automatic theme updates
        'auto_update_theme'
);

foreach($lock_these_down as $key=>$value) {
        add_filter($value, '__return_false');
}


// disable author archives
function disable_author_archives() {
        if (is_author()) {
                global $wp_query;
                $wp_query->set_404();
                status_header(404);
        } else {
                redirect_canonical();
        }

}
remove_filter('template_redirect', 'redirect_canonical');
add_action('template_redirect', 'disable_author_archives');


# remove creation of multiple sizes of images to save storage space
remove_image_size('large');
remove_image_size('medium');
remove_image_size('thumbnail');

/wp-config.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php
error_reporting(0);
@ini_set('display_errors', 0);

define('DB_NAME',                       '' );
define('DB_USER',                       '' );
define('DB_PASSWORD',                   '' );
define('DB_HOST',                       '' );
define('WORDPRESS_TABLE_PREFIX',        'somethingrandom_' );
define('WP_CACHE_KEY_SALT',             'SOME_RANDOM_SALT' );
define('WP_SITEURL',                    'http://www.onezeroless.com' );
define('WP_HOME',                       WP_SITEURL );
define('DISABLE_WP_CRON',               true );
define('WP_CACHE',                      false );
define('WP_POST_REVISIONS',             2 );
define('DISALLOW_FILE_EDIT',            true );
define('WP_MEMORY_LIMIT',               '512M' );
define('EMPTY_TRASH_DAYS',              0 );
define('DB_CHARSET',                    'utf8' );
define('DB_COLLATE',                    '' );
define('WP_DEBUG',                      false );

/**#@+
 * You can generate these using the {@link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org secret-key
 */

define('AUTH_KEY',         '');
define('SECURE_AUTH_KEY',  '');
define('LOGGED_IN_KEY',    '');
define('NONCE_KEY',        '');
define('AUTH_SALT',        '');
define('SECURE_AUTH_SALT', '');
define('LOGGED_IN_SALT',   '');
define('NONCE_SALT',       '');

$table_prefix  = WORDPRESS_TABLE_PREFIX;

if ( !defined('ABSPATH') ) { define('ABSPATH', dirname(__FILE__) . '/'); }
require_once(ABSPATH . 'wp-settings.php');

Run these commands in bash in your wordpress root

1
2
3
find . -type f -exec chmod 404 {} \;
find . -type d -exec chmod 515 {} \;
find ./wp-content/uploads -type d -exec chmod 755 {} \;