WordPress Security Hardening with .htaccess Rules

This way of protecting your website uses a combination of htaccess rewrite rules to harden WordPress sites at the server level, plugins to harden WP sites at the site level i.e internally, and plugins to detect changes to files and the database SQL injections.

There’s no need to be squeamish. Toughening WordPress is easier than it looks but there are caveats:

  • No site or server can ever be 100% secure from malicious bots, hacker scripts or other non desirables.
  • Denial of Service (DoS) attacks require a very different method of site protection.

The site defence methods shown here have been checked on hundreds of servers and have been well researched. They work.

Keep in mind that if your site is hacked even after you have used the methods outlined here then none of JournalXtra, me, the plugin developers or any of the originators of any of the scripts and htaccess directives outlined here can be held liable.

A bonus to using the WP security measures shown here is that your site’s 404 errors will be reduced considerably.

WordPress Security Toughening

Securing WordPress is best done during the site’s installation. There are a couple of changes to the way WordPress functions that are easier to implement immediately after installation. These changes make it difficult to launch automated attacks.

This WordPress security system is split into 7 parts:

  1. Bug fixing
  2. Security plugins
  3. Server protection & 404 reduction
  4. More WordPress 404 reduction
  5. Defences against query string, remote file inclusion, XSS & SQL injection and fingerprinting attacks.
  6. Protecting wp-login.php, wp-register.php and wp-signup.php.
  7. Disabling PHP easter eggs, proxy POST requests, bogus graphic exploits and more remote file inclusion (RFI) protection.

The protection offered by part 5 is strongest when implemented almost immediately after WordPress installation.

Part One: Bug Fixing

Update your plugins, update your themes, visit the update page and click update/reinstall WordPress. Updates often fix vulnerabilities. Securing your server and site will not protect WordPress much if your scripts are insecure.

Updating/Reinstalling WordPress, themes and plugins will overwrite any modified files. This will remove any malicious content placed within them as well as remove any customizations you’ve made to them.

Before updating, backup any customizations made to files so you can reintroduce them afterwards.

Part Two: The Plugins

This is the quick section. Download and install the following plugins. Do not activate them just yet.

The 4 plugins shown here are enough to protect a site and keep 404 pages to a minimum. If you want to read more about these plugins or learn about more WP security plugins then visit this JournalXtra post.

Part Three: Toughen the Server

If you use an Apache (Linux) server that has htaccess files enabled, you can change the server’s global configurations on a per directory basis using directives in htaccess files.

The short explanation: Apache reads every htaccess file in every directory it encounters on its way to finding a requested file such as htaccess in the root directory when index.php in example.com/index.php is requested or both the root htaccess and subdirectory htaccess when example.com/sudirectory/index.php is requested.

Apache obeys all directives in all htaccess files it encounters, even those that negate each other. Precedence is given to the directives in the most recent htaccess read as Apache serves the requested file.

Put the following htaccess directives in you server’s top-most htaccess file. For the majority of servers, this will be in the directory above public_html. If your server doesn’t call the top most directory public_html, just put the following directives in an htaccess file at the highest level directory your server allows.

If an htaccess file already exists, just add these directives to the bottom of the file. If the htaccess file doesn’t exist, create it. htaccess is a dot-file and the full stop in front of the file name is important so if you create one, name it .htaccess.

Always keep an empty new line at the end of your htaccess files. Some scripts write to htaccess files without starting a new line. This causes server errors because new data is appended to the last line of existing data.

## Deny directory browsing
Options +FollowSymLinks All -Indexes
## Enable the rewrite engine
RewriteEngine on
## Disable the server signature
ServerSignature Off

## Block known malicious hostnames and IP addresses
## Full blacklist available from https://journalxtra.com/blacklists/
order allow,deny
deny from 107.20.5.217
deny from 108.60.0.1
deny from 109.230.217.94
deny from example.com
allow from all

## Block known bad user agents, bots and potentially malicious request methods
## The following list may include bots that no longer exist or that are not a problem for your site.
## The list will always be incomplete and it is therefore wise to
## follow discussions on one of the many "security" mailing lists or on a forum
## such as http://www.webmasterworld.com/search_engine_spiders/
## It is also unwise to rely on this list as your ONLY security mechanism.

