How to make Wordpress (near) unhackable

One of our bigger name-brand customers who has a custom SLA with us, have a Wordpress site which they were particularly paranoid about. The site in question dealt with their services in IT security, and obviously it would be very bad (embarassing) if this site were to be hacked. As we all know, Wordpress is the biggest target of hackers on the modern web. Publish a Wordpress page, add some plugins and you have practically speaking painted a giant bullseye for hackers to aim for. You are at a very high risk of getting hacked within weeks.

Publish a Wordpress page, add some plugins and you have practically speaking painted a giant bullseye for hackers to aim for. You are at a very high risk of getting hacked within weeks.

We started going through the usual security audits of the site: Identify which plugins were out of date, looking for vulnerabilities and common attack vectors. At one point a harsh reality hit us - it didn't really matter how much we'd prepare and review, we would never be able to absolutely guarantee our client that the site in question would not be hacked. In this case there was zero tolerance: The customer could not afford for the site to be hacked, for any length of time.

Usually keeping everything update and securing Wordpress using our standard Wordpress Lockdown functionality is enough for most use cases. And if all else fails, our automated snapshotting and quick rollback of containers would serve to quickly restore a website to a vanilla state.

In any case, a more sustainable solution was required. Then it occurred to us: What if we could remove Wordpress completely from the equation?

Preventing hacks by disabling PHP code

The most common hacks against Wordpress are automated. We see a lot of brute-force attempts against wp-login, automated exploits of known vulnerabilities against popular plugins etc.. It is extremely rare that a Wordpress site is hacked by an actual human. Most of the time it's just bots.

It is extremely rare that a Wordpress site is hacked by an actual human. Most of the time it's just bots.

The next thing to realize is that what's actually allowing the hackers to gain access to the server: PHP. It is the fundamental fact that the webserver is processing PHP code which opens the door to bad actors. If the server wasn't processing any scripts, and just delivering static files, we would have removed any entrypoint hackers might be able to exploit. In short: If we could turn the site into a completely static HTML website, there would be preciously few attack vectors left.

If we could turn the site into a completely static HTML website, there would be preciously few attack vectors left.

If we could turn the site from a dynamic PHP site to a static HTML site, the only attack vectors left would be the webserver itself (in this case Apache) and the http stack and related services. These components are very secure to begin with, and if the server is configured correctly (as Webdock stacks are) and kept updated then there is very little chance of a hack. What we would be left with would be highly motivated hackers with large resources and access to zero-day exploits. A rare thing to see in the wild. In all reasonable and practical terms, the site would be "unhackable"

Webdock has Wordpress lockdown functionality built-in. This functionality is not as hard-core as described in this article, but rather uses restrictive permissions and htaccess rules in order to prevent modification of files and execution of PHP in your upload folders. Click on this card for more information.

Getting rid of Wordpress - while still using Wordpress

A plan emerged: What if we could generate a static HTML version of the entire website, on the fly?

In this way, we could enable the website administrator to "lock" and "unlock" the website in such a fashion that when they needed to change content, they could temporarily enable the fully-fledged Wordpress site, do their changes and then generate a static version which would go live in its place. This would mean that 99.9% of the time, what was live and publicly accessible on the web to hackers would be a static HTML version of the site with no crappy and insecure PHP plugin-code written by a 14-year old in a basement somewhere. All that would be live would be the generated HTML output. There is a fundamental requirement to this plan however: There could be no form processing or Ajax calls to the backend.

There is a fundamental requirement to this plan however: There could be no form processing or Ajax calls to the backend.

We got lucky with the site in question. It turned out it mostly consisted of articles and "dead" content. The only forms on the site were newsletter subscription forms which posted to a 3rd party service (Marketo) - which meant that the site didn't really need PHP for anything else than just booting up Wordpress and delivering the content. 

If there had been forms submitting data or some ajax functionality, it is conceivable that we could have written small scripts to handle these very specific tasks. This would still have been a vast improvement in security, as having just Wordpress running and publicly available is an inherent security risk.

What we ended up with on paper was something like the following:

  1. Create a shell script which can generate a static HTML version of the live website in a subfolder of /var/www/html and then point the webserver to this subfolder - Our "lockdown" script
     
  2. Create another script which can point the webserver back to the original Wordpress site - Our "unlock" script
     
  3. Create a simple web-interface over HTTPS with basicauth and IP whitelisting which controls these lock and unlock scripts so that the webmaster could unlock the site whenever they need to do changes that need to go live.

