Leitura arbitrária de arquivos sem autenticação no GitLab pela API de commits
Uma falha de severidade máxima no GitLab CE/EE permite que qualquer pessoa, sem conta e sem senha, leia arquivos do servidor. O ponto de entrada é a API de criação de commits: um auxiliar de upload roda antes da checagem de autenticação e usa um parâmetro controlado pelo atacante como caminho absoluto de arquivo, sem confinamento. Ao codificar uma única letra da palavra commits na URL, o atacante engana o roteamento do componente de front-end e ainda alcança o código vulnerável no Rails. Um erro de reparse transforma a resposta de erro em um canal que reflete o conteúdo do arquivo. Basta que exista um projeto público na instância. A falha está no catálogo de vulnerabilidades exploradas da CISA e foi observada sob sondagem ativa na internet dias após a correção.
O auxiliar que grava o corpo de um upload de commit roda antes da checagem de autenticação e lê um parâmetro controlado pelo atacante, file.path, como caminho absoluto, sem qualquer confinamento ao repositório [1] [2]. Um desvio no roteamento, obtido ao codificar uma letra de commits para %63ommits, impede o componente de front-end (Workhorse) de sobrescrever esse parâmetro, enquanto o Rails ainda decodifica e roteia para o código vulnerável [1]. A leitura acontece sem login. Basta existir um projeto público na instância, condição comum em servidores de código [1] [3].
Em 10 de setembro de 2026, a GitLab publicou correções para o CVE-2026-85706, uma vulnerabilidade de leitura arbitrária de arquivos sem autenticação nas edições Community (CE) e Enterprise (EE) do seu servidor de código [3] [4]. A falha recebeu a pontuação máxima na escala CVSS, 10.0, e foi classificada como travessia de caminho (CWE-22, Path Traversal) [4] [5]. O ponto de entrada é a API de criação de commits de um repositório: uma requisição sem autenticação alcança um trecho de código que lê um arquivo do servidor a partir de um caminho absoluto fornecido pelo atacante [1] [2].
A causa raiz combina três defeitos [1] [2]. Primeiro, o auxiliar de upload que grava o corpo da requisição roda antes da chamada authenticate!, então uma requisição anônima já alcança a lógica de leitura de arquivo. Segundo, esse auxiliar usa o parâmetro file.path diretamente como caminho absoluto, sem restringi-lo ao diretório do repositório. Terceiro, um desvio no roteamento, obtido ao codificar na URL uma letra da palavra commits, faz o componente de front-end (GitLab-Workhorse) ignorar a rota e não sobrescrever file.path, ao passo que o Rails ainda decodifica a URL e entrega a requisição ao código vulnerável [1].
O impacto vai além da simples leitura. Arquivos como gitlab-secrets.json e database.yml guardam as chaves e as credenciais que sustentam a instância, e a leitura deles converte um acesso anônimo em comprometimento potencial de todo o servidor, com posterior acesso autenticado [1] [2]. A falha entrou no catálogo KEV (Known Exploited Vulnerabilities) da CISA em 11 de setembro de 2026, um dia após a correção, e empresas de segurança observaram sondagens contra servidores expostos já nesse mesmo período [6] [7] [8].
A única solução completa é atualizar para as versões corrigidas: 19.3.2, 19.2.6, 19.1.8 e, nas ramificações mais antigas, 19.0.9 e 18.11.12 [3] [4]. Como a leitura sem autenticação pode ter exposto segredos antes da correção, toda instância que esteve acessível na internet deve tratar as credenciais e chaves como comprometidas e girá-las [2] [8].
Este relatório não executou o exploit contra uma instância própria do CiberLab nem realizou engenharia reversa do código-fonte do GitLab. A descrição técnica da causa raiz e da cadeia de exploração vem da prova de conceito publicada pela equipe EQSTLab da SK Shieldus [1], e os metadados da vulnerabilidade (versões, pontuação, situação de exploração) vêm dos comunicados oficiais e de fontes de inteligência abertas verificadas [3] [4] [5] [6] [7] [8]. As regras de detecção derivadas nas seções 8 devem ser homologadas no ambiente de cada organização antes do uso em bloqueio.
| Campo | Valor |
|---|---|
| Identificador | CVE-2026-85706 |
| Produto afetado | GitLab Community Edition (CE) e Enterprise Edition (EE), autogerenciado [4] |
| Classe de fraqueza | CWE-22 (Improper Limitation of a Pathname to a Restricted Directory, Path Traversal) [5] |
| Componente vulnerável | API de commits do repositório, auxiliar de upload de corpo da requisição [1] [2] |
| Ponto de entrada | POST /api/v4/projects/:id/repository/commits [1] |
| Vetor de ataque | Rede, sem autenticação, sem interação do usuário [1] [4] |
| Pontuação CVSS v3.1 | 10.0 (Crítica) [4] [5] |
| Pré-condição | Ao menos um projeto público na instância [1] [3] |
| Descoberta e reporte | Pesquisador com o codinome "s3ntago", via programa de recompensas da GitLab no HackerOne [7] |
| Correção | 10 de setembro de 2026 (19.3.2, 19.2.6, 19.1.8); backport em 23 de setembro de 2026 (19.0.9, 18.11.12) [3] [7] |
| Situação de exploração | Explorada ativamente; catálogo KEV da CISA desde 11 de setembro de 2026 [6] [7] [8] |
| Ramificação | Versões vulneráveis | Primeira versão corrigida |
|---|---|---|
| 18.7 a 18.11 | 18.7 até anterior a 18.11.12 [4] | 18.11.12 (23 de setembro de 2026) [7] |
| 19.0 | 19.0 até anterior a 19.0.9 [4] | 19.0.9 (23 de setembro de 2026) [7] |
| 19.1 | 19.1 até anterior a 19.1.8 [4] | 19.1.8 (10 de setembro de 2026) [3] |
| 19.2 | 19.2 até anterior a 19.2.6 [4] | 19.2.6 (10 de setembro de 2026) [3] |
| 19.3 | 19.3 até anterior a 19.3.2 [4] | 19.3.2 (10 de setembro de 2026) [3] |
A prova de conceito da EQSTLab descreve o alvo como as versões 18.7 a 19.1.7, 19.2.0 a 19.2.5 e 19.3.0 a 19.3.1, e monta o laboratório sobre a 19.1.7 [1]. O comunicado oficial da GitLab abrange também a ramificação 19.0, que recebeu correção por backport em 23 de setembro [4] [7]. A tabela 2 segue os intervalos oficiais, mais abrangentes.
Para entender por que uma única letra codificada na URL derruba a proteção, é preciso conhecer o caminho que uma requisição percorre dentro do GitLab [1]. Diferentemente de uma aplicação web simples, o GitLab não é um único processo: a requisição atravessa camadas com responsabilidades distintas, e a falha nasce justamente na costura entre duas dessas camadas.
O GitLab-Workhorse é um proxy reverso escrito em Go que fica na frente da aplicação Rails [1]. Sua função é aliviar o servidor de aplicação das tarefas pesadas de entrada e saída, principalmente o manuseio de uploads e downloads de grandes volumes, como os pacotes de dados do Git [1]. Quando uma requisição corresponde a uma rota de upload conhecida, o Workhorse intercepta o corpo da requisição, grava-o em um arquivo temporário e substitui os parâmetros do upload por valores que apontam para esse arquivo controlado pelo sistema, entre eles o file.path [1].
Esse comportamento é uma proteção implícita: em condições normais, o Workhorse é quem define o file.path, apontando para o arquivo temporário que ele mesmo gravou, e não o cliente [1]. A aplicação Rails, portanto, sempre esperaria receber um caminho seguro, gerado internamente.
Atrás do Workhorse roda a aplicação principal, em Ruby on Rails, servida pelo Puma [1]. A API de commits do GitLab (POST /api/v4/projects/:id/repository/commits) aceita, em fluxos legítimos, o envio de conteúdo de arquivo no corpo da requisição para compor um novo commit [1] [2]. Para isso, existe um auxiliar que processa esse corpo de upload, lendo o arquivo indicado por file.path [1] [2].
Nem toda a API do GitLab exige autenticação. A resolução do projeto, feita pela rotina find_project!, permite acesso anônimo a projetos de visibilidade pública [1]. Assim, se a instância tem ao menos um projeto público, um atacante sem conta consegue endereçar a API de commits daquele projeto e fazer a requisição chegar ao servidor de aplicação [1] [3]. Servidores de código quase sempre hospedam algum repositório público, o que torna essa pré-condição trivial na prática [1].
A vulnerabilidade resulta de três defeitos que, isolados, seriam inofensivos, mas que juntos abrem a leitura irrestrita de arquivos [1] [2]. Esta seção percorre cada um deles na ordem em que a requisição maliciosa os aciona.
O auxiliar que processa o corpo do upload de commit é executado antes da chamada authenticate! no fluxo do controlador [1] [2]. Isso significa que a operação de acesso ao arquivo, uma verificação de existência seguida da leitura (File.exist? e depois File.read), acontece antes de o GitLab decidir se o solicitante tem permissão para estar ali [1]. Uma requisição anônima já dispara o acesso ao sistema de arquivos. A correção oficial, introduzida a partir da 19.1.8, insere justamente a chamada authenticate! antes desse auxiliar, de modo que a requisição não autenticada é recusada antes de tocar o disco [1].
Ao ler o corpo do upload, o auxiliar toma o valor de params['file.path'] e o utiliza como caminho absoluto de arquivo, sem verificar se ele permanece dentro do diretório do repositório ou de qualquer área confinada [1] [2]. Não há canonicalização, não há verificação de prefixo, não há bloqueio de caminhos que apontem para fora da árvore esperada [1]. Se o atacante controlar esse valor, ele controla exatamente qual arquivo do servidor será lido, de /etc/passwd a /flag.txt [1].
Resta o obstáculo descrito na seção 3.1: em condições normais, o Workhorse sobrescreve file.path com o caminho do arquivo temporário que ele gravou, apagando o valor do atacante [1]. O truque que resolve isso para o atacante é uma dessincronização de roteamento entre o Workhorse (Go) e o Rails (Ruby) [1].
O atacante codifica na URL a primeira letra da palavra commits, trocando o caractere c pela sua forma percent-encoded %63 [1]:
/api/v4/projects/1/repository/%63ommits
O casador de rotas do Workhorse compara a cadeia literal e não reconhece %63ommits como a rota de upload de commits. Por isso ele não intercepta o corpo, não grava arquivo temporário e, decisivo, não sobrescreve o file.path [1]. O Rails, por sua vez, decodifica a URL segundo a norma HTTP, converte %63 de volta em c, reconhece a rota commits e entrega a requisição ao controlador vulnerável, com o file.path do atacante intacto [1]. É a diferença de interpretação da mesma URL entre dois componentes que quebra a proteção.
Nenhum dos dois componentes está, isoladamente, errado ao decodificar a URL: o Workhorse casa rotas por texto literal e o Rails decodifica o percent-encoding, ambos comportamentos defensáveis. O perigo mora na diferença entre eles. Ao proteger uma aplicação atrás de um proxy que reescreve requisições, nunca presuma que o proxy e a aplicação leem a URL do mesmo jeito. Normalize a requisição em um único ponto antes de qualquer decisão de rota ou de segurança.
Com os três defeitos alinhados, o atacante já consegue fazer o servidor ler o arquivo escolhido. Falta trazer o conteúdo de volta na resposta, e é aqui que entra o segundo truque do exploit [1].
A requisição completa combina o desvio de rota com os parâmetros de upload passados na query string, e declara o tipo de conteúdo como formulário urlencoded [1]:
POST /api/v4/projects/1/repository/%63ommits?file=&file.path=/flag.txt&file.size=1&Content-Type=application/x-www-form-urlencoded HTTP/1.1
Host: alvo
Content-Length: 0
Connection: close
O parâmetro file.path aponta para o arquivo a ler, e Content-Type=application/x-www-form-urlencoded instrui o auxiliar a tratar o conteúdo lido como um formulário [1].
Ao ver o tipo application/x-www-form-urlencoded, o auxiliar passa o conteúdo do arquivo que acabou de ler para o analisador de formulários do Rack, na função Rack::Utils.parse_nested_query(File.read(path)) [1]. O Rack então tenta interpretar os bytes do arquivo como se fossem pares de um formulário codificado [1].
Aqui está a engenhosidade do exploit: se o conteúdo do arquivo contém um caractere de porcentagem (%) que não seja seguido de dois dígitos hexadecimais, o Rack considera aquilo uma codificação inválida e lança um erro, cuja mensagem é invalid %-encoding (<trecho>) [1]. O trecho reproduzido nessa mensagem é justamente o conteúdo do arquivo, e ele reaparece verbatim no corpo da resposta HTTP 400 [1]. O atacante lê o arquivo do servidor simplesmente lendo a mensagem de erro.
Esse mecanismo cria dois canais distintos de divulgação, e a diferença entre eles importa para o defensor avaliar o alcance real [1]:
% inválido têm o conteúdo refletido na resposta 400 [1]. Registros de aplicação (logs) frequentemente se enquadram, e arquivos de configuração e credenciais que embutem valores codificados na forma URL também [1] [2]./etc/passwd, são lidos antes da autenticação, mas não têm conteúdo refletido: a resposta é um 401, que apenas confirma se o arquivo existe ou não [1]. Arquivos puramente hexadecimais ou em base64 caem nesse caso e retornam somente o oráculo [1].A prova de conceito da EQSTLab automatiza os dois canais. No laboratório dela, o arquivo-bandeira é plantado com um % ao final justamente para acionar a reflexão [1]:
[*] target http://172.17.0.2
[*] endpoint /api/v4/projects/1/repository/%63ommits
[*] file.path /flag.txt
[+] arbitrary file read OK -> content of /flag.txt:
EQST{gitlab_cve_2026_85706_arbitrary_file_read}%
[+] FLAG: EQST{gitlab_cve_2026_85706_arbitrary_file_read}
O exploit extrai o conteúdo com a expressão invalid %-encoding \((.*)\) sobre o corpo da resposta, e reconhece a ausência de arquivo pela mensagem local file not present, que serve para confirmar apenas que o ponto vulnerável foi alcançado [1].
O valor de uma leitura arbitrária de arquivos depende inteiramente de o que existe no disco para ser lido. Num servidor GitLab, existe muito [1] [2].
A leitura alcança arquivos que guardam as chaves e as credenciais mestras do servidor [1] [2]:
| Arquivo | Conteúdo e consequência |
|---|---|
| gitlab-secrets.json | Chaves de criptografia da instância, usadas para cifrar tokens, variáveis de CI/CD e segredos armazenados. Sua leitura permite decifrar segredos e forjar sessões [1] [2]. |
| database.yml | Credenciais de acesso ao banco de dados PostgreSQL do GitLab [1] [2]. |
| Registros de aplicação (logs) | Frequentemente contêm um % inválido, então têm conteúdo refletido pelo canal de vazamento. Podem expor tokens, parâmetros e caminhos internos [1] [2]. |
| Configurações e credenciais diversas | Arquivos que embutem valores codificados na forma URL entram no canal de divulgação de conteúdo [1] [2]. |
A cadeia de consequências é direta [1] [2] [8]. A leitura sem autenticação expõe segredos; os segredos dão acesso autenticado ao GitLab; o acesso autenticado a um servidor de código dá acesso ao código-fonte, aos pipelines de CI/CD, às variáveis de ambiente e às credenciais de implantação que a organização guarda ali [2] [8]. Um servidor GitLab é, para muitas empresas, o centro do desenvolvimento e da entrega de software, o que faz dele um alvo de alto retorno e um ponto de partida para ataques à cadeia de suprimento de software [8].
Não trate o incidente como "apenas leitura de arquivos". A leitura do gitlab-secrets.json entrega as chaves que cifram todos os outros segredos da instância. Uma única requisição bem-sucedida contra esse arquivo, se ele contiver o byte que aciona a reflexão, converte o acesso anônimo em controle sobre os segredos do servidor [1] [2].
Diferente de muitas falhas divulgadas de forma teórica, o CVE-2026-85706 passou a ser sondado na internet quase imediatamente após a correção [6] [8]. A tabela a seguir consolida a linha do tempo pública [3] [6] [7] [8].
| Data | Evento |
|---|---|
| Antes da divulgação | Falha reportada pelo pesquisador "s3ntago" via programa de recompensas da GitLab no HackerOne [7] |
| 10 de setembro de 2026 | GitLab publica as versões corrigidas 19.3.2, 19.2.6 e 19.1.8 e o comunicado de segurança [3] |
| 11 de setembro de 2026 | Sondagens contra servidores GitLab expostos observadas na internet; CVE adicionado ao catálogo KEV da CISA [6] [8] |
| 23 de setembro de 2026 | Correção retroportada para as ramificações mais antigas: 19.0.9 e 18.11.12 [7] |
A inclusão no catálogo KEV da CISA significa que há evidência de exploração real, e não apenas potencial [6]. Para os órgãos federais civis dos Estados Unidos, essa inclusão impõe um prazo de correção obrigatório; para qualquer outra organização, é o sinal mais forte de que a correção não pode esperar pela próxima janela de manutenção [6] [7].
A janela entre a correção e a exploração ativa foi de cerca de um dia [3] [6] [8]. Isso reforça um ponto operacional: para vulnerabilidades de servidores de código expostos, o intervalo entre "há correção" e "há exploração em massa" é curto demais para depender de ciclos mensais de atualização.
A detecção segue da observação mais barata e abrangente para a mais específica: identificar as requisições de exploração na borda, depois nos registros da aplicação [1]. O padrão da requisição é distintivo e não aparece em uso legítimo do GitLab [1].
A marca mais confiável é a presença do percent-encoding na palavra commits dentro do caminho da API [1]. Nenhum cliente legítimo do GitLab escreve %63ommits; a rota normal é commits em texto claro [1]. Da mesma forma, uma requisição à API de commits que carregue um parâmetro file.path com um caminho absoluto do sistema (iniciado por /) é anômala [1].
Para servidores web e proxies que registram a URL requisitada, busque o padrão nos registros históricos e alerte em tempo real:
/api/v4/projects/[^/]+/repository/%[0-9a-fA-F]{2}ommits
Para sensores de rede que inspecionam o tráfego HTTP antes da terminação TLS (por exemplo, atrás de um descarregador de TLS), a regra abaixo alerta o padrão de rota codificada combinado com o parâmetro de caminho absoluto [1]:
alert http $EXTERNAL_NET any -> $HOME_NET any (msg:"CIBERLAB GITLAB CVE-2026-85706 UNAUTH FILE READ ATTEMPT"; \
flow:to_server,established; http.method; content:"POST"; \
http.uri; content:"/repository/"; content:"ommits"; distance:0; content:"file.path="; \
pcre:"/\/repository\/%[0-9a-f]{2}ommits/i"; \
classtype:web-application-attack; sid:1000110; rev:1; \
reference:cve,2026-85706; reference:url,ciberlab.seg.br/reports/gitlab-fileread;)
A regra Sigma a seguir opera sobre registros de proxy web ou de servidor web que exponham o campo da URI requisitada [1]:
title: Tentativa de Leitura Arbitraria de Arquivo no GitLab (CVE-2026-85706)
id: 5234914f-ec3a-473a-b4ca-6d22e88ad2ee
status: experimental
description: Detecta a rota commits com uma letra percent-encoded e o parametro file.path absoluto
author: Lucas Rayan Guerra (CiberLab)
references:
- https://github.com/EQSTLab/CVE-2026-85706
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2026-85706
logsource:
category: webserver
detection:
selection_route:
cs-uri-stem|re: '/api/v4/projects/[^/]+/repository/%[0-9a-fA-F]{2}ommits'
selection_param:
cs-uri-query|contains: 'file.path=/'
condition: selection_route or selection_param
fields:
- c-ip
- cs-uri-stem
- cs-uri-query
- sc-status
falsepositives:
- Desconhecidos em uso legitimo do GitLab
level: high
tags:
- attack.initial_access
- attack.t1190
- attack.collection
- attack.t1005
Além da requisição, dois sinais ajudam a distinguir tentativa de sucesso [1]. Respostas HTTP 400 da API de commits contendo a cadeia invalid %-encoding indicam que o canal de vazamento de conteúdo foi acionado, ou seja, que um arquivo foi lido e refletido [1]. Uma sucessão de requisições à API de commits com valores variados de file.path, partindo de um mesmo endereço sem autenticação, caracteriza a varredura de arquivos [1]. Após qualquer indício, revise os registros de acesso subsequentes em busca de autenticação bem-sucedida com credenciais que possam ter vazado (seção 9) [2] [8].
A única solução completa é atualizar o GitLab autogerenciado para uma versão corrigida [3] [4]. A correção adiciona a verificação de autenticação antes do auxiliar de leitura, de modo que a requisição anônima é recusada antes de tocar o disco [1]:
Instâncias hospedadas no serviço gerenciado da GitLab (GitLab.com) e no GitLab Dedicated foram tratadas pela própria GitLab; a ação de atualização recai sobre as instalações autogerenciadas [4] [8].
Se a atualização imediata não for possível, reduza a superfície de ataque [2] [8]:
POST e PUT às rotas */repository/commits* e */repository/files*, inclusive nas formas percent-encoded do caminho [1] [8].Como a leitura acontecia sem autenticação e sem registro de sessão, uma instância que esteve acessível na internet antes da atualização deve ser tratada sob a premissa de comprometimento (Assume Breach) [2] [8]. A correção fecha a porta, mas não desfaz o que já pode ter vazado [8].
gitlab-secrets.json e as credenciais do banco em database.yml, seguindo o procedimento oficial da GitLab para rotação de segredos [2] [8].A tabela alinha as ações da exploração ao framework MITRE ATT&CK for Enterprise, com IDs e nomes conferidos na base oficial [9]:
| Técnica | Nome | Relação com o caso |
|---|---|---|
| T1595 | Active Scanning | Sondagem em massa de servidores GitLab expostos após a divulgação [6] [8] |
| T1190 | Exploit Public-Facing Application | Exploração da API de commits do GitLab exposta à internet, sem autenticação [1] [4] |
| T1083 | File and Directory Discovery | Uso do oráculo de existência (resposta 401) para mapear arquivos no servidor [1] |
| T1005 | Data from Local System | Leitura de arquivos arbitrários do sistema de arquivos do servidor [1] [2] |
| T1552.001 | Unsecured Credentials: Credentials In Files | Extração de credenciais e chaves de gitlab-secrets.json e database.yml [1] [2] |
| T1078 | Valid Accounts | Reuso das credenciais e tokens vazados para obter acesso autenticado à instância [2] [8] |
Os indicadores desta vulnerabilidade são de comportamento, não de arquivo: a exploração usa apenas requisições HTTP legítimas na forma, e não deposita binários no servidor [1]. A tabela consolida os padrões a caçar nos registros e no tráfego [1].
| Tipo | Indicador | Severidade | Contexto e ação |
|---|---|---|---|
| Vulnerabilidade | CVE-2026-85706 | Crítica | Leitura arbitrária de arquivos sem autenticação no GitLab CE/EE. Atualize de imediato [3] [4]. |
| Padrão de URL | /repository/%63ommits | Crítica | Rota commits com a letra c percent-encoded; nenhum cliente legítimo a usa [1]. |
| Padrão de URL | /repository/%[0-9a-fA-F]{2}ommits | Alta | Forma geral do desvio de rota, cobrindo outras letras codificadas [1]. |
| Parâmetro | file.path=/<caminho absoluto> | Alta | Caminho absoluto do sistema na API de commits; anômalo em uso legítimo [1]. |
| Resposta HTTP | 400 com "invalid %-encoding" | Crítica | Indica leitura bem-sucedida com conteúdo refletido; provável exfiltração de arquivo [1]. |
| Resposta HTTP | "local file not present" | Média | Confirma que o ponto vulnerável foi alcançado com um caminho inexistente (sondagem) [1]. |
| Arquivo-alvo | gitlab-secrets.json, database.yml | Crítica | Alvos de maior valor; se acessados, presuma vazamento de segredos e gire tudo [1] [2]. |
% inválido. Arquivos sem esse byte retornam apenas o oráculo de existência [1]. O alcance prático da exfiltração de um arquivo específico depende, portanto, do seu conteúdo, e não pode ser presumido universal.%63ommits, canal de vazamento por reparse urlencoded, ambiente de laboratório e exploit em Python.
https://github.com/EQSTLab/CVE-2026-85706
CVE-2026-85706, leitura arbitrária de arquivos sem autenticação no GitLab. Relatório técnico de análise de vulnerabilidade, versão 1.0, emitido em 29 de setembro de 2026.
Lucas Rayan Guerra, CiberLab, uma iniciativa do Ciência Embarcada.
Classificado TLP:CLEAR: distribuição livre, sem restrição de compartilhamento.