22 November 2021

Toolkit includes nearly 20 different security measures you can apply. In this article I look at what the options do, and I will give some additional tips along the way. This article is more technical than the other Toolkit articles but hopefully you will take away a few useful things.

Also, making WordPress more secure is a very broad topic. The available measures in Toolkit are convenient and useful, but they are not the holy grail. If you want to learn more about securing WordPress, the official documentation is a good starting point.

The Security Staus page in Toolkit lists just under 20 measures that can be applied to make your WordPress website more secure.
Image: the hardening measures can make your WordPress instance more secure.

File permissions

Restrict access to files and directories

WordPress websites typically have 644 permissions for files and 755 permissions for directories. Restrict access to files and directories sets these permission. It effectively fixes any incorrect file permissions.

In addition, it changes the permissions on the wp-config.php file to 600 (read and write permissions for the owner of the file only). That is important, because the file contains the database credentials. Restricting the permissions makes the file a little more protected.

WordPress tweaks

Configure security keys

WordPress defines eight security keys in the wp-config.php file. The keys do slightly different things, but the overall aim is to make your website more secure by making cookies for logged in users more difficult to read.

By default, the keys are not set. If you look at the wp-config.php file on a fresh WordPress install you almost certainly see this:

define( 'AUTH_KEY',         'put your unique phrase here' );
define( 'SECURE_AUTH_KEY',  'put your unique phrase here' );
define( 'LOGGED_IN_KEY',    'put your unique phrase here' );
define( 'NONCE_KEY',        'put your unique phrase here' );
define( 'AUTH_SALT',        'put your unique phrase here' );
define( 'SECURE_AUTH_SALT', 'put your unique phrase here' );
define( 'LOGGED_IN_SALT',   'put your unique phrase here' );
define( 'NONCE_SALT',       'put your unique phrase here' );

You can easily add unique phrases. If you open api.wordpress.org/secret-key/1.1/salt/ in your browser you get randomly generated phrases which you can copy and paste into your wp-config.php file. Enabling configure security keys in Toolkit does that for you: it grabs random phrases and adds them to your wp-config.php file.

Turn off pingbacks

WordPress was launched in 2003, during the years that blogs (short for weblogs) became very popular. Although WordPress is no longer primarily used for blogs, much of its design is still centered around blogs. For instance, WordPress still uses the concept of posts (dynamic content, such as blog articles) and pages (static pages, such as an ‘About’ page). Other types of content, such as product pages in a shop or this here knowledgebase, are typically shoehorned into these basic content types.

One typical blog feature are pingbacks. A pingback is a notification that someone has linked to content on your website. So, if someone links to one of your blog posts on their blog a notification can be shown in the comment section of your blog post. The notification includes a link back to the website that linked to your post. The idea is to connect like-minded bloggers.

Of course, this opens all sorts of possibilities for SEO professionals and, more generally, people desperate to get links to their website. They can simply link to lots of WordPress blogs and hope that they have pingbacks enabled. If so, they will get lots of links, and search engines might think their website is somewhat popular. That might then improve the website’s search engine rankings. As said, it is pretty desperate.

By default, pingbacks are enabled in WordPress. Unless you really like pingbacks you probably want to disable them. The turn off pingbacks option does just that. In the background, it simply updates the default_ping_status and default_pingback_flag settings in the options table.

Bonus tip: disable xmlrpc.php

As a bonus tip, pingbacks rely on XML-RPC scripts. The xmlrpc.php file is a common target for attackers. If you don’t use pingbacks you might also want to also disable access to the xmlrpc.php file. You can do so by adding the below rule in your .htaccess file:

# Deny xmlrpc.php requests
<Files xmlrpc.php>
order deny,allow
deny from all

It is worth noting that XML-RPC does more than facilitating pingbacks. WordPress apps on smartphones, for instance, also make use of XML-RPC. If you use such an app then you might want to consider not using it. WordPress apps make your website’s attack surface larger (because the app might have vulnerabilities), and by not using the app you can safely disable XML-RPC.