When the webserver is pointed to the subfolder containing the static website, then that is all that the world sees: Just a collection of html, css, js and image files. No PHP files will be present, and no way of uploading or executing malicious code on the server. Added bonus: As the webserver is not processing any PHP, this sort of works like a varnish cache: Time To First Byte became 30% faster!

Added bonus: As the webserver is not processing any PHP, this sort of works like a varnish cache: Time To First Byte became 30% faster!

Enter HTTrack ...

The first task was to generate a static copy of a live website on the Linux command line. Turns out this is quite easily achievable with a piece of software called HTTrack. HTTrack can be easily installed on Ubuntu by executing

sudo apt install httrack

With HTTrack it is easy to create a full mirror of a site, including all resources. What you need to watch out for however is pages which may not be directly linked to from anywhere. We had one such page on the site we dealt with here, and that was a "Thank you for your submission" page which the 3rd party newsletter subscription service would redirect back to. This page included the site header and footer however, so it was just a simple matter of defining this as our entrypoint to the site to HTTrack, and then we were sure this page would get picked up in the mirror as well.

Our lockdown script ended up with being something like:

#!/bin/bash

# Prep environment - we place our mirrored site in the /static subfolder

rm -rf /var/www/html/static
mkdir /var/www/html/static
cd /var/www/html/static/

# Mirror the site using httrack -s0 means we ignore any robots.txt directives, 
# the + tells httrack to stick to this domain and not crawl the entire internet

httrack https://website-to-mirror.com/thank-you-for-subscribing +website-to-mirror.com/* -%v -s0

# Make sure we have everything, we saw httrack miss some image files, for some reason

cp -rn /var/www/html/wp-content/uploads/* /var/www/html/static/website-to-mirror.com/wp-content/uploads/

# Clean up files. HTTrack adds index.html to all links, and doesn't know to add https:// properly to links.
# Finally we clean up some comment junk httrack adds to files
find . -name '*.html' -exec sed -i 's%index.html%%g' {} \;
find . -name '*.html' -exec sed -i 's|http://|https://|g' {} \;
find . -name '*.html' -exec sed -i -e :a -re 's/<!-- Mirrored.*?-->//g;/<!-- Mirrored/N;//ba' {} \;
find . -name '*.html' -exec sed -i -e :a -re 's/<!-- Added.*?-->//g;/<!-- Added/N;//ba' {} \;

# Now point webserver config to our new static site
find /etc/apache2/sites-enabled/ -name '*.conf' -exec sed -i "s/DocumentRoot .*/DocumentRoot \/var\/www\/html\/static\/website-to-mirror\.com/" {} \;

# Make sure our control interface is available to the new web root
ln -s /var/www/html/control/ /var/www/html/static/website-to-mirror.com/

# Disable any htaccess rewrites
cat > /var/www/html/static/cyber.deloitte.dk/.htaccess << EOF
<ifmodule mod_rewrite.c>
RewriteEngine Off
</ifmodule>
EOF

# Reload the webserver so that our new static version goes live
/usr/sbin/service apache2 reload

# Drop a file to the control folder in order to mark the site as locked
echo " " > /var/www/html/control/locked

Our modifications here to the webserver config works with an otherwise unmodified Webdock Apache stack with SSL enabled via. Certbot/LetsEncrypt.

Our unlock script is vastly simpler as here we just need to point the webserver back to the Wordpress site:

#!/bin/bash

# Replace in config
find /etc/apache2/sites-enabled/ -name '*.conf' -exec sed -i "s/DocumentRoot .*/DocumentRoot \/var\/www\/html/" {} \;

# Reload webserver
/usr/sbin/service apache2 reload

# Mark as unlocked
rm /var/www/html/control/locked

The control interface

As you can see in the scripts above, our lock and unlock scripts are placed in a /control subfolder. Here we have a php file which only the website administrator can access in order to lock or unlock the site for editing.

As we have full SSL encryption, we decided that BasicAuth (.htaccess + .htpasswd) login along with IP restriction (whitelist) would be adequate to secure this interface.

The system works asynchronously by dropping files in the /control folder which then are detected by a shell script run as a cron job every minute. In this fashion the simple PHP control file does not interact with the system in any "creative" ways which may compromise security. In addition, as the cron job is run as root, all files in our /static can only be read by the webserver and not written to by the webserver, further increasing security. It would be bad however, if any PHP files were placed in the web root and these were owned by root. So watch out for that ...

