Let's talk
security

WordPress site keeps getting hacked? What actually stops it

A WordPress site keeps getting hacked when attackers can still write to the server. What stopped it for us, and how to decide what will stop it for you.

· · updated

Diagram: From maintaining a site to nothing to attack. WordPress: database, login, plugins, uploads; Hardened, still defended every update; Static HTML served by nginx; No database, no PHP, no login, no plugins

If your WordPress site keeps getting hacked after every cleanup, the usual cause is that nothing about the position has changed: the site still has a database, plugins written by other people and folders the web can write to. What stops it is removing what can be written to or run: separate access for each site, no PHP in the uploads folder, files checked against the official release. For a site that is mostly read, it can mean replacing WordPress with static pages. We did the last of these for our own site.

Here is how the argument played out, and what it cost us.

You clean up the hacked WordPress site. You install the security plugins and keep everything updated. And you are still running a site where every plugin is somebody else’s code, working on your server with your database password. Every update is a race. Every folder that accepts uploaded files is a place where a file might be written and then run.

That is the argument you are always slightly losing. You can do all of it properly, give each site the least access it needs, run each site in its own separate process, block PHP from running in the uploads folder, check files on a schedule, and you are still defending a position, not holding one.

We had been doing exactly that across a set of sites, and doing it competently, after a painful education. A site that has been compromised accumulates injected addresses, thousands of them, indexed and linked from elsewhere. Then we rebuilt our own site, and the position stopped needing to be defended, because there was nothing left to attack.

What was actually going on

Most of these compromises last because the attacker can write to the server. A WordPress site needs a database, a login page, plugins and places for files to be written, and each is a door. Patching them is a duty that never ends.

There is also a trap in the usual advice. These sites run on nginx, and nginx does not read .htaccess files at all. A great deal of WordPress security advice, and several popular security plugins, protect a site by writing rules into those files. On Apache that works. On nginx they are inert text in the folder they claim to protect. The plugin reports protection on, the file exists, and nothing is enforced.

That is how a folder of site backups, the sort of thing that holds a full copy of the database, can sit publicly readable for a long time while a dashboard says everything is fine. The lesson: verify that a protection is enforced by whatever actually serves the request, not that it was configured somewhere.

What we changed

We rebuilt the site as a folder of HTML, CSS, images and two XML files served by nginx. That does not reduce the attack surface; it removes most of it:

  • no database, so nothing to inject into, no password in a config file and no dump to steal
  • no PHP running anywhere in the site’s folder, so an uploaded file is a file, not a program
  • no admin login, so nothing to brute force and no password to leak
  • no plugins, so a flaw found in a popular extension is somebody else’s incident

The last point is the quiet one: the site cannot be written to by a web request at all. Remove the ability to write and a compromise has nowhere to live. The next publish overwrites the whole folder from a build that came from version control.

Every request for a PHP address returns a real “not found”, deliberately, and will keep doing so. That is not suspicion of a language. It is about what the old era left behind: several thousand previously-injected addresses. If those started answering again because a helpful rule caught them, the spam would come back to life in search results. So unknown addresses must keep returning a genuine 404, and PHP must not run even by accident.

It also changed how we operate. Publishing became a swap: the new build is unpacked beside the live folder, checked in place, and two commands exchange them. Rolling back is the same two commands in the other order, and the previous version is still there. Backups became trivial, because the site is the build and the build comes from the repository. The thing to back up is the enquiry database, a far smaller problem than a CMS, its uploads, its database and whatever it has collected since 2019. There is no patching schedule for the website any more, because nothing in it executes.

What it did not fix

We gave up three things, and they are real.

Non-technical editing: there is no admin screen. Content lives in text files in version control and publishing is a commit and a build. For a team that writes its own material that is an improvement. For an organisation where marketing has to publish without engineers, it is a genuine cost and should be priced as one.

Anything that needs a server. The contact form is now a small service of our own, running as its own user with a restricted profile, instead of a plugin. That is better, but it is something we now own and maintain.

Instant changes: a typo fix is a build of about ten seconds and a swap, so in practice a minute, not zero.

How to decide what will stop it on your site

  1. Ask what the site actually needs to write to, and whether it needs a database at all. If it is mostly read and occasionally changed, by people comfortable with a pull request, that describes a company site and most product pages, and a static site removes most of the attack surface. It does not describe a newsroom.
  2. If it really is a content operation, with many authors, frequent publishing, scheduled posts and non-technical staff, WordPress is the right tool and the answer is to run it properly. That means per-site isolation, checking files against the official release rather than scanning for known patterns, and treating updates as a duty rather than a notification.
  3. Ask your host or developer to show that each protection is enforced by the server that actually runs your site. On nginx, rules written into .htaccess files do nothing.
  4. Ask where the backups are kept, and whether anyone has tried to download them from outside.
  5. Price what you would give up. Without WordPress there is no admin screen, and a change is a commit and a build.

Where this ends up

Deciding which of those two a system actually is, and then moving it in stages while the business carries on running on it, is the work described under enterprise modernisation.

Working on something like this?

We build this kind of software, and we staff the teams that do.

Get in touch