Disable file editing in WordPress Dashboard

Lots of bad things can happen when an attacker is able to compromise (or create) and account with admin privileges. Among others, the attacker could add malicious code to plugin and theme files. The disable file editing measure prevents that users can edit such files from within the WordPress dashboard.

The option simply adds a DISALLOW_FILE_EDIT line to your wp-config.php file:

define( 'DISALLOW_FILE_EDIT', true );

Database tweaks

Change default administrator’s name

Some WordPress installers (including Softaculous) create an admin user with the username admin by default. This is terrible for security. Attackers know that the default login URL is /wp-login.php and that the default username is admin. That means they only have to guess the password to gain access to your website’s dashboard.

If your WordPress login name is admin then you want to change that. Unfortunately, it is difficult to do so manually. The process is as follows:

  • Create a new user with admin privileges.
  • Delete the admin user and make sure that all content belonging to the user is transferred to the new user.

The change default administrator’s name measure does the above for you. So, with the flick of a switch you can make your website much more secure.

Bonus tip: change the login URL

Changing the default username is definitely one of the more useful options in Toolkit. If you want to further improve the security of your website then you can also use a plugin to change the login URL. Doing so makes it near-impossible for attackers to brute-force your website, as they will also have to guess where the login page is.

There are a few different plugins that can change the login page from wp-login.php to something else. I typically use All In One Security. The plugin lets you easily change the login URL and has some other nice features, such as adding spam protection to any forms on your website.

If you change the login URL then you can tell Toolkit where the new login page is via the Set Up menu. However, if you are security-minded then you probably don’t want to do so. You won’t be able to log in to WordPress via the Login button in Toolkit, but that is arguably a feature rather than a bug. People with access to your cPanel account shouldn’t be able to log in to your WordPress dashboard with the click of a button.

Another bonus tip: enable 2FA

You can also use two-factor authentication (2FA). This is again not something Toolkit can enable – you have to use a plugin. I show how 2FA works in my article about password hygiene and multi-factor authentication.

Change database prefix

The wp-config.php file defines a table prefix. This is simply a string that is prepended to tables names. It makes it possible to use a single database for multiple WordPress instances; as long as the WordPress instances use different table prefixes the database tables won’t clash.

By default, the prefix is wp_. So, in a standard WordPress database you see tables such as wp_posts and wp_users. It is sometimes recommended to not use the default table prefix. The argument is that using the default prefix makes it easier for malicious scripts to exploit the database.

It can also be argued that any script that can access the database will have no trouble finding the table names; a simple SHOW TABLES; command lists all the tables in the database. In other words, if an attacker can execute database queries it is pretty much game over. Random table prefixes are not going to stop the attacker.

Anyway, the change database prefix measure changes the prefix to a random string: it updates the $table_prefix variable in wp-config.php and renames the database tables. Renaming tables is a relatively risky operation, so you want to make a backup of the database before you use this option.

Changes to Apache config

Most of the hardening measures update the Apache configuration for your virtual host. The first time you enable one the below measures a new config file is created in the /etc/apache2/conf.d directory. Any directives in this file are applied to just your website.

The file is stored outside your home directory, so you can’t access it. If you suspect that a measure broke your website then you can revert and/or re-apply the option via Toolkit. You can also manually add the rules to your .htaccess file (which is just another Apache configuration file). The advantage of the latter approach is that you can easily view, edit and/or remove the rules.

Block directory browsing

By default, anyone can list the contents of directories without an index file (such as index.php). For instance, for my example.com website I can see all the files and directories in wp-content/uploads/2021/11 by opening example.com/wp-content/uploads/2021/11 in a browser.

When directory browsing is enabled anyone can list the contents of the WordPress uploads directory. The screenshot shows the files I uploaded in November 2021.
Image: browsing files in the uploads directory.

The Block directory browsing measure adds a rule that disables directory listings:

# "Block directory browsing"
<Directory "/home/example/public_html">
    Options -Indexes

Block unauthorised access to wp-config.php