Our .htaccess file

# IP whitelist - deny any IP's except the ones defined below - replace with your own IP addresses
order deny,allow
deny from all
allow from 0.0.0.0 0.0.0.1 0.0.0.2

# Standard basicauth stuff
AuthType Basic
AuthName "Control"
AuthUserFile /var/www/html/control/.htpasswd
Require valid-user

Our Cronjob shell script

#!/bin/bash

# If file exists remove it and run lock script
if [ -f /var/www/html/control/lockitdown ]
then
 rm /var/www/html/control/lockitdown
 /var/www/html/control/lock.sh
fi

# If file exists, remove it and run unlock script
if [ -f /var/www/html/control/unlockit ]
then
 rm /var/www/html/control/unlockit
 /var/www/html/control/unlock.sh
fi

Our simple lockdown PHP "control panel"

<?php
/*
  Control for lockdown
*/
 $locked = file_exists("/var/www/html/control/locked");

 if ($_REQUEST["action"] == "lock") {
   touch("/var/www/html/control/lockitdown");
   $justlocked = true;
 }
 if ($_REQUEST["action"] == "unlock") {
   touch("/var/www/html/control/unlockit");
   $justunlocked = true;
 }


?>

<h3>Lockdown control</h3>

<p>Current status: <?php echo ($locked ? "Locked" : "Unlocked"); ?></p>
<p><a href="/control/">Check for new status</a></p>

<p></p>

<?php if ($justlocked) { ?>
  <p>Site is locking down. It will be locked down within the next 5 minutes or so. <a href="/control/">Reset form</a></p>
<?php } else if ($justunlocked) { ?>
  <p>Site is unlocking. In about a minute, you can visit <a href="/admin/">WP Admin</a></p>
  <p><a href="/control/">Reset form</a></p>
<?php } else { ?>

<form method="post" action="/control/">

  <input type="hidden" name="action" value="<?php echo ($locked ? "unlock" : "lock"); ?>">
  <button type="submit"><?php echo ($locked ? "Unlock site" : "Lock site"); ?></button>

</form>

<?php } ?>

The final challenge ... Human error

The last thing we ran into after having succesfully deployed this approach was the human factor: We learned that administrators would sometimes unlock the site, do their work in Wordpress and then completely forget to turn the site back into a static site. To mitigate this we simply made a simple shell script which would run once a day at night. This script would check if the site was unlocked, and if it was then it would run the lock script.

In this way the site would never be exposed for more than 24 hours - typically much less than that as admins would do their work during the day and then at night the site would auto-lock. Not a foolproof solution, but at least something which would mitigate the issue. If you attempt something similar, this is something you should keep in mind.

Improvements and other approaches

This novel approach of getting rid of Wordpress is not perfect. It predicates that the site is relatively static by nature. It would also be good to just disable PHP processing alltogether when switching to the static version of the site, but we needed PHP processing for the control interface.

One improvement could be to simply place the Wordpress site in a walled garden. I.e. have another server/host which runs the Wordpress site, and then protect it with basicauth or IP restrictions. In your lockdown script you'd then enable httrack to "log in" to this protected site in order to generate the publicly available static version.

You could use other methods of generating your static files, and you might use something like git to keep track of changes and as a deploy tool to your static version of the site.

In general, it might be a good idea for you to use git to keep track of changes to a live Wordpress site, as an alternative to our Wordpress Lockdown approach. Using git is a great way to see exactly what files a hacker has changed, and maybe even learn their tricks in the process ...

In any case, we hope this might inspire others to come up with creative solutions to the tricky issue which is Wordpress and its many vulnerabilities.

Some other things to look out for:

  1. Make sure your front-end javascript libs are updated. It is common, for example, to see out-of-date jQuery libs with vulnerabilities in the wild.
     
  2. If you use BasicAuth over SSL - or gather any information from users - make sure your SSL certs are up to snuff and you use a modern profile in your webserver config
     
  3. You should review HTTP headers such as X-Frame-Options and Strict-Transport-Security

You should try Webdock

Setting up your Wordpress site on a Webdock stack and using our standard Wordpress Lockdown functionality will get you most of the way to a secure Wordpress site. We've created a hosting platform which is thoroughly documented, fast and easy to "mod" using scripts directly in the Webdock backend.

Webdock gives you, the developer, a rock-solid foundation along with the flexibility to build and host websites the way you want to. Give us a spin, it's free to try, and let us know what you think :)