
SonicWall Capture Labs threat research team became aware of the threat CVE-2026-26980, assessed its impact, and developed mitigation measures for this vulnerability. The flaw, also known as the Ghost CMS Content API slug Filter SQL Injection, is a critical unauthenticated SQL injection vulnerability affecting Ghost in versions 3.24.0 through 6.19.0. The flaw allows a remote attacker to inject arbitrary SQL into the ORDER BY clause of Content API queries by placing a crafted payload inside the filter=slug:[...] or order=slug:[...] query parameter, yielding blind read of any table in the Ghost database, including admin credentials, bcrypt password hashes, session secrets, and Admin API keys. Classified under CWE-89 (SQL Injection) and rated CVSS 9.4 (Critical, AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L), the flaw was reported via GitHub Security Advisory GHSA-w52v-v783-gw97. Two public proof-of-concept exploits are already available. The EPSS score of 32.74% places this vulnerability in the top ~1% of CVEs for near-term exploitation probability. Affected products include every Ghost deployment from v3.24.0 through v6.19.0, including the official ghost Docker image and Ghost(Pro) self-hosted installations. Fixes are available in Ghost v6.19.1. Users should upgrade immediately.
Ghost is an open-source Node.js content management and publishing platform used for blogs, newsletters, and paid memberships, with over 52,000 GitHub stars and a first-party Docker image. Ghost exposes a public, read-only Content API at /ghost/api/content/posts/ that authenticates requests with a Content API key embedded directly in theme HTML, making the endpoint effectively unauthenticated from the attacker's perspective.
The root cause of CVE-2026-26980 lies in slug-filter-order.js inside the Content API input serializer. The function slugFilterOrder(table, filter) parses slug:[a,b,c] expressions out of the NQL (Ghost Query Language) filter and builds a raw SQL CASE WHEN … END ASC fragment that Knex later attaches as options.orderRaw. The attacker-controlled slug values are concatenated directly into that fragment through JavaScript string interpolation with no sanitization and no query binding.

Because the return value of slugFilterOrder is a raw SQL string, the injected payload is executed verbatim by the database engine inside the ORDER BY clause. Both the SQLite default configuration shipped in Ghost's developer image and production MySQL deployments are affected. The vulnerable interpolation is a six-line diff (the smallest possible SQL injection footprint), but the impact is complete database read via a blind boolean/timing oracle.

Reaching the injection point requires defeating Ghost's NQL parser, which enforces lexer rules on tokens such as single quotes, parentheses, and commas. The bypass exploits a mismatch between NQL and slugFilterOrder: NQL accepts a quoted STRING token as a single slug value, but slugFilterOrder then does a naive .split(',') on the bracket contents, splitting right through the NQL string and producing multiple synthetic "slug" values. Each synthetic slug is interpolated into its own WHEN … = '<slug>' THEN n clause, and the attacker uses paired single quotes and SQL || concatenation to stitch the resulting fragments into one attacker-controlled scalar expression.

