Unhackable Wordpress
Last updated: January 26th 2021
Introduction
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"
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:
- 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
- Create another script which can point the webserver back to the original Wordpress site - Our "unlock" script
- 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:
- 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.
- 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
- 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 :)
Related articles
-
Migrate your WordPress website to Webdock for free, without downtime using plugins
This guide shows how to migrate a Wordpress website without downtime to your Webdock server using a Wordpress plugin.
Last updated: July 19th 2022
-
Migrating a Wordpress site to Webdock
Our friend Erik Hanchett shows in this video how you can move a Wordpress site from any server to Webdock.
Last updated: July 19th 2022
-
Fixing Wordpress Redirect Issues
Wordpress really likes to redirect to the website address where it thinks it is installed. This may cause infinite redirect loops where you get a browser error, or your site is redirecting you to an old or unexpected address. Learn how to fix these issues.
Last updated: July 19th 2022
-
Wordpress lockdown
Learn how our Wordpress Lockdown feature works and how it can help protect your Wordpress installation from hacks
Last updated: November 2nd 2020
-
How to speed up your Wordpress using just plugins
This article shows how to speed up your Wordpress site without having to touch any code whatsoever and just install some plugins to do the job for you.
Last updated: January 21st 2022
-
How To Secure Your WordPress Website: A beginners guide
In this guide we outline the most basic and rudimentary steps you can take as a beginner in order to better secure your Wordpress website
Last updated: August 12th 2022