WordPress security:
13 recommendations and 1 myth

20 May 2018

Hi!

Here is a small beginner’s guide to protecting a WordPress website. I admit, there’s nothing from me in this article, but after collecting information here and there, I told myself I was going to check them and put them together in a post.

I’m talking about basic security here; so I limit myself to simple recommendations, using common sense, and a few lines of code to fill some gaps in WordPress installations, to be inserted into .htaccess (Apache server directives) and functions.php (your WordPress child theme) files.

None of my sites seem to have been hacked and I have never detected anything abnormal in my logs or files, so I consider for the moment that the items below are sufficient for their content and (low) traffic. It may not be the same story for more exposed targets and/or containing more coveted information such as bank details.

In the following list, I start with the most elementary advice, of a general nature, then I continue with more specific and more technical measures (although in practice it is just copy/paste …). Ready? Then let’s go!

Never assume that your website is not worth being attacked

WordPress website attacks are not personal; they are systematic attacks carried out by automated scripts (bots or botnets). The motivations for these attacks are varied, for example using your bandwidth, redirecting traffic, turning your machine into a gateway, sending spam…

So do not think that your “simple blog” has no interest in being hacked … ALL WORDPRESS SITES are targets!

Finish installing WordPress properly

This might sound strange, but if you copy WordPress files to your hosting without starting the installation (or abandoning it in progress), the system is in a very vulnerable state, since the /wp-admin/setup-config.php file is freely accessible.

Now, robots scan the URLs looking for this file in order to perform the installation themselves and implement a back door before returning the site to you in the initial state.

If you wonder by what miracle they are aware of your installation, find out how hackers find your site even before you have installed it.

Choose secure passwords

Choose complicated passwords for hackers but not too much for you:

  • A different password on each site allows you not to put all your eggs in one basket.
  • The longer the password, the longer it will take to crack. Today, beyond a dozen characters, it takes several years to decode a password with a conventional computer, so a length between 15 and 20 will be sufficient (it is unlikely that you are attacked by a CRAY …).
  • The addition of capital letters, numbers and special characters further increases the complexity of decryption.
  • The use of words from the dictionary is obviously to be avoided, because the password would be discovered immediately or within a few minutes.
  • A sentence can be very easy to memorize but very complex to decipher. To be customized anyway for each site, and again with numbers, capital letters and special characters.

As an indication, you can check the complexity of your password.

Choose a secure administrator login

NEVER use “admin” for the site administrator login. Neither too obvious identifier like “root”, the name of the site, your name, etc.

It would make the work of hackers and botnets and their brute force attacks far too easy. So let’s add a little difficulty by masking the most sensitive access.

Keep your system up-to-date

Updating your WordPress installation regularly is unfortunately not a reflex for everyone, including web professionals. Yet this is the only way to remove flaws, public or not, exploited by hackers.

Personally, I sometimes wait a few days, even weeks, for big Wordpress or themes updates, when I read or feel that they are potentially at risk … I consult the forums to see if there are any returns, patches, etc., before switching.

But most of the time I do it as soon as I see a notification, especially for plugins, INCLUDING INACTIVE PLUGINS, whose code can be executed, and therefore flaws exploited.

Note: Beyond the security aspect, it’s also a huge maintenance time-saver, compared to more spaced updates, which are much more difficult to debug when things go wrong (and statistically, after a few months, it often goes wrong).

Perform regular backups

Since the security of a system is never 100% guaranteed, regular backup is the only practice that ensures you do not lose content. Store your archives in several places (external disk, cloud, USB key …) to reduce risks to “almost” absolute zero.

Check the origin of themes and extensions

It is always safer to check the software you want to install, whether on a computer or on a website.

Themes on popular sites and with a lot of positive reviews are obviously safer than a cracked plugin or a free theme found on more obscure sites, perfect vectors for spreading viruses or more generally malware.

In addition, even if it is rare, some botched and/or buggy themes or plugins can simply corrupt your installation without malicious intent. If in doubt, make a backup before testing anything.

Fill in security keys and salt

Secret keys in the config.php file (located at the root folder of your website) are “hash” keys used by WordPress to encrypt passwords (stored in the database) and information contained in its cookies, including user identifier and password.

“Salt”, consisting of the same number of additional keys (with _SALT suffix), has been added to harden the decryption of generated hashes even more.

Example of security keys:

