My Blog Was Hacked

How CVE-2026-26980 was used to plant adware on my blog.

Share
My Blog Was Hacked

TL;DR — My self-hosted Ghost blog was compromised via CVE-2026-26980, a critical SQL injection vulnerability exploited against 700+ sites in May 2026. An attacker used it to steal my Admin API key and inject an ad iframe into my site footer, quietly earning ad revenue from my readers. After discovering the injection during a routine site check, I removed the code, rotated all credentials, and migrated to Ghost Pro.

Timeline

  1. May 26th, 2026 — Mailgun notifies me of suspicious activity linked to my account
  2. May 28th, 2026 — I identify CVE-2026-26980 as the root cause, a critical SQL injection flaw in Ghost's Content API that was actively exploited against 700+ sites beginning May 7, 2026
  3. May 31st, 2026 — While setting up a custom domain on Ghost Pro, I notice an unfamiliar "Advertise In This Ad Space" banner appearing at the top of my site
  4. May 31st, 2026 — After ruling out browser extensions, I find the source of the hack, a hidden ad iframe injected into Ghost's footer Code Injection field
  5. June 4th, 2026 — I remove the injected code, rotate all credentials, and complete the migration to Ghost Pro, decommissioning the old server

Table of Contents

  1. Signs of Compromise
  2. The Investigation
  3. Anatomy of the Attack
  4. Remediation
  5. Learn More

Signs of Compromise

On May 27, 2026, 01:51 UTC I received an email from Mailgun support (an email service I was using to distribute this blog) containing the following: "We apologize for the inconvenience, but the account was disabled due to what appears to be a compromise."

I was actually annoyed. My blog wasn't compromised, their compliance team had made a mistake and was inconveniencing me. I responded swiftly stating as such, though I was much more tactful. Five hours later, a compliance specialist provided a wealth of information showing that my blog had in fact been compromised. She shared an article outlining the vulnerability and internal evidence she had collected proving someone had stolen and used my own Mailgun API keys.

Fun.

The Investigation

Everything Mailgun shared checked out, and I moved quickly to address it. At the time, I was hosting the blog on a DigitalOcean Droplet, a rented virtual server running in the cloud, which meant fixing the problem required logging into that server remotely and editing configuration files by hand, through a command line interface. I had chosen this over Ghost Pro because it was marginally cheaper.

I quickly realized how annoyed I was at the process. Connecting to the server remotely over an encrypted channel, reconfiguring API keys, adjusting network port settings...none of this was how I wanted to spend my time. I wanted to write and work on personal projects, not maintain the plumbing underneath a blog.

I think emotions are useful signals for what we actually care about, and my frustration stemmed from the infrastructure getting in the way of writing.

The cost savings weren't compelling either. I was saving only a few dollars a month by self-hosting. I have an hourly rate I use to value my time, and make better judgements about manually doing things vs. outsourcing them. When I applied it to the hours spent managing the server, let alone remediating a security incident, the comparison wasn't close. Ghost Pro cost a few dollars more per month. The remediation alone had already cost me more than a year's worth of the difference.

I decided to fix both problems at once. I migrated the blog to Ghost Pro, Ghost's own managed hosting (which handles all server maintenance, security patches, and infrastructure automatically) and shut down the old DigitalOcean server at the same time.

Once the blog was migrated to Ghost Pro, I navigated the pages and checked the content to ensure the attackers hadn't altered anything. I noticed that Brave, the browser I use most often, was blocking an advertisement on the main page of my blog.

At the right side of the address bar there is a small grey #1, indicating the number of advertisements blocked by the browser.

What stuck out to me was that Ghost isn't supposed to have advertisements. I selected it specifically because it was ad-free, minimalist, open source software. When I removed the shields to stop blocking advertisements, one showed up at the top of my blog.

Who actually likes advertisements?

