
Análisis de causa raíz, laboratorio Docker vulnerable y scripts PoC para CVE-2026-85706, una lectura arbitraria de archivos sin autenticación en GitLab mediante un diferencial de analizador Workhorse/Puma.
CVSS 10.0 · Sin autenticación · Explotada activamente (CISA KEV)
Una diferencia de análisis (parser differential) entre GitLab Workhorse (proxy inverso en Go) y Puma/Grape
(Ruby) permite a un atacante no autenticado eludir el traspaso de carga acelerada de Workhorse y
alcanzar tres endpoints de carga con un file.path controlado por el atacante, lo que produce lectura arbitraria
de archivos en el host de GitLab.
Codificar un carácter de files como %66iles hace que la ruta de carga de Workhorse falle (coincide con la ruta codificada) mientras que Rails la decodifica de vuelta y llega al manejador real (enruta según la ruta decodificada). El manejador confía en el parámetro file.path sin procesar que se suponía que Workhorse debía sobrescribir — por lo que file.path=/etc/passwd se lee del disco sin credenciales.
POST /api/v4/projects/1/repository/%66iles/x?file=&file.path=/etc/passwd&file.size=1
^^^^^^ Workhorse misses -> raw file.path survives to Rails
| Versión | |
|---|---|
| Afectados | CE/EE 18.7 → 19.1.8, 19.2 → 19.2.6, 19.3 → 19.3.2 |
| Corregidos | 19.1.8 / 19.2.6 / 19.3.2 (2026-09-10) |
| Ruta | Contenido |
|---|---|
docs/ANALYSIS.md | Análisis completo de la causa raíz — diferencia de análisis, los dos JWT de Workhorse, el fallo de confianza en el file.path sin procesar, el diff del parche y el canal de exfiltración por errores reflejados. Análisis realizado contra el código fuente real (v19.3.1-ee vs v19.3.2-ee). |
lab/ | Laboratorio vulnerable basado en Docker (gitlab-ce:19.3.1-ce.0) + instrucciones de puesta en marcha. |
poc/ | detect.sh (oráculo de existencia no destructivo) y exploit.sh (lectura de archivos por errores reflejados). Objetivo único, con autorización obligatoria. |
¿No estás familiarizado con las interioridades de GitLab? Esto es lo que hace cada capa — en el orden en que una petición las atraviesa. Un glosario más detallado con conceptos clave está en
docs/ANALYSIS.md §0.
Internet
│
▼
┌────────┐ ┌───────────┐ ┌────────┐ ┌──────────────────────┐
│ NGINX │────▶│ Workhorse │────▶│ Puma │────▶│ Grape / Rails app │
│(proxy) │ │ (Go) │ │ (Ruby) │ │ (Ruby) │
└────────┘ └───────────┘ └────────┘ └──────────────────────┘
| Componente | Qué es |
|---|---|
| NGINX | Proxy inverso más externo. Termina TLS, sirve archivos estáticos y reenvía todo lo demás hacia dentro. No participa directamente en esta vulnerabilidad. |
| Workhorse | Un proxy inverso en Go específico de GitLab. Su función principal es descargar el trabajo en el que Ruby es lento — especialmente la transmisión de cargas grandes. Para los endpoints de carga, Workhorse almacena el cuerpo en un archivo temporal, firma un JWT y reescribe file.path para que Ruby nunca vea los bytes de carga sin procesar. También estampa un JWT Gitlab-Workhorse-Api-Request por petición en cada petición que reenvía (demostrando "esto llegó a través del proxy", no "este usuario está autenticado"). |
| Puma | El servidor de aplicaciones Ruby que ejecuta la aplicación Rails. Recibe peticiones de Workhorse, ejecuta middleware (Rack) y las despacha al enrutador. |
| Rack | La capa de interfaz del servidor web Ruby. El middleware de Rack gestiona el análisis de la cadena de consulta, la gestión de sesiones y — de forma crítica aquí — la verificación del JWT de carga de Workhorse y la construcción de objetos UploadedFile. Rack::Utils.parse_nested_query es la función cuyos mensajes de error filtran el contenido de archivos en este exploit. |
| Grape | Un framework de API REST usado por GitLab para todos los endpoints /api/v4/*. Proporciona definiciones de rutas y before-filters como require_gitlab_workhorse! (comprobación del proxy) y authenticate! (comprobación de identidad del usuario). Se ejecuta dentro de Rails, sobre Puma, detrás de Workhorse — por lo que ve la ruta de URL decodificada. |
| Rails | El framework web general (Ruby on Rails). GitLab es un monolito Rails — modelos, servicios y middleware se ejecutan aquí dentro de Puma. |
La vulnerabilidad reside en la brecha entre Workhorse (que hace coincidir rutas según la ruta codificada) y Grape/Puma (que enrutan según la ruta decodificada). Ver más abajo.
r.URL.EscapedPath() (codificada);
Puma/Grape enruta según la ruta decodificada. %66iles ≠ regex files para Workhorse, pero
se decodifica a files para Rails.file.path a una ruta temporal firmada y nunca establece la
cabecera Gitlab-Workhorse-Multipart-Fields — pero aun así actúa como proxy de la petición (con un
JWT Gitlab-Workhorse-Api-Request válido).authenticate!, y el manejador
leía params['file.path'] directamente (la cadena del atacante) en lugar del UploadedFile
params[:file] verificado por Workhorse y confinado a una ruta.Rack::Utils.parse_nested_query
("invalid %-encoding (<file bytes>)") — la respuesta refleja los bytes del archivo
hasta el primer % inválido.El parche añade authenticate!, cambia al UploadedFile verificado y deja de
reflejar e.message. Ver 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
Esto se publica con fines defensivos y educativos: comprender, detectar y parchear un CVE divulgado y parcheado que está en la lista CISA KEV. Los scripts aquí son de objetivo único y requieren que pases el objetivo explícitamente.
localhost. No lo apuntes a hosts de terceros.Si ejecutas GitLab, actualiza a una versión corregida — esa es la única remediación real.
gitlab-org/gitlab @ v19.3.1-ee vs v19.3.2-eeMIT — solo análisis y código PoC. GitLab es una marca registrada de GitLab Inc.; este repositorio no está afiliado ni respaldado por GitLab Inc.