
Análise de causa raiz, laboratório Docker vulnerável e scripts PoC para CVE-2026-85706, uma leitura arbitrária de arquivos não autenticada no GitLab via diferencial de parser.
CVSS 10.0 · Não autenticada · Explorada ativamente (CISA KEV)
Uma diferença de parser entre o GitLab Workhorse (proxy reverso em Go) e o Puma/Grape
(Ruby) permite que um atacante não autenticado contorne a transferência de upload acelerado
do Workhorse e alcance três endpoints de upload com um file.path controlado pelo atacante,
resultando em leitura arbitrária de arquivos no host do GitLab.
Codificar um caractere de files como %66iles faz com que a rota de upload do Workhorse
não corresponda (ela faz a correspondência no caminho codificado), enquanto o Rails o
decodifica de volta e atinge o handler real (ele roteia no caminho decodificado). O handler
confia no parâmetro bruto file.path que o Workhorse deveria ter sobrescrito — então
file.path=/etc/passwd é lido do disco sem credenciais.
POST /api/v4/projects/1/repository/%66iles/x?file=&file.path=/etc/passwd&file.size=1
^^^^^^ Workhorse misses -> raw file.path survives to Rails
| Versão | |
|---|---|
| Afetadas | CE/EE 18.7 → 19.1.8, 19.2 → 19.2.6, 19.3 → 19.3.2 |
| Corrigidas | 19.1.8 / 19.2.6 / 19.3.2 (2026-09-10) |
| Caminho | Conteúdo |
|---|---|
docs/ANALYSIS.md | Análise completa da causa raiz — diferença de parser, os dois JWTs do Workhorse, o bug de confiança no file.path bruto, o diff do patch e o canal de exfiltração por erro refletido. Análise feita contra o código-fonte real (v19.3.1-ee vs v19.3.2-ee). |
lab/ | Laboratório vulnerável baseado em Docker (gitlab-ce:19.3.1-ce.0) + instruções de inicialização. |
poc/ | detect.sh (oráculo de existência não destrutivo) e exploit.sh (leitura de arquivo por erro refletido). Alvo único, com autorização obrigatória. |
Não está familiarizado com os internos do GitLab? Aqui está o que cada camada faz — na ordem
em que uma requisição passa por elas. Um glossário mais detalhado com conceitos-chave está em
docs/ANALYSIS.md §0.
Internet
│
▼
┌────────┐ ┌───────────┐ ┌────────┐ ┌──────────────────────┐
│ NGINX │────▶│ Workhorse │────▶│ Puma │────▶│ Grape / Rails app │
│(proxy) │ │ (Go) │ │ (Ruby) │ │ (Ruby) │
└────────┘ └───────────┘ └────────┘ └──────────────────────┘
| Componente | O que é |
|---|---|
| NGINX | Proxy reverso mais externo. Termina o TLS, serve arquivos estáticos, encaminha todo o resto para dentro. Não está diretamente envolvido nesta vulnerabilidade. |
| Workhorse | Um proxy reverso em Go específico do GitLab. Sua principal função é descarregar o trabalho no qual o Ruby é lento — especialmente streaming de uploads grandes. Para endpoints de upload, o Workhorse armazena o corpo em um arquivo temporário, assina um JWT e reescreve file.path para que o Ruby nunca veja os bytes brutos do upload. Ele também carimba um JWT Gitlab-Workhorse-Api-Request por requisição em toda requisição que encaminha (provando "isto veio através do proxy", não "este usuário está autenticado"). |
| Puma | O servidor de aplicação Ruby que executa o app Rails. Recebe requisições do Workhorse, executa middleware (Rack) e despacha para o roteador. |
| Rack | A camada de interface do servidor web Ruby. O middleware Rack lida com a análise da query-string, gerenciamento de sessão e — criticamente aqui — verificação do JWT de upload do Workhorse e construção de objetos UploadedFile. Rack::Utils.parse_nested_query é a função cujas mensagens de erro vazam conteúdo de arquivo neste exploit. |
| Grape | Um framework de API REST usado pelo GitLab para todos os endpoints /api/v4/*. Fornece definições de rota e before-filters como require_gitlab_workhorse! (verificação de proxy) e authenticate! (verificação de identidade do usuário). Executa dentro do Rails, no Puma, atrás do Workhorse — então ele vê o caminho de URL decodificado. |
| Rails | O framework web geral (Ruby on Rails). O GitLab é um monólito Rails — models, services e middleware todos executam aqui dentro do Puma. |
A vulnerabilidade está na lacuna entre o Workhorse (que faz correspondência de rotas no caminho codificado) e o Grape/Puma (que roteiam no caminho decodificado). Veja abaixo.
r.URL.EscapedPath()
(codificado); o Puma/Grape roteia no caminho decodificado. %66iles ≠ regex files para
o Workhorse, mas decodifica para files para o Rails.file.path para um caminho
temporário assinado e nunca define o cabeçalho Gitlab-Workhorse-Multipart-Fields — mas
ele ainda faz proxy da requisição (com um JWT Gitlab-Workhorse-Api-Request válido).authenticate!, e o handler lia params['file.path'] diretamente (a string do atacante)
em vez do UploadedFile verificado pelo Workhorse e confinado ao caminho params[:file].Rack::Utils.parse_nested_query ("invalid %-encoding (<file bytes>)") — a resposta
reflete os bytes do arquivo até o primeiro % inválido.O patch adiciona authenticate!, muda para o UploadedFile verificado e para de refletir
e.message. Veja docs/ANALYSIS.md §5.
# 1. Stand up the vulnerable lab (see lab/README.md for details)
cd lab && docker compose up -d # wait ~5 min for GitLab to become healthy
# 2. Non-destructive detection
../poc/detect.sh http://localhost:8929
# 3. File-read PoC against a file you're authorized to read on your own lab
../poc/exploit.sh http://localhost:8929 /var/opt/gitlab/gitlab-rails/etc/gitlab.yml
Isto é publicado para propósitos defensivos e educacionais: entender, detectar e corrigir uma CVE divulgada e corrigida que está na lista CISA KEV. Os scripts aqui são de alvo único e exigem que você passe o alvo explicitamente.
localhost. Não o aponte para hosts de terceiros.Se você executa o GitLab, atualize para uma versão corrigida — essa é a única remediação real.
gitlab-org/gitlab @ v19.3.1-ee vs v19.3.2-eeMIT — apenas análise e código PoC. GitLab é uma marca registrada da GitLab Inc.; este repositório não é afiliado nem endossado pela GitLab Inc.