I restored an old droplet on DigitalOcean and navigated to my "old" blog hosted on the old infrastructure. The same ad showed up there. After spending an hour combing through Ghost forums, Reddit and a few other sources, I decided to check the "code injection" tab in the Ghost admin portal. This allows you to modify the webpage code right before Ghost renders a blog. I used this feature once before, but decided against it, because it can affect how your blog is rendered in some pretty unexpected ways, especially after Ghost updates from time to time.

To my surprise, there was code being injected in the footer. I never put that there. I deleted it, and reloaded my blog. Voila, no more advertisements.

Anatomy of the Attack

After removing the injected code, I was able to piece together what likely happened.

The attackers started by scanning the internet for sites running Ghost. Ghost-powered sites share recognisable characteristics like version numbers, file paths and structural patterns in the HTML. This makes them identifiable from the outside. The attackers scanned for these fingerprints at scale, and this blog appeared on their list.

Ghost has two types of API keys: a public one and a private one. The public key is embedded in every page of the site. It's what allows your browser to fetch content like images and posts when you visit. Because it's public, it's also accessible to anyone, including attackers.

Older versions of Ghost had a flaw in how they handled requests made with this public key. When you ask a website for content, like the next page of posts, you're sending a small piece of input to the server. Normally that input is harmless. But Ghost's software took that input and passed it, unmodified, directly into the database query that fetched the results. This is a well-known class of vulnerability called SQL injection. By carefully crafting that input, an attacker can manipulate the database query to return data it was never meant to expose publicly.

The database behind a Ghost site holds more than just published posts. It contains unpublished drafts, member email addresses, login credentials, configuration settings, and most importantly, the private Admin API key. By sending a single malicious request using the public key, the attacker was able to extract that private key from the database. No login required.

A screenshot of the malicious ad-ware injected into my site. The actual ad URL is blurred out.

The Admin API key is essentially a master key to the site. Whoever holds it can publish or delete posts, access member information, change settings, and inject arbitrary code into every page of the site at once. Ghost includes a feature called Code Injection that's intended for things like adding analytics scripts or custom styling. It lets administrators paste HTML or JavaScript into the header or footer of every page globally. The attackers used this feature to paste their ad code into my site's footer.

The ad itself loaded from A-Ads, an anonymous advertising network. The attacker had registered an ad unit with A-Ads, a simple process, requiring no verification, and the injected code pointed to that unit. Every time a reader loaded a page on this site, their browser fetched the ad, and A-Ads registered the impression and paid the attacker accordingly. My readers' attention was being sold without my knowledge.

To maximise how long this could continue undetected, the attackers left the rest of the site untouched. No content was modified, no posts deleted, nothing broken. The only visible sign was a banner at the top of the page that could easily be mistaken for a deliberate ad placement and dismissed with a click.

Remediation

Like I mentioned above, I just want to write. I'm lazy. Though I do enjoy deeply technical projects, I don't care about the technical details of hosting a blog. I want to write code on my own time.

The first priority was migrating to Ghost Pro and decommissioning the old server entirely. Once that was done, I went through the new installation methodically. Every page, every setting, every configuration option was reviewed for anything that shouldn't be there. For good measure, I pointed Claude Code (an AI coding assistant) at the site and had it parse every HTML page for injected or suspicious code. Fresh eyes from my AI overlords.

Next, I rotated every credential associated with the old installation. Every API key, every password, every integration. Even the Mailgun connection, which I no longer needed but which could have been compromised regardless, was evaluated. If a key existed, I treated it as potentially stolen.

The migration also solved a problem I hadn't fully appreciated before: maintenance. Running a self-hosted Ghost installation means tracking security advisories, evaluating patch severity, and manually upgrading when new versions ship. The CVE that hit this site had a patch available for 95 days before the exploit campaign peaked. This is now handled automatically. I no longer need to think about it.

This is what I should have done from the start. The self-hosted route made sense on paper. It was marginally cheaper and provided more control, but it quietly accumulated a debt I eventually had to pay. Keeping a personal blog secure is not something I want to spend my time on.

Learn More