The wp-config.php file contains sensitive information, including your website’s database credentials. Visitors can’t access the file, unless PHP is somehow disabled on your server. When PHP is disabled users can download any PHP files, including wp-config.php.

Obviously, it is extremely unlikely that PHP is disabled in the Apache configuration. Still, there is an easy way to prevent certain PHP files can be downloaded, and it can’t hurt to always deny direct access to wp-config.php. When you enable block unauthorized access to wp-config.php the following rule is added:

# "Block unauthorized access to wp-config.php"
<Files wp-config.php>
  Require all denied

As an aside, please never store sensitive files such as website backups in your website’s document root. The backup is likely to include the wp-config.php file, so you don’t want it to be publicly available. Visitors to your website can’t see the file (provided there is an index file), but there are malicious bots that scan websites for archive files. If the bot manages to download your backup then the attacker has a copy of your entire website, including the wp-config.php file.

Block access to sensitive files

There are quite a few more Toolkit measures that block access to certain files. Block access to sensitive files denies access to, among others, copies of the wp-config.php file. For instance, when you edit the file your text editor may create a temporary copy named wp-config.php.swp. That file could be accessed, and it would expose the database credentials.

The rule that is added also denies access to some of the default WordPress files, such as readme.html and license.txt. Those files are unlikely to reveal much information about your website, but it is good practice to not publish files that are not for public consumption.

And as an aside, in the Apache configuration file the rule is incorrectly labelled Block author scans (which is a bad “copy and paste” job by the Toolkit developers):

# "Block author scans"
<LocationMatch "(?i:(?:wp-config\\.bak|\\.wp-config\\.php\\.swp|(?:readme|license|changelog|-config|-sample)\\.(?:php|md|txt|htm|html)))">
  Require all denied

Block access to potentially sensitive files

There are more files that should not be publicly accessible, such as log files, .ini files and shell scripts. The block access to potentially sensitive files rules matches various file extensions and denies direct access to matched files.

# "Block access to potentially sensitive files"
<LocationMatch ".+\\.(?i:psd|log|cmd|exe|bat|csh|ini|sh)$">
  Require all denied

Block access to .htaccess and .htpasswd

And there are yet more files that should not be accessed directly: .htaccess and .htpasswd. The former is an Apache configuration file and the latter is used to password-protect directories. The block access to .htaccess and .htpasswd directive matches files the start with .ht and denies access to them.

It is worth noting that the default Apache configuration already denies direct access to these files. Some of the Toolkit measures are redundant on a properly configured Apache server. Still, it can’t hurt to enable measures like this. The worst possible outcome is that access to the files is denied twice.

# "Block access to .htaccess and .htpasswd"
<FilesMatch ^(?i:\.ht.*)$>
  Require all denied

Forbid execution of PHP scripts in the wp-content/uploads directory

There are certain directories that shouldn’t include PHP scripts. One of them is wp-content/uploads. The forbid execution of PHP scripts in the wp-content/uploads directory measure adds a rule that prevents PHP scripts in the directory can be accessed:

# "Forbid execution of PHP scripts in the wp-content/uploads directory"
<Directory "/home/example/public_html/wp-content/uploads">
  <FilesMatch \.php$>
    Require all denied

So, someone trying to access example.com/wp-content/uploads/2021/11/xyz.php would get an error 403. This is a useful measure. Compromised WordPress websites often have malicious PHP scripts in the uploads directory, and it is not so easy to clean up those files. You can’t simply remove the entire directory and manually reinstall it, as the directory contains images and other files you have uploaded.

Forbid execution of PHP scripts in the wp-includes directory

Forbid execution of PHP scripts in the wp-includes directory does a similar thing for the wp-includes directory:

# "Forbid execution of PHP scripts in the wp-includes directory"
<IfModule mod_rewrite.c>
  <Directory "/home/example/public_html/wp-includes">
    <FilesMatch \.php$>
      RewriteEngine on
      RewriteCond %{REQUEST_FILENAME} !^/home/example/public_html/wp\-includes/js/tinymce/wp\-tinymce\.php$ [NC]
      RewriteRule .* - [NC,F,L]

