Threat intelligence

Ghost CMS Content API Blind SQL Injection

by Security News

Ghost CMS Content API Blind SQL Injection (CVE-2026-26980)

Overview

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.

Technical Overview

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.

figure1.png
Figure 1: Attack flow: unauthenticated Content API request to blind SQLi oracle

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.

figure2.png
Figure 2: Vulnerable slug-filter-order.js: user slug values interpolated into a raw SQL CASE fragment

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.

figure3.png
Figure 3: Payload mechanics: comma splitting inside an NQL STRING token reshapes the raw SQL fragment

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.

figure4.png
Figure 4: Time-based blind oracle and char()-based character extraction in the Python exploit

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.

figure5.png
Figure 5: Security fix: parameterized bindings replace raw string interpolation in slug-filter-order.js

Triggering the Vulnerability

The following conditions must be met for successful exploitation of CVE-2026-26980:

  • Vulnerable Ghost Version: The target must be running Ghost v3.24.0 through v6.19.0. This includes all Ghost(Pro) releases in that range and the official ghost Docker image up to tag 6.19.0.
  • Public Content API Enabled: The /ghost/api/content/ endpoint must be reachable. This is the default configuration; Ghost embeds the Content API key in every theme's HTML via the {{ghost_head}} helper, so the key is not a secret.
  • At Least One Published Post: At least one published post must exist. The injection lives in the ORDER BY clause, which is only evaluated when the WHERE clause returns at least one row, so the attacker must include a real post slug in the filter to force rows out of the query.
  • Database Engine Supports String Concatenation: Both SQLite (||) and MySQL (|| with PIPES_AS_CONCAT or CONCAT() variants) are exploitable. SQLite is the default for Ghost's development image; production deployments typically use MySQL 8.
  • HTTP Access to the Ghost Host: The attacker must be able to send unauthenticated HTTP GET requests to the Ghost server, typically on port 2368 for standalone deployments or 80/443 behind a reverse proxy.

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.

Exploitation

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.

Exploit Payload Structure

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.

figure6.png
Figure 6: HTTP exploit request: Content API key, real slugs, and time-based blind oracle payload
Payload Key Components
ComponentValuePurpose
Target EndpointGET /ghost/api/content/posts/Public Content API route backed by the vulnerable serializer
Auth Parameterkey=<CONTENT_API_KEY>Ghost Content API key, published in theme HTML and therefore effectively public
Injection Parameterfilter=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
Attack Chain

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.

Exploitation Demo

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.

Video Demonstration

SonicWall Protections

To ensure SonicWall customers are prepared for any exploitation that may occur due to this vulnerability, the following signatures have been released:

Signature IDSignature Name
IPS: 22123Ghost CMS slug Filter SQL Injection

Remediation Recommendations

The risks posed by CVE-2026-26980 can be mitigated or eliminated by:

  • Upgrade to Ghost v6.19.1 or Later: Apply the official security patch that replaces raw SQL string interpolation in slug-filter-order.js with parameterized Knex bindings. This is the only complete fix and should be rolled out first; Docker users can pull ghost:6.19.1 or later.
  • Apply the WAF Mitigation from the Advisory: Block or alert on any Content API request whose filter or order parameter contains the exact marker slug:[ or its URL-encoded form slug%3A%5B followed by SQL keywords (CASE, WHEN, THEN, END, ||, char(, SELECT, randomblob). This matches the on-wire artifact called out in GHSA-w52v-v783-gw97.
  • Restrict Content API Exposure: Where feasible, front the Ghost Content API with an authenticated reverse proxy, IP allowlist, or rate limiter. The Content API key is embedded in theme HTML and cannot be treated as a secret; defense must occur at the network or proxy layer.
  • Rotate Secrets After Patching: Once patched, rotate all Ghost Admin passwords, the session_secret, every Admin API key, and every Content API key. Any of these may have been exfiltrated during the exposure window, and rotation is the only way to invalidate credentials already in attacker hands.
  • Monitor for Exploitation Attempts: Inspect HTTP access logs for GET /ghost/api/content/ requests whose query string contains slug:[ or slug%3A%5B paired with CASE, randomblob, char(, SELECT+, or FROM+users. Pair with response-time anomaly detection: repeated ~400 ms Content API responses against a normally sub-50 ms endpoint are a strong oracle indicator.
  • Utilizing IPS signatures: Deploy updated IPS signatures to detect and block malicious payloads.
  • Network segmentation: Isolate application servers from sensitive internal resources and implement egress filtering to detect unauthorized outbound connections.

Relevant Links

Attribution

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

Security News

The SonicWall Capture Labs Threat Research Team gathers, analyzes and vets cross-vector threat information from the SonicWall Capture Threat network, consisting of global devices and resources, including more than 1 million security sensors in nearly 200 countries and territories. The research team identifies, analyzes, and mitigates critical vulnerabilities and malware daily through in-depth research, which drives protection for all SonicWall customers. In addition to safeguarding networks globally, the research team supports the larger threat intelligence community by releasing weekly deep technical analyses of the most critical threats to small businesses, providing critical knowledge that defenders need to protect their networks.

Related Articles

  • GPT Academic Pickle Deserialization Remote Code Execution
    Read More
  • Budibase Cloud View Filter Eval Injection Allows Full Remote Code Execution
    Read More