
Анализ первопричины, уязвимая лаборатория Docker и PoC-скрипты для CVE-2026-85706 — неаутентифицированного произвольного чтения файлов в GitLab через расхождение парсеров.
CVSS 10.0 · Без аутентификации · Активно эксплуатируется (CISA KEV)
Различие в парсинге между GitLab Workhorse (обратный прокси на Go) и Puma/Grape
(Ruby) позволяет неаутентифицированному злоумышленнику обойти ускоренную передачу
загрузок Workhorse и достичь трёх эндпоинтов загрузки с контролируемым злоумышленником
file.path, что приводит к произвольному чтению файлов на хосте GitLab.
Кодирование одного символа в files как %66iles заставляет маршрут загрузки Workhorse
промахнуться (он сопоставляет по закодированному пути), в то время как Rails
декодирует его обратно и попадает в реальный обработчик (он маршрутизирует по
декодированному пути). Обработчик доверяет сырому параметру file.path, который
Workhorse должен был перезаписать — поэтому file.path=/etc/passwd читается с диска без
учётных данных.
POST /api/v4/projects/1/repository/%66iles/x?file=&file.path=/etc/passwd&file.size=1
^^^^^^ Workhorse misses -> raw file.path survives to Rails
| Версия | |
|---|---|
| Уязвимые | CE/EE 18.7 → 19.1.8, 19.2 → 19.2.6, 19.3 → 19.3.2 |
| Исправленные | 19.1.8 / 19.2.6 / 19.3.2 (2026-09-10) |
| Путь | Содержимое |
|---|---|
docs/ANALYSIS.md | Полный анализ первопричины — различие в парсинге, два JWT Workhorse, баг доверия к сырому file.path, диф патча и канал эксфильтрации через отражённые ошибки. Анализ выполнен по реальному исходному коду (v19.3.1-ee против v19.3.2-ee). |
lab/ | Уязвимая лаборатория на базе Docker (gitlab-ce:19.3.1-ce.0) + инструкции по развёртыванию. |
poc/ | detect.sh (неразрушающий оракул существования) и exploit.sh (чтение файлов через отражённые ошибки). Одна цель, с обязательной авторизацией. |
Не знакомы с внутренним устройством GitLab? Вот что делает каждый слой — в порядке
прохождения запроса через них. Более глубокий глоссарий с ключевыми концепциями находится
в docs/ANALYSIS.md §0.
Internet
│
▼
┌────────┐ ┌───────────┐ ┌────────┐ ┌──────────────────────┐
│ NGINX │────▶│ Workhorse │────▶│ Puma │────▶│ Grape / Rails app │
│(proxy) │ │ (Go) │ │ (Ruby) │ │ (Ruby) │
└────────┘ └───────────┘ └────────┘ └──────────────────────┘
| Компонент | Что это |
|---|---|
| NGINX | Внешний обратный прокси. Завершает TLS, обслуживает статические файлы, перенаправляет всё остальное внутрь. Напрямую не участвует в этой уязвимости. |
| Workhorse | Обратный прокси на Go, специфичный для GitLab. Его основная задача — снять нагрузку, с которой Ruby справляется медленно, особенно потоковую передачу больших загрузок. Для эндпоинтов загрузки Workhorse буферизует тело во временный файл, подписывает JWT и перезаписывает file.path, чтобы Ruby никогда не видел сырые байты загрузки. Он также проставляет JWT Gitlab-Workhorse-Api-Request для каждого запроса, который пересылает (доказывая «это пришло через прокси», а не «этот пользователь аутентифицирован»). |
| Puma | Сервер приложений на Ruby, который запускает приложение Rails. Получает запросы от Workhorse, выполняет middleware (Rack) и передаёт их маршрутизатору. |
| Rack | Интерфейсный слой веб-сервера на Ruby. Middleware Rack обрабатывает парсинг строки запроса, управление сессиями и — что критично здесь — проверку JWT загрузки Workhorse и создание объектов UploadedFile. Rack::Utils.parse_nested_query — это функция, сообщения об ошибках которой утекают содержимое файлов в этом эксплойте. |
| Grape | Фреймворк REST API, используемый GitLab для всех эндпоинтов /api/v4/*. Предоставляет определения маршрутов и before-фильтры, такие как require_gitlab_workhorse! (проверка прокси) и authenticate! (проверка личности пользователя). Работает внутри Rails, на Puma, за Workhorse — поэтому видит декодированный путь URL. |
| Rails | Общий веб-фреймворк (Ruby on Rails). GitLab — это монолит на Rails — модели, сервисы и middleware работают здесь внутри Puma. |
Уязвимость живёт в разрыве между Workhorse (который сопоставляет маршруты по закодированному пути) и Grape/Puma (которые маршрутизируют по декодированному пути). См. ниже.
r.URL.EscapedPath()
(закодированному); Puma/Grape маршрутизирует по декодированному пути. %66iles ≠
регулярному выражению files для Workhorse, но декодируется в files для Rails.file.path на
подписанный временный путь и никогда не устанавливает заголовок
Gitlab-Workhorse-Multipart-Fields — но он всё равно проксирует запрос (с
действительным JWT Gitlab-Workhorse-Api-Request).authenticate!, и обработчик читал params['file.path'] напрямую (строку
злоумышленника) вместо проверенного Workhorse и ограниченного путём params[:file]
UploadedFile.Rack::Utils.parse_nested_query ("invalid %-encoding (<file bytes>)") — ответ
отражает байты файла до первого недопустимого %.Патч добавляет authenticate!, переключается на проверенный UploadedFile и перестаёт
отражать e.message. См. 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
Это опубликовано в защитных и образовательных целях: понимание, обнаружение и исправление раскрытой и уже исправленной CVE, которая находится в списке CISA KEV. Скрипты здесь работают с одной целью и требуют явной передачи цели.
localhost. Не направляйте её на сторонние хосты.Если вы используете GitLab, обновитесь до исправленной версии — это единственное реальное средство устранения.
gitlab-org/gitlab @ v19.3.1-ee против v19.3.2-eeMIT — только анализ и код PoC. GitLab является товарным знаком GitLab Inc.; этот репозиторий не связан с GitLab Inc. и не одобрен ею.