Journal
What should you do when a WordPress site has been hacked?
Take the site offline from the public if you can, preserve a copy of what is there, and restore from a backup you trust. If you do not have that backup, stop guessing at files and get help. Steelbridge does this as site rescue. The rest of this note is the defensive checklist behind that work. It was first published on November 5, 2017.
WordPress is a common target because it is widely used. That is not a reason to abandon it, and it is not a reason to publish a percentage from 2017 as if it were still the measurement. Any site can be compromised. A popular one gets more attention.
What a compromised site can look like
Defacement. The pages you wrote are replaced with someone else’s content. The markup is often broken enough that the navigation no longer makes sense.
A hidden redirect or a hidden page. Visitors, or only some visitors, are sent somewhere else. A page can also sit on the server without a link in your menu, which is how a phishing page once landed on a fly-fishing site I was responsible for. I found out when the authorities did. I am not naming that site here.
Search spam. Extra pages are added so someone else can borrow whatever trust the domain had earned.
Malware delivered through an old plugin or an old core. A plugin from the official directory is not automatically dangerous. A plugin that has not been updated, or that was never reviewed before it was installed, is a common door.
Ransomware. Files are encrypted and a payment is demanded for a key. Do not treat payment as the recovery plan. Restore from a clean backup, or hire someone who can. There is no honest shortcut in this paragraph.
Backups that are not on the same disk
Keep backups off the server as well as on it. A copy in the same account is useless when the account is the thing that was taken. In 2017 I was running scheduled backups about once a week and keeping months of them. The schedule you choose matters less than having a copy you did not leave in the web root. Update the site, or ask someone to, before you trust a new backup.
Hardening that is still worth doing
Keep WordPress, the theme, and the plugins current. The compromised sites that have crossed my desk were running old core, old plugins, or both.
Do not leave the first administrator named admin. Create a new administrator, log in as that user, and delete the old one.
Limit how many times a password can be tried. A security plugin can do this. I have used Wordfence for that job. The brand is an example, not a requirement.
On Apache, deny direct web requests for wp-config.php. Make a copy of .htaccess first. If the site returns a 500 after the change, put the copy back.
<FilesMatch "^wp-config\.php$">
Require all denied
</FilesMatch>
Older Apache 2.2 servers used Deny from all inside Files instead of Require. Use the syntax your server actually runs.
Refresh the authentication keys and salts. WordPress publishes a generator at https://api.wordpress.org/secret-key/1.1/salt/. Replace the block in wp-config.php that starts with the authentication unique keys. Everyone who was logged in will need to log in again. That is the point.
Serve the whole site over HTTPS, and set the WordPress address to the https version. The 2017 post recommended FORCE_SSL_LOGIN and FORCE_SSL_ADMIN. Forcing the login screen alone is obsolete. HTTPS belongs on every URL, with a certificate from the host, not a self-signed certificate the browser will reject.
Moving wp-content out of the WordPress directory is a step I described in 2017 for installs that live in a subdirectory. It still works in principle, and it still breaks plugins that assume the normal paths. I would not start there.
Hiding the generator tag is a small step, not a defense:
remove_action( 'wp_head', 'wp_generator' );
If you would rather hand it over
Cleaning a live site is easy to make worse. Site rescue is the done-for-you version of this note. The machine the site runs on is a separate decision. Chris runs the servers at ParsonsHosting. That is hosting, not the repair itself.