
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 diferencial de parser.
CVSS 10.0 · Sin autenticación · Explotada activamente (CISA KEV)
Una diferencia de análisis sintáctico (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 una 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 sintáctico, 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 el interior. 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 filtros previos 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 el análisis y el código PoC. GitLab es una marca registrada de GitLab Inc.; este repositorio no está afiliado ni respaldado por GitLab Inc.