A client emailed me about their WordPress site. When I logged in, the dashboard showed more than 30,000 published blog posts. He hadn’t written any of them. The site was publishing over 600 new posts a day, entirely on its own, and had been for a while. The site itself still loaded fine, which is exactly why attacks like this go unnoticed for weeks. Nothing looks broken. Something very much is.
This is the build log of how I cleaned it up and locked it down, working entirely from my Mac against a server on the other side of the world. No hosting panel clicking. No emergency agency retainer. A terminal, SSH, and WP-CLI.
What a spam attack actually looks like
Before the fix, it helps to know what you’re dealing with. A spam attack on WordPress usually means automated bots have found a way in, most often through weak credentials or an exposed endpoint, and are now using your site as free infrastructure. In this case they were mass-publishing posts, hundreds a day, every day. Do the maths on 600 posts daily and you understand how a site quietly accumulates 30,000 of them. Your site becomes a billboard for someone else’s rubbish, at industrial scale.
The damage isn’t only cosmetic. Search engines notice spammy outbound links and will quietly demote you. Your hosting provider may flag the account. And if bots can register users and post content, you have to assume they’ve been probing for worse.
So the job is twofold: throw out whoever is inside, and close the doors they came through.
Step 1: Rotate every password
Start with the obvious. Every admin password, the database password, and the hosting account credentials all get changed. Not just the account that looks compromised. All of them.
The reasoning is simple. You don’t know which credential leaked, so you treat them all as burned. A password rotation costs you ten minutes. Guessing wrong about which account was compromised costs you a second cleanup next month.
With WP-CLI, resetting a user’s password from the terminal is one line:
bash
wp user update admin --user_pass="$(openssl rand -base64 20)"
That generates a strong random password on the spot. Do it for every administrator account, and while you’re in there, delete any admin accounts you don’t recognise.
Step 2: Shuffle the WordPress salts
This is the step most guides skip, and it matters more than people realise.
WordPress uses a set of secret keys, called salts, to sign the cookies that keep users logged in. Here’s the important bit: changing a user’s password does not log out sessions that are already active. If an attacker has a valid session cookie, they stay logged in even after you’ve rotated every password on the site.
Shuffling the salts invalidates every cookie at once. Every session, everywhere, is instantly logged out, including the attacker’s. It’s the equivalent of changing the locks rather than just asking people to hand back their keys.
One command:
bash
wp config shuffle-salts
Everyone has to log back in, including your client. That’s a feature, not a bug. Warn them first.
Step 3: Block XML-RPC
XML-RPC is a legacy WordPress API endpoint, a leftover from the days before the modern REST API. Almost no modern site needs it. Bots love it for two reasons: it accepts login attempts, and it allows multiple authentication attempts in a single request, which makes brute-forcing dramatically more efficient than hammering the normal login page.
If your site doesn’t use the old Jetpack connection method or a legacy mobile app, close it. On an Apache server, a few lines in .htaccess do it:
apache
<Files xmlrpc.php>
Order Deny,Allow
Deny from all
</Files>
Check your access logs afterwards. On this site, requests to xmlrpc.php made up a startling share of all traffic before the block. Bots don’t announce themselves. They just keep knocking until someone bolts the door.
The setup that made this possible
None of the above is exotic. What made it fast was the remote tooling, so it’s worth documenting.
The site runs on a DirectAdmin server. My working machine is a Mac running macOS Ventura, with PHP provided by Laravel Herd and WP-CLI installed manually as a Phar file. An SSH alias in my config means connecting to the client’s server is one short command rather than a memorised hostname and port.
The piece I lean on most is the backup workflow. Before touching anything on a compromised site, you take a copy. Mine is three moves: export the database on the server, pull it down, then remove the export from the server so a database dump is never left sitting in a public-adjacent directory.
bash
wp db export backup.sql
scp client-server:~/backup.sql ./backups/
ssh client-server "rm ~/backup.sql"
Export, copy, delete. That last step is the one people forget, and a forgotten .sql file on a web server is a breach waiting to be indexed.
With that safety net in place, every remediation step above becomes low-risk. If anything goes sideways, you restore and try again.
It also made the final cleanup job feasible. Deleting 30,000 spam posts through the WordPress dashboard, twenty at a time, is not a serious option. From the terminal, you can list every post by the compromised author accounts and remove them in bulk, something like:
bash
wp post delete $(wp post list --author=SPAM_USER_ID --format=ids) --force
Minutes, not days. This is the difference tooling makes.
What this taught the client, and what it should teach you
The site wasn’t on bad hosting. It was on unmanaged hosting, and those are different problems. Cheap hosting is fine. A site nobody is watching, updating, or hardening is not, whatever it costs per month.
The attack ended the moment someone with the right tooling spent an afternoon on it. The vulnerability existed because, for months beforehand, nobody had.
If your site is showing the symptoms I opened with, the three steps above will get you most of the way. And if you’d rather someone were watching before it happens next time, that’s precisely the kind of arrangement I offer.




