Tech 7 min read

Ghost CMS CVE-2026-26980 hijacked 700+ sites for ClickFix via Content API SQLi

IkesanContents

TL;DR

What happened Ghost 3.24.0–6.19.0: a public Content API SQL injection led to arbitrary data reads, Admin API key theft, and mass tampering of post bodies.

What to do

  1. Update to Ghost 6.19.1+.
  2. Even if already patched, rotate the Admin API keys, Content API keys, staff accounts, and session secrets that were exposed during the window.

What to grep /ghost/api/content/ requests carrying filter=slug:[ or slug%3A%5B; external <script> appended to post bodies; redirects to clo4shara[.]xyz or com-apps[.]cc; fake-CAPTCHA ClickFix screens.


Ghost CMS’s CVE-2026-26980 was a Content API SQL injection that got fixed in February.
By May, it was being used to pull Admin API keys from unpatched Ghost sites and inject a JavaScript loader at the end of post bodies.
QiAnXin XLab says it confirmed contamination across more than 700 domains.

This didn’t stop at database reads; it moved on to tampering with post bodies.
The attackers used the Admin API keys they had read to call the Ghost Admin API and rewrite existing posts in bulk.
From the end of a body on a legitimate site, visitors are funneled to a fake Cloudflare verification page, which leads into a ClickFix attack that gets the user to paste commands into the Windows “Run” dialog or PowerShell.
ClickFix refers to a technique that uses a fake error or human-verification screen to suggest “run this and it’ll be fixed,” getting the user to execute the attacker’s commands themselves.

The Content API key is not a secret

Ghost’s Content API is the API for reading published posts from a theme.
The Content API key is meant to be embedded in theme HTML, so it can’t be treated as a secret in itself.
The GitHub Advisory likewise notes that restricting access to the Content API key does not mitigate this vulnerability.

The problem in CVE-2026-26980 sat between the handling of the Ghost Query Language slug:[...] filter and the code that builds the SQL ORDER BY clause.
In vulnerable versions, the slug value was embedded directly into the SQL string and not parameterized.
SonicWall’s analysis describes a path that enters blind SQL injection from filter=slug:[...] or order=slug:[...] and reads the Admin API key, session secret, password hashes, and more.

In the patched 6.19.1, this changed to use placeholders and bindings rather than concatenating a raw SQL string.
The affected range runs from Ghost 3.24.0 through 6.19.0.
NVD rates it CVSS 7.5 with High confidentiality only, but the GitHub CNA (CVE Numbering Authority, the body that assigns CVE IDs and performs the initial assessment) scores it 9.4 Critical with High confidentiality, High integrity, and Low availability.
In this exploitation, once the Admin API key has been pulled, it reaches the point of tampering with post bodies. In operational terms, you have to handle this including the impact on integrity.

Post tampering becomes the distribution channel

In the flow XLab observed, after obtaining Ghost’s Admin API key, the attacker uses the Admin API to insert a JavaScript loader at the end of a post body.
This loader goes out to an external domain to fetch the next stage of code.
In the first wave, clo4shara[.]xyz/11z77u3.php was used, and XLab writes that it later switched to com-apps[.]cc.

The PHP that shows up here is not a simple fixed redirect.
It runs as a filtering script that collects browser fingerprint information and, based on a server-side decision, returns a redirect, popup, download, or arbitrary JavaScript execution.
It’s a branch that shows a harmless page to security scanners and crawlers, and sends only likely targets on to the fake CAPTCHA.

The fake CAPTCHA mimics Cloudflare’s human verification and prompts the user to copy and run a command.
The Hacker News’s write-up confirms a flow where a Base64-encoded command is pasted into the Windows Run dialog, then proceeds to a ZIP, batch file, PowerShell, DLL, and rundll32.exe.
In later waves, malicious JavaScript code is used instead of a DLL, ultimately dropping a Windows executable.

flowchart TD
    A["Ghost 3.24.0–6.19.0"] --> B["Content API<br/>slug filter SQLi"]
    B --> C["Read Admin API key"]
    C --> D["Mass-tamper post bodies<br/>via Admin API"]
    D --> E["JavaScript loader<br/>appended to posts"]
    E --> F["Filter script<br/>branches visitors"]
    F --> G["Fake Cloudflare check"]
    G --> H["ClickFix runs<br/>Windows commands"]

This isn’t an attack that plants a web shell in Ghost itself; the distinguishing feature here is that the post content is used as the distribution source.
From the site operator’s point of view, even if no server process or theme file has changed, the published posts themselves end up serving attack code.

What remains after upgrading to 6.19.1

Updating to Ghost 6.19.1 or later is the first thing to do.
But the update only closes the entry point for the SQL injection; it does not erase keys that have already been stolen or JavaScript that has been written into post bodies.

This is close to the point covered in Joomla JCE’s CVE-2026-48907 about “checking for rogue profiles and PHP files even after the update.”
With JCE, the profile and web shell remained.
With Ghost, the Admin API keys and post-body tampering remain.

On the Ghost side, the first place to check is the end of post bodies.
Look for unfamiliar <script> tags, loaders from external domains, and fake CAPTCHAs loaded in an iframe.
If the injections are concentrated at the end of posts, then not only recently updated posts but old published posts are in scope too.

In the logs, look for requests to /ghost/api/content/ containing filter=slug:[, the URL-encoded slug%3A%5B, SQL keywords, char(, randomblob, and CASE WHEN.
Because blind SQL injection relies on differences in response time, a run of unusually slow Content API responses can also be a clue.
A burst of PUT /ghost/api/admin/posts/:id/ against Ghost’s Admin API in a short window is likewise a trace of bulk post tampering.

For credentials, swap out the Admin API key, Content API key, staff users, and session secret.
On the assumption that staff password hashes were read, change the administrator password too.
If you use the same email and password on another CMS or hosting control panel, separate those as well.

If you block with a WAF, treat it as a stopgap

Ghost’s Advisory states there is no application-level workaround.
As an interim measure, it suggests blocking slug:[ or slug%3A%5B inside the filter parameter at a reverse proxy or WAF.
That said, this can break the legitimate slug filter feature.

Even when you block with a WAF, put it in place as a way to buy time until you upgrade Ghost itself to 6.19.1 or later.
Because the Content API key is meant to be public, a design change that hides the key won’t close this entry point.
Treating a value that can be read from a public page’s HTML as a secret leads you to overlook the entry point you actually need to close.

XLab detected this contamination on May 7, 2026, and by May 16 it had switched to a different domain and an info-stealer (information-stealing malware) with zero detections.
Even if the first domain is shut down on Cloudflare’s side, as long as the loader remains in the post body, it can be swapped to the next domain.
In cleanup, don’t just block by external domain; remove the loader itself that got into the Ghost post body.

References