Disable PHP execution in cache directories

There shouldn’t be any PHP scripts in cache directories either. Disable PHP execution in cache directories adds a rule that tries to match any PHP scripts in cache directories and denies access to them.

This may cause issues if a plugin or theme stores PHP scripts in a cache directory (which is a nonsensical thing to do). You can revert the rule if it breaks any plugins or themes.

# "Disable PHP execution in cache directories"
<LocationMatch "(?i:.*/cache/.*\\.ph(?:p[345]?|t|tml))">
  Require all denied

Disable scripts concatenation for WordPress admin panel

By default WordPress concatenates scripts when you are logged in. This improves performance, as it reduces the number of scripts that need to be run. Script concatenation can be disabled via the wp-config.php file, which is exactly what this measure does:

define( 'CONCATENATE_SCRIPTS', false );

In addition, the measure adds a rule that denies URLs containing the string load-styes or load-scripts:

# "Disable scripts concatenation for WordPress admin panel"
<Directory "/home/example/public_html/wp-admin">
  <FilesMatch (load-styles|load-scripts)\.php$>
     Require all denied

Of all the hardening measures Toolkit enables this is arguably the only one that is questionable. According to the Toolkit help text the rule prevents certain denial of service attacks. It does not explain how, and adding a rule to a config file users can’t access can result in unexpected errors. If, for instance, you set CONCATENATE_SCRIPTS to ‘true’ in your wp-config.php file you will run into error 403s, as the concatenation URLs are blocked in the Apache configuration.

Block author scans

An attacker who wants to brute-force your website needs to (at a minimum) crack your username and password. To get the username the attacker can run an author scan. As the name suggests, the scan looks for author names. If the attacker finds an author name they can next try to use the author name as the login name.

It is worth noting that the author and username can – and ideally should – be different. For instance, your admin username can be a random string, while your author name can be your real name (or anything you fancy).

Anyway, the block author scans option denies access to URLs that contain the string author= followed by one or more digits.

# "Block author scans"
<IfModule mod_rewrite.c>
  <Directory "/home/example/public_html">
     RewriteEngine on
     RewriteCond %{QUERY_STRING} author=\d+
     RewriteCond %{REQUEST_FILENAME} !^/home/example/public_html/wp\-admin/ [NC]
     RewriteRule .* - [F,L]

Enable bot protection

There are quite a few useless bots (also known as crawlers) that can cause an excessive amount of traffic to your website. Many of the bad bots are so-called SEO bots. They often ignore robots.txt files that ask bots not to index your website, and they often have no crawl delay. They just scrape your website as fast as they can, and cause your site to slow down in the process. The enable bot protection measure blocks a number of bad bots via the Apache configuration file. This is a good approach for crawlers that misbehave.

Toolkit’s rule includes some of the usual suspects, such as MJ12bot, AhrefBot and SemrushBot. Other bots on the lists are ones we rarely see nowadays. And that is kind of a problem with blocking bots. It sometimes feels like a new SEO bot is created every day, while other bots disappear. Rules like this need to be kept up to date.

We block useless bots that misbehave on all servers (using ModSecurity). So, you can enable the measure, but doing so is more or less redundant. We already have you covered; we got a more up to date rule that does exactly what this Toolkit measure does. Still, if you are curious, Toolkit adds the following rule:

<IfModule mod_rewrite.c>
  <Directory "/home/example/public_html">
     RewriteEngine on
     RewriteCond %{HTTP_USER_AGENT} "(?:acunetix|BLEXBot|domaincrawler\\.com|LinkpadBot|MJ12bot/v|majestic12\\.co\\.uk|AhrefsBot|TwengaBot|SemrushBot|nikto|winhttp|Xenu\\s+Link\\s+Sleuth|Baiduspider|HTTrack|clshttp|harvest|extract|grab|miner|python-requests)" [NC]
     RewriteRule .* - [F,L]

Other Toolkit articles