For a blind oracle on SQLite (Ghost's default during development and in many small deployments), the attacker substitutes SLEEP() (which does not exist in SQLite) with a CPU-bound length(hex(randomblob(50000000))) expression that produces a consistent ~400 ms delay on a TRUE branch while a FALSE branch returns in ~10 ms. Character extraction uses char() concatenation with GLOB/LIKE to avoid the comma-split problem entirely, since functions such as substr(x,1,1) would be shredded by the same .split(',') that makes the attack possible.

The security fix in Ghost v6.19.1 replaces the interpolated CASE … END ASC string with a Knex-compatible {sql, bindings} object that uses ? placeholders and a parallel bindings array. A supporting change in crud.js teaches the model layer to unpack the new object form into options.orderRaw and options.orderRawBindings. A defense-in-depth change in get.js replaces the jsonpath template-helper dependency with a strict regex-validated path resolver, eliminating a secondary injection surface shipped in the same release.

The following conditions must be met for successful exploitation of CVE-2026-26980:
Critical Note: No authentication, no user interaction, and no pre-existing foothold are required. Because the Content API key is by design a public value embedded in every theme, the only prerequisite beyond network reach is the existence of a single published post, a condition that holds on nearly every production Ghost site.
Exploiting CVE-2026-26980 requires only a single unauthenticated HTTP GET request carrying a crafted filter=slug:[...] payload. The injection exploits the gap between NQL's STRING token grammar and slugFilterOrder's naive comma split, using SQL string concatenation to bridge the resulting fragments into a single attacker-controlled scalar expression inside the ORDER BY clause. A time-based blind oracle then extracts arbitrary database values one character at a time.
The attacker appends two real post slugs (so the WHERE clause returns rows and ORDER BY actually runs), followed by a single NQL STRING token 'THEN 0 END ||, || <INJECTION> ||, || CASE WHEN'. slugFilterOrder splits the STRING on commas into three synthetic slugs, each of which becomes its own WHEN posts`.`slug` = '' THEN clause. Paired single quotes close the outerCASE, ||concatenation absorbs the code-generated quotes, and executes as an ordinary SQL scalar expression during sorting.

| Component | Value | Purpose |
|---|---|---|
| Target Endpoint | GET /ghost/api/content/posts/ | Public Content API route backed by the vulnerable serializer |
| Auth Parameter | key=<CONTENT_API_KEY> | Ghost Content API key, published in theme HTML and therefore effectively public |
| Injection Parameter | filter=slug:[<s1>,<s2>,'<PAYLOAD>'] | NQL filter; slug:[...] path reaches slugFilterOrder and becomes raw SQL |
| Bridge Primitive | 'THEN 0 END ||, || <INJECTION> ||, || CASE WHEN' | NQL STRING token; comma split + || concat stitches fragments into one scalar |
| SQLite Oracle | (SELECT length(hex(randomblob(50000000)))) | CPU-bound ~400 ms delay on TRUE branch (SQLite has no SLEEP()) |
| Comma-Free Extractor | <subquery> GLOB char(c1)||char(c2)||...||char(42) | Rebuilds comparison strings with char() because substr(x,1,1) would be split |
Step 1. Key Discovery: The attacker issues a plain GET / against any Ghost site and scrapes the Content API key from the rendered theme HTML (data-key="…" attribute injected by {{ghost_head}}). No login is required.
Step 2. Slug Enumeration: The attacker calls GET /ghost/api/content/posts/?key=<KEY> and extracts two published post slugs from the JSON response. These will seed the filter so the underlying WHERE clause returns at least one row and the ORDER BY executes.
Step 3. Blind SQLi Oracle: The attacker sends the crafted filter=slug:[<s1>,<s2>,'<PAYLOAD>'] request with a time-based oracle body and measures response time. TRUE conditions return in ~400 ms; FALSE conditions return in ~10 ms. The attacker iterates per character across the charset to extract users.email, users.password (bcrypt hash), settings.value (session secret), and api_keys.secret.
Step 4. Credential Recovery and Escalation: The extracted bcrypt admin hash is cracked offline with hashcat mode 3200 against a standard wordlist. With the plaintext admin password the attacker authenticates to Ghost Admin and escalates to remote code execution via Ghost's built-in Code Injection feature or a malicious theme upload. This escalation path is outside the scope of the CVE itself but is the natural post-exploitation outcome.
Local lab validation against Ghost v6.19.0 (SQLite) confirmed the oracle: baseline Content API latency 13 ms, TRUE branch 411 ms, FALSE branch 13 ms. The 15-character admin email admin@lab.local was extracted cleanly one character at a time. Packet captures for the curl PoC (40 packets, 16 KB) and Python extractor (1,128 packets, 534 KB) are preserved in captures and expose every signature primitive on the wire.
To ensure SonicWall customers are prepared for any exploitation that may occur due to this vulnerability, the following signatures have been released:
| Signature ID | Signature Name |
|---|---|
| IPS: 22123 | Ghost CMS slug Filter SQL Injection |
The risks posed by CVE-2026-26980 can be mitigated or eliminated by:
Vulnerability coordinated through the Ghost security program and disclosed via GitHub Security Advisory GHSA-w52v-v783-gw97.
Additional PoC contributions by:
Share This Article

An Article By
An Article By
Security News
Security News