define('AUTH_KEY',         ']T_4Nsg#nF]EqrdOr~d4OBZNOgm_a(<<d,a>wR]WIYu]%a+?/}TMUSKF(vs1[Q6*');
define('SECURE_AUTH_KEY',  'nZIES6@lmU;u9H>-p}Q_gi.Wc/JRREMxc1p!${[||_8tW%jSUw3FW$P+:VaA&UA<');
define('LOGGED_IN_KEY',    'cnp$A1D+xSREH0z%omm=p-,+RfYRYN]<`h8unv6)vOB-jp`%$D#X~{S|e>vyAjV3');
define('NONCE_KEY',        'Pu_;II4^4cF{KQ8b)=|BW2s+)Oyt7CndzPXyE~$#2@qs/>ts@T @$-_lZ {N,^yv');
define('AUTH_SALT',        'bI5VoBw65an{+}4j,_-[p=5cYaexs6FPIJSNteG[A~F;UJ!f^&T1wjyFC2CGP:qH');
define('SECURE_AUTH_SALT', '9v@HSUWaHZ142&]aTltLEtJc8& D>G6yj-=0lZf.9X!9m9d:L_H}=O^aq`boL)*9');
define('LOGGED_IN_SALT',   'te&SX/K-C}GkGQvy~f+gI-h0WE9G*2tkT)o]g-Id(>kM3AX^n``vX0Ks|:,8JZ?+');
define('NONCE_SALT',       '(fO8D+SLtD+4nC&5&KJy~07_;21~&P_M@/=(-J@~/[zt&p!eE-8KT4wf`GCQ?+,|');

These keys must be filled in the config.php file, for otherwise WordPress automatically generates some and stores them in database, thus in the same place as the passwords, which facilitates the deciphering of these last ones.

If this is not the case, use this keys generator provided by WordPress and copy the result to your file.

You will also be able to renew these keys to invalidate all cookies that have not yet expired or just as a precaution, in the same way that you should change all your passwords regularly in case one day some files with your IDs ever get leaked (it happens to the biggest companies!).

And of course you MUST change them at the slightest hint of intrusion, just like your password.

Note: To know more about salt, read why salt keys are important.

Prevent discovery of author’s ID

Hide authors profile pages

By entering the domain name of your website followed by “? Author = 1” in your browser’s address bar, chances are that you will be redirected to a page yoursite.ext/author/<user>/, where <user> represents the administrator login. And if it does not work with 1, it will work for another one. Therefore, a very basic script will allow anyone to find the sensitive identifiers of your site.

Information that we would like to hide … For this, let’s just ignore the query and return an error by adding these lines in the .htaccess file located at the root folder of your website:

# Hide authors pages
<IfModule mod_rewrite.c>
 RewriteCond %{QUERY_STRING} ^author=([0-9]*)
 RewriteRule .* - [F]
</IfModule>

Remove authors’ logins from comments

When a registered user answers a WordPress comment, his or her login is directly revealed in the comment’s HTML code itself!

So, let’s insert the following lines into your child theme functions.php file to hide the authors’ logins:

// Remove author's login from comments
function remove_comment_author_class( $classes ) {
 foreach( $classes as $key => $class )
  if(strstr($class, 'comment-author-' ))
   unset( $classes[$key] );
 return $classes;
}
add_filter( 'comment_class' , 'remove_comment_author_class' );

Protect sensitive files and folders access

Some files in your WordPress installation contain highly confidential data that must be protected.

Similarly, it is better to prevent the display of folders content when they have no index.php file (thus accessible to anyone by default).

Add these lines to your .htaccess file:

# Protect folder content
Options -Indexes

# Protect wp-config.php
<files wp-config.php>
 Order allow,deny
 Deny from all
</files>

# Protect .htaccess and .htpasswds
<Files ~ "^.*\.([Hh][Tt][AaPp])">
 Order allow,deny
 Deny from all
</Files>

Remove login error messages

When making a mistake in the WordPress login form, a message informs the visitor that the ID does not exist or that the password does not match the ID.

It may be convenient for us, but it is especially handy for a hacker, who could first launch an attack to find the site’s logins, then a second one to crack the needed passwords.

So, remove these messages using the functions.php file of your child theme:

// Remove login error messages
function remove_login_error_msg() {
 return 'Curious...?';
}
add_filter( 'login_errors', 'remove_login_error_msg' );

Disable XML-RPC

XML-RPC is a protocol used for WordPress remote login (among others). Large-scale attacks used to exploit a vulnerability in its system.multicall() method, which allows hundreds of login attempts to be made with a single call, increasing the chances of finding access before being blocked.

You can completely disable XML-RPC (and delete a line of information in the HTML page headers) via these two lines in the functions.php file:

// Disable XMLRPC
add_filter('xmlrpc_enabled', '__return_false');
remove_action('wp_head', 'rsd_link');

Alternatively, you can also protect the xmlrpc.php file located in the WordPress root folder with an Apache directive in the .htaccess file:

# Protect xmlrpc.php
<Files xmlrpc.php>
 Order allow,deny
 Deny from all
</Files>

Note: If you use JetPack, it seems that using the protect module protects you against this type of attack and makes these codes useless (at one time, JetPack required the XML-RPC protocol to work, but some say that it is no longer the case today).

Hide version numbers

Why make it easier for hackers by providing them with information about the system, and thus the system vulnerabilities they can exploit?

