My blog was hacked and Claude and I just fixed it

My self-hosted Ghost blog got silently hacked via a malicious script injected directly into the database, targeting only Windows users. I traced the source of the issue and fixed it quickly with the help of Claude Code.

My blog was hacked and Claude and I just fixed it
Fraudulent "I'm not a robot" captcha

How it started

It all started from a message on Linkedin yesterday "Hey, you've been hacked I'm getting fake captchas on your blog".

Obviously I checked and saw nothing wrong. But I also remembered about someone telling me something very similar a couple weeks back on Mastodon. So I checked from all of the devices I could think of in the house and couldn't reproduce either, even with a VPN from the location of the user who reported the issue.

At this point, I dismissed it as something coming from the user side. Maybe they had an infected browser extension running. Just as a safe check, I decided to send a call for help on social media to ask people to check the blog and let me know if they saw anything weird. No new reports, either....

Starting the investigation

After a couple hours I still had an uneasy feeling, so I started reading online and found the exact issue the users were reporting. I fired up Claude Code on my server and started investigating to see if I could find something.

For reference I run a self-hosted Ghost instance on Digital Ocean (currently migrating to Hetzner, but that's another story) on top of an Ubuntu with Nginx as reverse proxy.

I spent the next hour or so poking at my server with Claude and we arrived at the same conclusion :

The error is on the other side.

I can reproduce 😖!

But that's when a third report from an old friend finally came in that it all finally clicked in my head.

This all seems to be very Windows related! So I fired up by gaming rig and voila : here's the popup

With the ugly scary don't do it follow up message:

The good news : I could reproduce. The bad news ? Well my server is actually infected (remember this next time you're tempted to trust AI to make conclusions for you :)).

Time to investigate

Now that I could reproduce myself, I had access to the source of the generated popup

So it was a matter of finding where this was embedded. The popup seems to be created in a rcoverlay id. Let's dive deeper into what's in there.

I made a curl request with a Windows user-agent to see if anything suspicious could be found

  curl -s -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" \
    https://lengrand.fr/ways-in-which-genai-has-changed-my-coding-so-far/ | grep -i "rc_overlay\|_rc_\|robot\|script"

There we go, it looks like somehow a script is injected into the footer of all of my posts and activated only on Windows machines.

BTW, if we forget the fact that Claude is often wrong about its conclusions, it's definitely been a great companion and helped me speed up my research by a bunch!

Now that we know what to look for, it gets easier. Nothing in the ghost install files, nothing on the reverse proxy side, the next logical target is the database. And as expected :

$ mysql ghost_prod -e "SELECT COUNT(*) FROM posts WHERE codeinjection_foot LIKE '%eralfduolccitats%';"
  +----------+
  | COUNT(*) |
  +----------+
  |      148 |
  +----------+

  mysql ghost_prod -e "SELECT id, slug, codeinjection_foot FROM posts WHERE codeinjection_foot LIKE '%eralfduolccitats%' LIMIT 1";

Bingo, the malicious script tag has been added to all of my articles inside the database directly, in the codeinjection_foot column.

That's sneaky, because the code injection settings of my ghost admin interface is where I looked for clues at the very beginning of my research. But since those changes were directly written in the database so my instance looked clean at first glance.

And now, the fix

Now that we know where the problem is, the fix comes rather easy. I don't actually have any footer injection on my instance, so I've simply set the columns to NULLfor all of the articles in my database.

$ mysql ghost_prod -e "UPDATE posts SET codeinjection_foot = NULL WHERE codeinjection_foot LIKE '%eralfduolccitats%';"
$ mysql ghost_prod -e "SELECT COUNT(*) FROM posts WHERE codeinjection_foot LIKE '%eralfduolccitats%';"
  +----------+
  | COUNT(*) |
  +----------+
  |        0 |
  +----------+

(Bit of a scary command to run, I can't think the snapshot capabilities of Digital Ocean enough). If you run this yourself, make sure to make backups first!

Cleanup

The last steps were to make sure the issue wasn't happening any more, and plugging any hole I could think of. So I

  • updated my server and the ghost install
  • changed my password
  • checked for unknown users in my ghost instance
  • rotated all my keys

The issue seems to have happened somewhere 2 days ago, date at which all of my articles were updated. They do have an update time in the database, which makes me believe that the changes were done via ghost itself, rather than a direct access to the server

SELECT slug, updated_at FROM posts WHERE updated_at >= '2026-05-16' ORDER BY updated_at DESC LIMIT 20;

  +-------------------------------------------------+---------------------+
  | slug                                            | updated_at          |
  +-------------------------------------------------+---------------------+
  | computer-vision-companies                       | 2026-05-16 21:06:13 |
  | about                                           | 2026-05-16 21:06:02 |
  | terms                                           | 2026-05-16 21:05:50 |
  | first-message                                   | 2026-05-16 21:05:35 |
  | synchronize-config-files-between-computers      | 2026-05-16 21:05:24 |
  | activate-numpad-on-startup                      | 2026-05-16 21:05:11 |
  | converting-a-flv-file-to-avi                    | 2026-05-16 21:05:01 |
  | my-pics-on-deviantart                           | 2026-05-16 21:04:49 |
  | why-i-dont-use-adblocker-and-co                 | 2026-05-16 21:04:38 |
  | keysonic-keyboard-and-linux-problems            | 2026-05-16 21:04:26 |
  | opencv-rect-expects-four-integers               | 2026-05-16 21:04:15 |
  | android-arm-optimized-computer-vision-library   | 2026-05-16 21:04:03 |
  | pombo-how-to-get-your-stolen-computer-back      | 2026-05-16 21:03:52 |
  | classification-hu-and-zernike-moments-matlab    | 2026-05-16 21:03:40 |
  | get-the-power-of-matlab-in-command-line         | 2026-05-16 21:03:30 |
  | errors-on-linux-boot-with-a-radeon-hd           | 2026-05-16 21:03:17 |
  | cool-computer-vision-project-shredded-documents | 2026-05-16 21:03:07 |
  | compiling-opencv-for-linux-debian               | 2026-05-16 21:02:54 |
  | simple-region-growing-implementation-in-python  | 2026-05-16 21:02:43 |
  | pythonunittest-assertraises-raises-error        | 2026-05-16 21:02:32 |
  +-------------------------------------------------+---------------------+

I can't know for sure where the problem is coming from, but my suspicions come from the (unofficial) Ghost MCP server I installed a few months back (and never did anything with). I removed it and deleted the associated keys.

Some learnings

I'll make it short but :

  • Don't be too quick thinking the issue is on the other side. The problem was coming from me all along
  • You surely can't trust AI's conclusions, but Claude has been insanely helpful along the way. I've saved hours with Claude helping me search for the next obvious issues, and proposing fixes.
  • This sounds obvious, but in retrospect I should have been more careful installing third party randomware on my production server. I'm lucky the issue was actually quite contained
  • A story for tomorrow, but it's probably a good trigger to migrate anyways and go European.

A big thank you for all of you who helped me, and especially Anna and Ruurd ❤️.