## White-list allowed sites
## Better to use IP addresses than {REMOTE_HOST} name
## RewriteCond %{REMOTE_HOST} !wordpress\.org$ [NC]
## White-list WordPress and Automatic servers
RewriteCond %{REMOTE_ADDR} !216\.65\.19\.4
RewriteCond %{REMOTE_ADDR} !64\.34\.174\.135
RewriteCond %{REMOTE_ADDR} !64\.34\.177\.159
RewriteCond %{REMOTE_ADDR} !66\.135\.48\.
RewriteCond %{REMOTE_ADDR} !69\.0\.231\.112
RewriteCond %{REMOTE_ADDR} !69\.170\.134\.
RewriteCond %{REMOTE_ADDR} !69\.174\.248\.140
RewriteCond %{REMOTE_ADDR} !72\.233\.104\.124
RewriteCond %{REMOTE_ADDR} !72\.233\.104\.98
RewriteCond %{REMOTE_ADDR} !72\.233\.56\.138
RewriteCond %{REMOTE_ADDR} !72\.233\.56\.139
RewriteCond %{REMOTE_ADDR} !72\.233\.69\.14
RewriteCond %{REMOTE_ADDR} !74\.200\.244\.
RewriteCond %{REMOTE_ADDR} !74\.200\.247\.188
RewriteCond %{REMOTE_ADDR} !76\.74\.159\.137
RewriteCond %{REMOTE_ADDR} !76\.74\.248\.
RewriteCond %{REMOTE_ADDR} !76\.74\.254\.126
## White list Disqus
RewriteCond %{REMOTE_ADDR} !75\.126\.109\.204
RewriteCond %{REMOTE_ADDR} !74\.86\.190\.242
RewriteCond %{REMOTE_ADDR} !173\.192\.61\.226
## White-list Livefyre
RewriteCond %{REMOTE_ADDR} !50.19.5.163
# White-list localhost
RewriteCond %{HTTP_HOST} !^(127\.0\.0\.1|localhost) [NC]
## Block bad user agents
RewriteCond %{HTTP_USER_AGENT} BlackWidow [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Bot\ mailto:[email protected] [NC,OR]
RewriteCond %{HTTP_USER_AGENT} ChinaClaw [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Custo [NC,OR]
RewriteCond %{HTTP_USER_AGENT} DISCo [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Download\ Demon [NC,OR]
RewriteCond %{HTTP_USER_AGENT} EirGrabber [NC,OR]
RewriteCond %{HTTP_USER_AGENT} EmailSiphon [NC,OR]
RewriteCond %{HTTP_USER_AGENT} EmailWolf [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Express\ WebPictures [NC,OR]
RewriteCond %{HTTP_USER_AGENT} ExtractorPro [NC,OR]
RewriteCond %{HTTP_USER_AGENT} EyeNetIE [NC,OR]
RewriteCond %{HTTP_USER_AGENT} FlashGet [NC,OR]
RewriteCond %{HTTP_USER_AGENT} GetRight [NC,OR]
RewriteCond %{HTTP_USER_AGENT} GetWeb! [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Go!Zilla [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Go-Ahead-Got-It [NC,OR]
RewriteCond %{HTTP_USER_AGENT} GrabNet [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Grafula [NC,OR]
RewriteCond %{HTTP_USER_AGENT} HMView [NC,OR]
RewriteCond %{HTTP_USER_AGENT} HTTrack [NC,OR]
RewriteCond %{HTTP_USER_AGENT} HTTrack [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Image\ Stripper [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Image\ Sucker [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Indy\ Library [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Indy\ Library [NC,OR]
RewriteCond %{HTTP_USER_AGENT} InterGET [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Internet\ Ninja [NC,OR]
RewriteCond %{HTTP_USER_AGENT} JOC\ Web\ Spider [NC,OR]
RewriteCond %{HTTP_USER_AGENT} JetCar [NC,OR]
RewriteCond %{HTTP_USER_AGENT} LeechFTP [NC,OR]
RewriteCond %{HTTP_USER_AGENT} MIDown\ tool [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Mass\ Downloader [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Mister\ PiX [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Navroad [NC,OR]
RewriteCond %{HTTP_USER_AGENT} NearSite [NC,OR]
RewriteCond %{HTTP_USER_AGENT} NetAnts [NC,OR]
RewriteCond %{HTTP_USER_AGENT} NetSpider [NC,OR]
RewriteCond %{HTTP_USER_AGENT} NetZIP [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Net\ Vampire [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Octopus [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Offline\ Explorer [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Offline\ Navigator [NC,OR]
RewriteCond %{HTTP_USER_AGENT} PageGrabber [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Papa\ Foto [NC,OR]
RewriteCond %{HTTP_USER_AGENT} ReGet [NC,OR]
RewriteCond %{HTTP_USER_AGENT} RealDownload [NC,OR]
RewriteCond %{HTTP_USER_AGENT} SiteSnagger [NC,OR]
RewriteCond %{HTTP_USER_AGENT} SmartDownload [NC,OR]
RewriteCond %{HTTP_USER_AGENT} SuperBot [NC,OR]
RewriteCond %{HTTP_USER_AGENT} SuperHTTP [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Surfbot [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Teleport\ Pro [NC,OR]
RewriteCond %{HTTP_USER_AGENT} TurnitinBot [NC,OR]
RewriteCond %{HTTP_USER_AGENT} VoidEYE [NC,OR]
RewriteCond %{HTTP_USER_AGENT} WWWOFFLE [NC,OR]
RewriteCond %{HTTP_USER_AGENT} WebAuto [NC,OR]
RewriteCond %{HTTP_USER_AGENT} WebCopier [NC,OR]
RewriteCond %{HTTP_USER_AGENT} WebFetch [NC,OR]
RewriteCond %{HTTP_USER_AGENT} WebGo\ IS [NC,OR]
RewriteCond %{HTTP_USER_AGENT} WebLeacher [NC,OR]
RewriteCond %{HTTP_USER_AGENT} WebReaper [NC,OR]
RewriteCond %{HTTP_USER_AGENT} WebSauger [NC,OR]
RewriteCond %{HTTP_USER_AGENT} WebStripper [NC,OR]
RewriteCond %{HTTP_USER_AGENT} WebWhacker [NC,OR]
RewriteCond %{HTTP_USER_AGENT} WebZIP [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Web\ Image\ Collector [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Web\ Sucker [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Website\ Quester [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Website\ eXtractor [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Widow [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Xaldon\ WebSpider [NC,OR]
RewriteCond %{HTTP_USER_AGENT} Zeus [NC,OR]
RewriteCond %{HTTP_USER_AGENT} archiverloader [NC,OR]
RewriteCond %{HTTP_USER_AGENT} casper [NC,OR]
RewriteCond %{HTTP_USER_AGENT} clshttp [NC,OR]
RewriteCond %{HTTP_USER_AGENT} cmsworldmap [NC,OR]
RewriteCond %{HTTP_USER_AGENT} curl [NC,OR]
RewriteCond %{HTTP_USER_AGENT} diavol [NC,OR]
RewriteCond %{HTTP_USER_AGENT} dotbot [NC,OR]
RewriteCond %{HTTP_USER_AGENT} eCatch [NC,OR]
RewriteCond %{HTTP_USER_AGENT} email [NC,OR]
RewriteCond %{HTTP_USER_AGENT} extract [NC,OR]
RewriteCond %{HTTP_USER_AGENT} flicky [NC,OR]
RewriteCond %{HTTP_USER_AGENT} grab [NC,OR]
RewriteCond %{HTTP_USER_AGENT} harvest [NC,OR]
RewriteCond %{HTTP_USER_AGENT} jakarta [NC,OR]
RewriteCond %{HTTP_USER_AGENT} java [NC,OR]
RewriteCond %{HTTP_USER_AGENT} kmccrew [NC,OR]
RewriteCond %{HTTP_USER_AGENT} larbin [NC,OR]
RewriteCond %{HTTP_USER_AGENT} libwww [NC,OR]
RewriteCond %{HTTP_USER_AGENT} miner [NC,OR]
RewriteCond %{HTTP_USER_AGENT} nikto [NC,OR]
RewriteCond %{HTTP_USER_AGENT} pavuk [NC,OR]
RewriteCond %{HTTP_USER_AGENT} pcBrowser [NC,OR]
RewriteCond %{HTTP_USER_AGENT} planetwork [NC,OR]
RewriteCond %{HTTP_USER_AGENT} pycurl [NC,OR]
RewriteCond %{HTTP_USER_AGENT} python [NC,OR]
RewriteCond %{HTTP_USER_AGENT} scan [NC,OR]
RewriteCond %{HTTP_USER_AGENT} skygrid [NC,OR]
RewriteCond %{HTTP_USER_AGENT} tAkeOut [NC,OR]
RewriteCond %{HTTP_USER_AGENT} wget [NC,OR]
RewriteCond %{HTTP_USER_AGENT} winhttp [NC]
## Note: The final RewriteCond must NOT use the [OR] flag.
## Return 403 Forbidden error.
RewriteRule .* - [F]

## Protect sensitive files from client-side viewing.
<FilesMatch "^(wp-config\.php|php\.ini|php5\.ini|install\.php|php\.info|readme\.html|bb-config\.php|\htaccess|readme\.txt|error_log|error\.log|PHP_errors\.log|\.svn)">
 Deny from all
</FilesMatch>

Read the comments in the above blacklist. The URL to a bigger blacklist is mentioned in them.

When installing software that uses a script called install.php, such as WordPress, you must disable the final htaccess directive until the installation is complete. Temporarily deactivate it by changing “Deny from all” to “Allow from all”.

An alternative way to disable the protection of install.php is to place the following directive into the root htaccess of the site being installed:

<files install.php>
Order allow,deny
Allow from all
</files>

Remember to remove it once the installation is complete.

Part Four: Reduce WordPress 404 Errors

404 errors are caused by missing files:

  • Files, posts, pages and directories that once existed but which you deleted
  • Files, posts, pages and directories that never existed on your server but which senile search engine bots, plugins and malicious scripts try to find anyway
  • Documents and directories that do exist but which have been backlinked to with bad characters in the backlinks so that your server can’t find them.

WordPress serves virtual files, mostly. Uploads are non virtual but the actual posts and pages that people see only exist in the WP database unless served from a file cache by caching plugins.

When your server receives a request for a directory or document, the server reads the htaccess files along the document path until it reaches the document.

When the server encounters the WordPress htaccess rewrite rules it is instructed to check whether the requested directory is a real directory and whether the requested document is a real document. If it is not a document that exists on the server, the server (Apache) is instructed to load the WordPress index.php script. This loads WordPress and WordPress then fulfils the URL request with a virtual file or cached version of it. This is also the reason the final directives in a WordPress htaccess file must always be the WordPress rewrite rules. Ditto for other content management systems.

Once WordPress takes the URL, it processes the URL according to its own internal rewrite directives that are controlled by a PHP script.

If you’ve ever wondered how WordPress processes URLs, here is where your wondering ends: the process flows something like this:

  • A request is made to the server in the form of a URL.
  • The server processes the request according to global directives set in httpd.conf, virtual host files, php.ini and other configuration files.
  • Apache further processes the URL request as defined in htaccess files in every directory encounted while fetching the requested directory or document.
  • Apache reaches the WordPress site’s root htaccess file and eventually reads the WordPress rewrite rules.
  • WordPress takes over the URL and begins processing the request according to its own internal directives if the request is for a non real file.

If the URL request is for a “real” document or directory stored on the server then Apache serves that document or directory. If it’s not a real file then WordPress handles the URL and tries to serve a virtual document. If WordPress can’t find the document, a 404 error page is served.

We can limit the number of 404s by altering the URL before WordPress receives it, by serving replacement files when we know a missing requested file is a type that WordPress doesn’t serve as a virtual file, and by using WordPress to match requests against suitable alternatives to serve.

Disable any security plugins that are presently active in your WordPress site.

Activate the Permalink Finder plugin that you installed earlier. This plugin serves a suitable replacement file when the requested file cannot be found.

Open your WordPress site’s htaccess file. It will be the one in the site’s home directory that also stores the wp-config.php script. This directory is known as the site’s root directory. Make a backup of it because you need to delete most of its current content.

Except for the WordPress rewrite rules, remove any htaccess directives that are currently in your htaccess file. The WordPress rewrite rules are sandwiched between # BEGIN WordPress and # END WordPress.

Put the following directives into the htaccess file. Put them above the line that reads # BEGIN WordPress. Remember to keep a new line between these directives and # BEGIN WordPress:

DirectoryIndex index.php

## These two are not always needed.
Options +FollowSymlinks
RewriteEngine on

## Standardize URLs - Remove www hostname.
## Uncomment these three lines if you wish to remove the www
## WordPress should do this automatically so this is not usually needed.
## This can cause images to vanish on old sites.
# RewriteCond %{HTTP_HOST} ^http://www\.(.*)$ [NC,OR]
# RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC]
# RewriteRule (.*) http://%1 [R=301,L]

## GENERAL 404 PREVENTION AND IDIOT REDIRECTION

## Remove the trailing slash from all file requests (otherwise Apache serves a 404)
## Directories should have a trailing slash, files should not.
## If you use a content management system that produces dynamic pages (like WP), comment out the third line and uncomment the 4th line.
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} /$ [NC]
## Remove slash from all files
# RewriteCond %{REQUEST_FILENAME} \.(\w+)$ [NC]
## Remove slash from image files only
RewriteCond %{REQUEST_FILENAME} \.(gif|jpe?g|png|bmp|tiff|xcf|psd)$ [NC]
RewriteRule ^(.*)/$ $1 [R=301,L]

## Redirect requests ending with bad characters as caused by poor backlinks and rogue bots.
## Make sure your site functions properly once these rules have been added. Remove them if it does not.
## Whitelist Site Search Requests
RewriteCond %{QUERY_STRING} !s\= [NC]
RewriteCond %{QUERY_STRING} !cx\= [NC]
## Do Redirect
RewriteCond %{QUERY_STRING} (.*)('|%27|"|%22|\\|%5C|\ |%20)$ [NC,OR]
RewriteCond %{REQUEST_URI} (.*)('|%27|"|%22|\\|%5C|\ |%20)$ [NC,OR]
RewriteCond %{QUERY_STRING} (.*)/(\+)/$ [NC,OR]
RewriteCond %{REQUEST_URI} (.*)/(\+)/$ [NC]
RewriteRule .* %1 [R=301,L]

## Fix missing image requests
## Change the bottom line to specify the replacement image and its location
## This will show up any missing image files in your site so watch for them and replace them as necessary.

RewriteCond %{REQUEST_URI} !/files/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} \.(gif|jpe?g|png|bmp|tiff|xcf|psd)$ [NC]
RewriteRule .* example/image.jpg [L]

Save the file. Close it. Reload your site.

Read the comments because they give further information about using the above directives properly. The final part replaces missing images with a one-size-fits-all stand-in. It requires you to upload an image to your server and place the path to the image in the rewrite rule. For example, if you use a file called dilbert.jpg in wp-content/uploads/dilbert.jpg then you would replace example/image.jpg with wp-content/uploads/dilbert.jpg.

Make sure the replacement file is not large in terms of data size. Try to keep it under 100kb.

Activate the Redirection plugin so you can monitor and fix 404s that the above methods do not resolve.

404s can be telltales signatures of attempts to hack a site. Consider the 404s you find and add originating IPs to your topmost htaccess file’s denial directives if they look like attempts to hack your site.

Part Five: Defend Against Query String Attacks and RFI Attacks

Activate the rest of the plugins we installed earlier. Do it in the following order:

  • Better WP Security

If you intend to change your admin username and database table prefix using Better WP Security, do so before activating the next two plugins.

  • Wordfence

The two other plugins (Redirection and Permalink Finder) should already be active.

Go through the settings for each plugin and configure them as suited to your environment and needs.

Both Wordfence and Better WP Security duplicate some features. Where features coincide, use the Wordfence features in preference to Better WP Security.

Better WP Security

Click the Security tab in your WordPress dashboard and follow its suggestions. It will write to your htaccess file. So when we next edit your htaccess file, make sure to reopen it for editing or you’ll end up overwriting vital security directives placed by Better WP Security.

It is usually safe to change your admin username and your WordPress database table prefix. I’ve made these changes to many mature sites and never had it go wrong. That doesn’t mean it can’t go wrong so back up your database before you change them.

You can change the name of your wp-content directory. This blocks most automated hacks that use bots to probe plugin and theme files in the wp-content directory for vulnerable files. Probing bots will not find what they are looking for if  wp-content doesn’t exist. It is good to change the name of wp-content but only do it when working on a fresh WordPress install with no media uploads or if you know how to edit the WordPress database to correct URL references.

Always rename the wp-content directory for fresh WP installs that have no posts, pages, internal links, uploads or other content added to them since installation.

Better WP Security makes all the required wp-config.php edits needed to rename the wp-content directory and it renames the directory but it doesn’t edit your database.

Renaming the wp-content directory of mature sites can be complicated. It breaks internal links within posts and post meta data. It breaks file hotlinks to images and downloads etc.. (might not be such a bad thing). If you rename the wp-content directory of a mature site you will need to edit your database to replace all instances of wp-content/ (that forward slash is important) with the new directory name e.g if you rename wp-content/ to file-uploads-xyz/ then you  would replace wp-content/ with file-uploads-xyz/. You must also be careful not to inadvertently give the game away by renaming any mentions of the old wp-content directory within posts and comments.

When editing the WordPress database, remember that entries in the wp_options table are in a format that states how many characters each entry has. WP ignores the entry when the number of characters do not match the number expected by WordPress.

When you rename the wp-content directory you must also edit any instances of wp-content within you site’s htaccess file to reflect the directory’s new name.

Disabling the theme and plugins editor in the backend can cause some plugins to stop functioning. Quick Cache is one of these plugins.

Wordfence

Wordfence is a newcomer to the WP security scene. It scans site files to check for malicious content, compares WordPress core files and plugins to their originals stored in the plugin repository and suggests fixes for faults discovered.

This plugin performs a few other security checks too. You can read more about  it at wordfence.com.

Wordfence has low impact on server resources. It uses an external server to perform some of its work.

If You Don’t Use Better WP Security to Rename wp-content

You can still protect your site from most automated probes of wp-content and against attempts to use query strings to upload files and inject code into the WordPress database through attempts to hack scripts in the wp-content directory.

Part of this is easy to  implement, part of this it tricky. How many of these snippets you use depends on what you use your site for, how often you intend to install plugins and how secure you want your site to be. It’s all explained in the small print.

All these snippets go above the line # BEGIN WordPress. They should be placed in the order they are listed. If you skip a block of directives, place the next block you use after the last block you used. Keep an empty line between each block to make it easier for you to read your htaccess file later.

How many themes do you actively use on your site?

General advice is to remove unused themes. Delete them. This is not always practical and often it’s a chore to reinstall a theme. Why delete it when you can simply block access to its directory. Indeed, reverse that notion: block access to all themes in the wp-content/themes/ directory but whitelist access to only the theme your site actively uses.

## Deny Access to All But the Active Theme
## Whitelist the active theme. Change "active-theme" to the name of your active theme's directory
RewriteCond %{REQUEST_URI} !.*/wp-content/themes/active-theme/.* [NC]
RewriteCond %{THE_REQUEST} !.*/wp-content/themes/active-theme/.* [NC]
## Block access to any directory or document not in the active-theme's directory.
RewriteCond %{REQUEST_URI} ^.*/wp-content/themes/.* [NC,OR]
RewriteCond %{THE_REQUEST} ^.*/wp-content/themes/.* [NC]
RewriteRule .* - [F,L]

How many plugins do you actively use?

This one is a little more tricky. It requires you to white-list access to the directories of every plugin your site actively uses.

Just as done for the theme’s directory, add additional lines to the whitelist to permit access to your site’s active plugins only.

## Deny Access to All But Active Plugins
## Whitelist the active plugins.
## Change "active-plugin" in the next two lines to the name of the directory used by one of your active plugins.
## Copy the two lines, paste them below the first two lines and use them to whitelist another plugin. Repeat until all active plugins have been white-listed.

RewriteCond %{REQUEST_URI} !.*/wp-content/plugins/active-plugin/.* [NC]
RewriteCond %{THE_REQUEST} !.*/wp-content/plugins/active-plugin/.* [NC]

## Block access to any directory or document not in the active-theme's directory.
RewriteCond %{REQUEST_URI} ^.*/wp-content/plugins/.* [NC,OR]
RewriteCond %{THE_REQUEST} ^.*/wp-content/plugins/.* [NC]
RewriteRule .* - [F,L]

Block Query Strings

Ever seen a URL that has a question mark in it? Maybe one that looks like example.com/index.php?$login=true.

Anything after the question mark forms part of a query string. Query strings allow URLs to be used to give values to variables in scripts (a variable such as $login).

As with anything useful, query strings are vulnerable to misuse. Well written scripts and plugins sanitize query string values to mitigate hacking potential but even security minded script developers make mistakes.

You need to be very careful with this next set of directives. This set will prevent updates to plugins, themes and WordPress while-so-ever it is in your htaccess file. It will also stop some plugins from functioning properly. Despite the downside, they block query string attacks and are very, very useful for owners of sites that are left to run without much maintenance.

There are two forms of this directive set. The first is less stringent and will only block query strings. The second is extreme and removes anything after a question mark within a URL even if it is not a query string.

Both these methods require any plugins that use query strings during their functioning to be whitelisted. Use only one method.

Gentle Query String Stripper

## Remove anything after a question mark (including the question mark) in a query string sent to scripts in wp-content/
## IMPORTANT!!! Plug-in and theme whitelist essential.

## Whitelist plugins that use query strings in their functioning
## You will need to experiment to discover which ones use query-strings
## Change "active-plugin" in the next two lines to the name of the directory used by an active plugin that uses query strings
## Copy the two lines, paste them below the first two lines and use them to whitelist another plugin. Repeat until all query string using plugins have been white-listed.

RewriteCond %{REQUEST_URI} !.*/wp-content/plugins/active-plugin/.* [NC]
RewriteCond %{THE_REQUEST} !.*/wp-content/plugins/active-plugin/.* [NC]

## Run Filter strip query strings
RewriteCond %{QUERY_STRING} .*/wp-content/.* [NC]
RewriteRule ^(.*)$ /$1? [R,L]

Extreme Query String Stripper

## Remove anything after a question mark (including the question mark) anywhere in a URL request sent to scripts in wp-content/
## IMPORTANT!!! Plug-in and theme whitelist essential.

## Whitelist plugins that use query strings in their functioning
## You will need to experiment to discover which ones use query-strings.
## Change "active-plugin" in the next two lines to the name of the directory used by an active plugin that uses query strings
## Copy the two lines, paste them below the first two lines and use them to whitelist another plugin. Repeat until all query string using plugins have been white-listed.

RewriteCond %{REQUEST_URI} !.*/wp-content/plugins/active-plugin/.* [NC]
RewriteCond %{THE_REQUEST} !.*/wp-content/plugins/active-plugin/.* [NC]

## Run Filter strip anything after a question mark in the URL
RewriteCond %{THE_REQUEST} ^.*/wp-content.*[a-z0-9]+\ /([^?#\ ]*)\?[^\ ]*\ HTTP/ [NC]
RewriteRule ^(.*)$ /$1? [R,L]

An Alternative to the Above

A much less restrictive method to protect against RFI attacks is to use the following rewrite directives. Either use them instead of the two methods shown above or in synergy with them.

This is not the most recommendable method but it gets the job done in most cases. It works by blocking query stings and URI requests that refer to files stored on remote hosts that match commonly used hostnames such as picasa.com and blogger.com.

## Block requests with specific hostnames in their query string.
RewriteCond %{QUERY_STRING} ^.*(http|https|ftp)(%3A|:)(%2F|/)(%2F|/)(w){0,3}.?(blogger|picasa|blogspot|tsunami|petapolitik|wordpress\.com|kkc|start-thegame).*$ [NC,OR]
RewriteCond %{THE_REQUEST} ^.*(http|https|ftp)(%3A|:)(%2F|/)(%2F|/)(w){0,3}.?(blogger|picasa|blogspot|tsunami|petapolitik|wordpress\.com|kkc|start-thegame).*$ [NC]
RewriteRule .* index.php [F,L]

If you use Better WP Security to Rename WP-Content

Block attempts to probe the non existent wp-content directory. Some plugins require that the wp-content directory exists. They don’t use it but they look for it just the same. So recreate the directory wp-content and create two directories within it: one called themes and another called plugins. Add a blank htaccess file into each of those new directories. Now block client side access to the wp-content directory. Only do this if you’ve renamed your real wp-content directory.

## Block Access to wp-content
RewriteCond %{REQUEST_URI} ^.*/wp-content/.* [NC,OR]
RewriteCond %{QUERY_STRING} ^.*/wp-content/.* [NC,OR]
RewriteCond %{THE_REQUEST} ^.*/wp-content/.* [NC]
RewriteRule .* - [F,NS,L]

I said it would be tricky. If you need help, post a request in the comments section or send me a private message with the contact form.

Part Five: If Registration is Disabled

Many bots and hackers attempt to create user accounts with WordPress blogs or to hack into WordPress sites through the signup, registration and login pages. I prefer to disable registration and completely deny access to the registration and signup scripts.

I use external authentication systems such as Disqus, OpenID, Livefyre and Social Login to enable commenting. I recommend you do likewise then use these directives to deny access to wp-signup.php and wp-register.php. If you want to be really secure, deny access to wp-login.php except from your own IP address/es.

## Registration is disabled so...
## White-list your own IP address/es. Change the numbers!!
RewriteCond %{REMOTE_HOST} !1.1.1.1
RewriteCond %{REMOTE_HOST} !2.2.2.2
## Uncomment to deny access to wp-login.php
# RewriteCond %{REQUEST_URI} wp-login\.php [NC,OR]
# RewriteCond %{QUERY_STRING} wp-login\.php [NC,OR]
# RewriteCond %{THE_REQUEST} wp-login\.php [NC,OR]
## Leave uncommented to deny access to wp-signup.php and wp-register.php
RewriteCond %{REQUEST_URI} wp-signup\.php [NC,OR]
RewriteCond %{QUERY_STRING} wp-signup\.php [NC,OR]
RewriteCond %{THE_REQUEST} wp-signup\.php [NC,OR]
RewriteCond %{REQUEST_URI} wp-register\.php [NC,OR]
RewriteCond %{QUERY_STRING} wp-register\.php [NC,OR]
RewriteCond %{THE_REQUEST} wp-register\.php [NC]
RewriteRule .* - [F,NS,L]

Part Seven: General Protection

These htaccess directives protect your site from PHP fingerprinting, POST requests sent through proxies, bogus image file uploads and executable file uploads. Place them in your WordPress site’s root htaccess file. Again, place them above the line that reads # BEGIN WordPress.

## Disallow PHP Easter Eggs (can be used in fingerprinting attacks to determine
## your PHP version). See http://www.0php.com/php_easter_egg.php and
## http://osvdb.org/12184 for more information
RewriteCond %{QUERY_STRING} \=PHP[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12} [NC]
RewriteRule .* - [F,L]

## Deny POST requests sent through proxies
RewriteCond %{REQUEST_URI} !^/(search/|site-search/|wp-login.php|wp-admin/|wp-access/plugins/|wp-includes/) [NC]
RewriteCond %{REQUEST_METHOD} =POST
RewriteCond %{HTTP:VIA}%{HTTP:FORWARDED}%{HTTP:USERAGENT_VIA}%{HTTP:X_FORWARDED_FOR}%{HTTP:PROXY_CONNECTION} !^$ [OR]
RewriteCond %{HTTP:XPROXY_CONNECTION}%{HTTP:HTTP_PC_REMOTE_ADDR}%{HTTP:HTTP_CLIENT_IP} !^$
RewriteRule .* - [F,NS,L]

## Deny bogus graphic exploits
RewriteCond %{HTTP:Content-Disposition} \.php [NC]
RewriteCond %{HTTP:Content-Type} image/.+ [NC]
RewriteRule .* - [F,NS,L]

## Move specified script file type uploads to a secure location and rename the file.
## Create a safe upload directory on your server.
## Put the secure no-exec htaccess described at https://journalxtra.com/2011/09/wordpress-security-hardening-htaccess-rules/ into the directory.
## Change the final line of this block to reflect your secure directory's path and name
RewriteCond %{REQUEST_METHOD} ^PUT$ [OR]
RewriteCond %{REQUEST_METHOD} ^MOVE$
RewriteRule (.*)\.(php|pl|py|jsp|htm|shtml|sh)$ /example/$1.noexec [L]
# RewriteRule (%{SCRIPT_NAME}) /example/$1.noexec [L]

The final part of the above snippet, moves uploaded files with the extensions specified in the second-to-last-line to a safe directory location mentioned in the final line. The files are renamed with the extension noexec. Create a directory within the directory of the htaccess file this directive is put into. Change the word “example” in the last two lines to the name of the directory you’ve just created. With this directive, attempts to upload executable scripts will result in the uploaded script being moved to that directory (theoretically).

Create a file called index.txt in the safe directory.

Now create an htaccess file with the following htaccess directives in the safe directory. These directives prevent scripts from being executed and ensure the file index.txt is loaded when someone tries to browse the directory.

DirectoryIndex index.txt

## secure directory by disabling script execution
AddHandler cgi-script .php .pl .py .jsp .asp .htm .html .shtml .sh .cgi .*
Options -ExecCGI

FAQ

Why am I redirected to my home page when I run plugin, theme and WordPress updates?

Either you have set Firewall 2 to block http and https strings in query string parameters or you have used htaccess directives to block query strings. For either case, disable the htaccess query string denial directives and disable the Firewall 2 plugin while you update your site.

Why do I get a forbidden error when I run plugin, theme and WordPress updates?

See the above answer. It’s the same.

I’ve used Better WP Security to rename the wp-content directory. Now some of my plugins return 404 errors. How do I fix this?

There are three possible reasons and solutions for this:

  1. A plugin can be hard-coded to look for its files in the wp-content directory. You’ll need to manually edit the plugin’s files to correct the code. Better still, speak with the plugin’s developer.
  2. Better WP Security might not have been able to edit wp-config.php to tell WordPress about the directory name change. If your theme works and most other plugins work then this reason can be discounted. Otherwise, check the permissions of wp-config.php to ensure it is writable by the server (644).
  3. A plugin might store file location data in your site’s database. You’ll need to manually edit your database to change occurrences of wp-content/ to whatever-you-changed-the-directory-name-to/. Instructions for this are next.

Is there an easy way to edit references to wp-content/ in my database?

Yes there is.

  • Download InterconnecIt’s Database Search and Replace script (searchreplacedb2.php).
  • Upload it to your server.
  • Backup your database. I usually install the backup database and work on the backup.
  • Change the script’s name.
  • Run the script by browsing to it. For example, if you uploaded it to example.com and changed its name to 123.php then you would browse to example.com/123.php.
  • Follow the script’s prompts. Please pay attention to warnings and read the instructions.

A guide to exporting/backing up databases is here at JournalXtra. Read it to prevent heartache from bad backups.

I changed my database table prefix with Better WP Security. Now I’m unable to log into my WordPress dashboard. I get a “You  do not have sufficient permissions to access this page” error. How do I fix it?

I bet you received an error along the lines of “Could not create table” from Better WP Security. Here’s how to get your dashboard back:

  1. Make a note of the new table prefix e.g xyz_
  2. Open wp-config.php and find the line that reads “$table_prefix  =“. Confirm the database prefix here is the one Better WP Security has changed it to e.g xyz_ so $table_prefix  = ‘xyz_’;
  3. Still in wp-config.php, make a note of your site’s database table name. It is shown in the line define(‘DB_NAME’, ‘table_name’); where table_name will be your table’s name.
  4. Open phpMyAdmin, select the database used by your site e.g table_name. Scroll to the bottom of the window pane that displays  the tables, click “check all” then use the dropdown box to select “Optimize”. Repeat but this time use the dropdown box to select “Repair”. Click the Export tab, click Custom, select all tables, select Replace, then export the database. More detailed instructions are available here.
  5. When your table has downloaded, open it in a text editor, find and replace instances of wp_ with your new database table prefix e.g xyz_.
  6. Use phpMyAdmin to import the edited database into your site’s database tables. It should replace the old ones.
  7. Log back into your site.

Your Turn

Do you agree or disagree with any of this post? Do you have additional WordPress security tips? We’d love to hear from you. Use the comment form to express yourself.

Sharing is caring!

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

18 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
18
0
Would love your thoughts, please comment.x
()
x