A patch released in February is now the engine of one of the larger website-poisoning campaigns of the year. CVE-2026-26980 is an unauthenticated SQL injection in Ghost CMS’s Content API, rated CVSS 9.4 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L, CWE-89). XLab threat intelligence at Qianxin has confirmed it has been weaponized against more than 700 domains — university portals at Harvard, Oxford, and Auburn, AI and SaaS vendors, fintech firms, media outlets, security blogs, and DuckDuckGo among them — to seed ClickFix malware lures.

What happened

Ghost quietly fixed the bug in version 6.19.1 on February 19, 2026. Every internet-facing Ghost install running 3.24.0 through 6.19.0 remained exploitable with a single crafted HTTP request. By May 7, XLab had detected the first poisoned site, and the investigation found an attack that had already been fully automated: a bulk scanner, automatic key extraction, bulk article injection, and dynamic payload distribution. The patch gap — disclosed flaw, available fix, slow installed base — is the entire story.

Technical details

The defect lives in slug-filter-order.js inside the Content API input serializer. The function slugFilterOrder(table, filter) parses slug:[a,b,c] expressions out of an NQL (Ghost Query Language) filter and builds a raw SQL CASE WHEN ... END ASC fragment used in an ORDER BY clause. Attacker-controlled slug values are concatenated straight into that fragment via JavaScript string interpolation — no sanitization, no query binding.

Reaching the injection point means defeating Ghost’s NQL parser, and the bug is a grammar mismatch. NQL accepts a quoted STRING token as a single, valid slug value. But slugFilterOrder then runs a naive .split(',') on the bracket contents, slicing right through the NQL string and producing multiple synthetic “slug” fragments. An attacker bridges those fragments back together with SQL string concatenation into one attacker-controlled scalar expression inside ORDER BY.

The result: a single unauthenticated GET request carrying a crafted filter=slug:[...] (or order=slug:[...]) parameter yields a time-based blind oracle. Arbitrary database values come out one character at a time — including admin credentials, bcrypt password hashes, session secrets, and, critically, Admin API keys.

Impact assessment

This is information disclosure that escalates to full content control. The campaign chains it cleanly: exploit the SQLi to extract the site’s Admin API key, then use that key — legitimately, through the API — to rewrite published articles. The injected payload is a lightweight JavaScript loader that pulls a second-stage cloaking script. That script fingerprints each visitor, and qualifying targets are shown a fake Cloudflare verification prompt loaded in an iframe over the article. That is the ClickFix lure. Observed payloads include DLL loaders, JavaScript droppers, and an Electron-based sample named UtilifySetup.exe.

For a self-hosted Ghost operator the blast radius is your readers’ machines and your own credential store at once.

Mitigation — do this now

Upgrade to Ghost 6.19.1 or later immediately; the fix replaces the vulnerable string interpolation with parameterized queries. Patching alone is not enough if you were exposed:

  • Assume your Admin API key and Content API key are stolen — rotate both.
  • Rotate the session secret, force-invalidate all sessions, and reset admin passwords (the bcrypt hashes may have been exfiltrated and are now offline-crackable).
  • Audit every published post for injected <script> tags or unfamiliar loader URLs.
  • Grep Content API access logs for filter=slug:[ and order=slug:[ request patterns to scope the intrusion window.
  • If you cannot patch instantly, have your WAF block filter/order parameters containing bracketed, comma-separated slug values as an interim measure.

Self-hosted CMS installs are infrastructure. Treat the version number on your Ghost box the way you treat the kernel on your hosts.

References