Estimated reading time: 10 minutes
I only went in to add one file
It started as one of those small, satisfying jobs. I was working with Claude, the AI assistant I use for much of my website work, to put together an llms.txt file for a client’s website. It’s a simple text file that helps AI tools understand what a site is about, and I’d been meaning to roll them out across my clients’ sites for a while.
To write it, Claude read through the live site to gather the details. Partway through, it stopped and flagged something I hadn’t asked about: right at the bottom of the page’s code, after the copyright line, were around 60 links to gambling websites.
I went and looked at the site. Nothing. The footer looked exactly as it always had. I checked on my phone. Still nothing.
That was the moment my stomach dropped. The links were there, but they’d been deliberately hidden so that no human visitor would ever see them. The only things that could see them were machines that read a page’s raw code, like Claude, and, more importantly, Google.
What was actually going on
Once I started digging, it turned out there were two separate tricks at work, both designed to fool search engines while keeping the site looking normal to everyone else.
Hidden spam links. The gambling links were tucked into a block positioned off the edge of the screen, with zero height. Your browser loads them, but you’d never see them. Search engines read the code, though, so as far as Google was concerned, this respectable small business was suddenly linking to dozens of dodgy betting sites.
A redirect just for search engines. This one was sneakier. When Google, Bing and other crawlers asked for the homepage, they were quietly sent somewhere else instead. Real people typing in the address got the normal site, so nobody had any reason to suspect a thing.
Why go to all that trouble? It’s called SEO spam, and it’s all about borrowing someone else’s reputation. A long-established website has built up years of trust with Google. Links from it carry weight, and spammers want that weight pointing at their own sites. Your site becomes an unpaid billboard you can’t even see.
The part that worried me most was how the links got there. They weren’t stored on the site at all. A few lines of code fetched them live from the attacker’s own server every time a page loaded. That meant they could change what was injected whenever they liked. Today it was gambling links, but tomorrow it could have been something that stole card details at checkout.
Following the trail
My first thoughts went to the usual suspects: an outdated plugin with a known hole, or a hacked WordPress core file. I checked WordPress itself and every standard plugin against the official versions. All clean. The database was clean too.
So I turned to the site’s activity log, a record of who logged in and what they changed. This is where the story fell into place.
In the early hours of one morning, about two weeks before I found the spam, an admin account belonging to a contractor had:
- Searched the plugin directory for “file” and installed a file manager plugin, a tool that lets anyone with an admin login browse, upload and edit the files on the server.
- Within a minute, used it to upload a backdoor: a hidden file that gives full control of the website from a web browser, no login needed.
- Uploaded a database tool and used it to read the customer accounts table.
- Created a new administrator account and renumbered it so it sat quietly among accounts from years ago rather than at the end of the list, where it would have stood out.
That hidden account showed up in the activity log on the same day, and I removed it straight away. At the time it looked like a one-off oddity. What I didn’t know was that the backdoor was already in place, and the attacker carried on using it for a few more days to plant the spam and the redirect, and to check their work.
A lesson within the lesson: an activity log is worth its weight in gold. Without one, I’d have been guessing. With one, I could see exactly what happened, when, and from which account.
So was it the contractor?
It would be easy to jump to that conclusion. The contractor had been brought in through a freelance marketplace for a tracking project months earlier, and their admin account had simply never been removed.
But the login records told a different story. In the eight hours before the break-in, that one account was logged into six times by four different browsers and tools, including an automated script. People log in with a browser. Bots that test stolen passwords use scripts. That’s the classic fingerprint of stolen login details, very often taken by password-stealing malware on the freelancer’s own computer and then sold on.
When I got in touch, the contractor confirmed they hadn’t logged in during that time and hadn’t installed anything. They were shocked, and straight away set about cleaning their laptop and changing their passwords.
I think this is the most important part of the whole story. This wasn’t a dodgy freelancer. It was a perfectly good one whose login was stolen. It could happen to anyone you’ve ever given access to, however trustworthy they are. The real problem was that the account still had full admin access long after the work was done.
The clean-up
Once I knew what I was dealing with, the clean-up was methodical:
- Evidence first. Before deleting anything, I copied every planted file and the server logs somewhere safe, outside the website. You only get one chance to see what an attacker left behind.
- Shut the doors. I removed the backdoor, the database tool and the file manager plugin, and reinstalled the theme from a fresh official copy to wipe out the spam code.
- Undo the damage. I removed the search-engine redirect and asked Google to re-check the site.
- Change every key. All admin passwords, the database password, the hosting password, and the keys the site uses to talk to the payment provider and email services. If an attacker could read it, it had to be changed.
- Remove old access. Contractor and dormant accounts went, leaving only the people who actually run the business.
- Check the neighbours. I checked every other site on the same server. Thankfully, they were all clean.
- Look after the customers. Because customer email addresses had been viewed, the business owners notified their customers, and every customer password was reset as a precaution. Card details were never at risk, as they’re never stored on the site.
The moment I dreaded most was telling my clients. But being able to give them a clear timeline, exactly what was and wasn’t exposed, and what had already been fixed made that conversation much easier than I feared.
What I’d tell every website owner
If you’ve ever hired someone through Upwork, Fiverr or a similar marketplace to work on your site, or given a friend-of-a-friend access “just to fix something”, this bit is for you.
- Every admin account is a door into your website. Go and count yours today. In WordPress, that’s Users, filtered by Administrator. If there are names you don’t recognise, or people who stopped working with you months ago, that’s your first job.
- Give the lowest access that does the job. Most work doesn’t need Administrator. Someone writing blog posts needs Editor. Someone managing orders needs Shop Manager. Only give full admin when there’s genuinely no other way.
- Set an end date. Agree up front that access is removed when the work is finished, and then actually remove it. A reminder in your calendar works wonders.
- Remove dormant accounts, even trusted ones. An account nobody uses is an account nobody watches. If someone needs access again later, it takes two minutes to set it back up.
- Never let anyone install a file manager plugin. They hand anyone with an admin login the keys to the whole server. If a contractor genuinely needs file access, your developer or host can arrange it safely.
- Turn on two-factor login for every admin. With two-factor switched on, a stolen password on its own isn’t enough to get in. It’s the single best protection against exactly what happened here.
- “It looks fine” doesn’t mean it is fine. This site looked perfect for nearly two weeks. Regular checks of what search engines see, and of what’s changed behind the scenes, are what catch the things you can’t see.
- Keep an activity log and backups, and know where they are. When something does go wrong, they turn a nightmare into a solvable puzzle.
For fellow developers
A few things I’ll be doing differently, in case they’re useful:
- Check the source, not just the screen. View source on the homepage now and then, and search for anything odd. That’s how this was found.
- Run the checksums. wp core verify-checksums and wp plugin verify-checksums –all compare files with the official versions in seconds. Remember that premium plugins and the theme aren’t covered, so check those separately.
- Trust a file’s change time, not its modified date. The attacker backdated the file they edited so it blended in. The change time (ctime) can’t easily be faked, and it gave the real timeline away.
- Look for odd files in the site root. WordPress’s own checks don’t flag every unexpected file, so look with your own eyes too.
- Make sure your server records real visitor IP addresses. If there’s a caching or proxy layer in front of the site, WordPress may only ever see the server’s own address. That quietly breaks login limiting and makes investigations much harder. It was the case here until we fixed it.
- Keep evidence before you clean. Copy everything out of the web root first. The planted files, logs and backups told the whole story.
The lucky break, and why you shouldn’t rely on one
If I hadn’t been working on that little text file, this could have gone on for weeks or months longer, quietly damaging the site’s standing with Google and leaving a door open for something far worse. It was caught by chance.
That’s exactly why regular checks matter. Luck isn’t a security plan. Knowing who has access, keeping things up to date, and having someone look behind the scenes now and then is what ongoing website care is for.
And one last thought. Claude found this, but I made every change myself, and that’s how I think it should be. More on that in a future post.
Not sure who has admin access to your website? Get in touch and I’ll happily take a look for you.