Warning: Experienced hackers or advanced scripts WILL find out WordPress (or plugins) version numbers in the end (using other means), but it will still eliminate some of the attacks. And since it’s simple to implement, let’s remove or hide this useless (to the system) information.

Page header and RSS feed

WordPress inserts in its page headers (<head> tag) some items that are useless for the functioning of your site, but exploitable by any bot/hacker, in particular WordPress and CSS/JS included files version numbers.

To remove these version numbers from the headers (as well as from the RSS feed), insert these PHP code into the functions.php file of your child theme:

// Remove WordPress version number
remove_action('wp_head', 'wp_generator');

// From RSS feed
function remove_version_wp() {
 return '';
}
add_filter(‘the_generator’, ‘remove_version_wp’);

// Remove included css/js version numbers
function remove_ver_css_js( $src ) {
 if ( strpos( $src, 'ver=' . get_bloginfo( 'version' ) ) )
  $src = remove_query_arg( 'ver', $src );
 return $src;
}
add_filter( 'style_loader_src', 'remove_ver_css_js', 9999 );
add_filter( 'script_loader_src', 'remove_ver_css_js', 9999 );

Readme, license, changelog files …

These files, located at the root folder of your site or your themes/plugins, also contain usable information. Deleting them manually is not the right solution, since they will be recreated with each update of your CMS.

So let’s protect their access once and for all with a directive in the .htaccess file:

# Protect info files
<FilesMatch "^(readme.html|readme.txt|README.txt|README.md|changelog.txt|license.txt|LICENCE.txt|LICENCE)">
 Order allow,deny
 Deny from all
</FilesMatch>

Server version information

On some error pages returned by your server, you can find version information about used software and modules.

Here is how to tell Apache not to display them in the .htaccess file:

# Hide server information
ServerSignature Off

The entire code

function.php

/******************************************/
/*           WORDPRESS SECURITY           */
/******************************************/

// Remove author's login from comments
function remove_comment_author_class( $classes ) {
 foreach( $classes as $key => $class )
  if(strstr($class, 'comment-author-' ))
   unset( $classes[$key] );
 return $classes;
}
add_filter( 'comment_class' , 'remove_comment_author_class' );

// Remove login error messages
function remove_login_error_msg() {
 return 'Et alors...?';
}
add_filter( 'login_errors', 'remove_login_error_msg' );

// Disable XMLRPC
add_filter('xmlrpc_enabled', '__return_false');
remove_action('wp_head', 'rsd_link');

// Remove WordPress version number
remove_action('wp_head', 'wp_generator');

// From RSS feed
function remove_version_wp() {
 return '';
}
add_filter(‘the_generator’, ‘remove_version_wp’);

// Remove included css/js version numbers
function remove_ver_css_js( $src ) {
 if ( strpos( $src, 'ver=' . get_bloginfo( 'version' ) ) )
  $src = remove_query_arg( 'ver', $src );
 return $src;
}
add_filter( 'style_loader_src', 'remove_ver_css_js', 9999 );
add_filter( 'script_loader_src', 'remove_ver_css_js', 9999 );

.htaccess

In this file, in addition to the previous code snippets, you may need to enable the FollowSymLinks option, essential for mod_rewrite module operation and WordPress permalink customization (if this is not done in your hosting server configuration).

######################################
#         WORDPRESS SECURITY         #
######################################

# Follow symbolic links
Options +FollowSymLinks

# Protect folder content
Options -Indexes

# Hide server information
ServerSignature Off

# Protect wp-config.php
<files wp-config.php>
 Order allow,deny
 Deny from all
</files>

# Protect .htaccess and .htpasswds
<Files ~ "^.*\.([Hh][Tt][AaPp])">
 Order allow,deny
 Deny from all
</Files>

# Hide authors pages
<IfModule mod_rewrite.c>
 RewriteCond %{QUERY_STRING} ^author=([0-9]*)
 RewriteRule .* - [F]
</IfModule>

# Protect xmlrpc.php
<Files xmlrpc.php>
 Order allow,deny
 Deny from all
</Files>

# Protect info files
<FilesMatch "^(readme.html|readme.txt|README.txt|README.md|changelog.txt|license.txt|LICENCE.txt|LICENCE)">
 Order allow,deny
 Deny from all
</FilesMatch>

The database tables prefix myth

Changing the prefix of WordPress tables in the database will not protect your site better (a simple SQL query is enough to find it when you have access to the database). On the contrary, it is a risky procedure that can very easily take it out of service.

And if many plugin editors still offer this service, it is only for marketing reasons.

Note: If WordPress proposes to modify this prefix during install process, it is simply to allow the installation of several sites using the same database.

Conclusion

This list of recommendations is obviously not exhaustive and will not guarantee the total security of your WordPress blog, but it will protect you from a number of vulnerabilities.

It is also very simple to put into practice and generic enough to match all WordPress sites.

Image credit: Background downloaded on Freepik

This article was largely translated by DeepL and Google Translate, then corrected by me.