
CVE-2026-85706에 대한 근본 원인 분석, 취약한 Docker 랩, 그리고 PoC 스크립트로, 파서 차이를 통해 GitLab에서 발생하는 인증되지 않은 임의 파일 읽기 취약점입니다.
CVSS 10.0 · 인증 불필요 · 실제 악용 중 (CISA KEV)
GitLab Workhorse(Go 리버스 프록시)와 Puma/Grape(Ruby) 간의 파서 차이로 인해
인증되지 않은 공격자가 Workhorse의 가속 업로드 핸드오프를 우회하고, 공격자가 제어하는
file.path를 사용하여 세 개의 업로드 엔드포인트에 도달할 수 있으며, 이로 인해 GitLab
호스트에서 임의 파일 읽기가 가능합니다.
files의 한 문자를 %66iles로 인코딩하면 Workhorse의 업로드 라우트가 매치되지 않고
(Workhorse는 인코딩된 경로로 매칭함), Rails는 이를 다시 디코딩하여 실제 핸들러에
도달합니다(Rails는 디코딩된 경로로 라우팅함). 핸들러는 Workhorse가 덮어쓰기로 되어
있던 원시 file.path 파라미터를 신뢰하므로, 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 | 전체 근본 원인 분석 — 파서 차이, 두 개의 Workhorse JWT, 원시 file.path 신뢰 버그, 패치 diff, 그리고 반사된 오류를 통한 유출 채널. 실제 소스(v19.3.1-ee vs 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 | GitLab 전용 Go 리버스 프록시. 주된 역할은 Ruby가 느린 작업 — 특히 대용량 업로드 스트리밍 — 을 오프로딩하는 것입니다. 업로드 엔드포인트의 경우, Workhorse는 본문을 임시 파일로 버퍼링하고, JWT에 서명하고, file.path를 재작성하여 Ruby가 원시 업로드 바이트를 절대 보지 못하게 합니다. 또한 전달하는 모든 요청에 요청별 Gitlab-Workhorse-Api-Request JWT를 찍습니다("이것이 프록시를 통해 왔다"는 것을 증명하며, "이 사용자가 인증되었다"는 것이 아님). |
| Puma | Rails 앱을 실행하는 Ruby 애플리케이션 서버. Workhorse로부터 요청을 받아 미들웨어(Rack)를 실행하고 라우터로 디스패치합니다. |
| Rack | Ruby 웹 서버 인터페이스 계층. Rack 미들웨어는 쿼리 문자열 파싱, 세션 관리, 그리고 여기서 결정적으로 Workhorse의 업로드 JWT 검증 및 UploadedFile 객체 생성을 처리합니다. Rack::Utils.parse_nested_query는 이 익스플로잇에서 오류 메시지가 파일 내용을 유출하는 함수입니다. |
| Grape | GitLab이 모든 /api/v4/* 엔드포인트에 사용하는 REST API 프레임워크. 라우트 정의와 require_gitlab_workhorse!(프록시 확인) 및 authenticate!(사용자 신원 확인)와 같은 before-filter를 제공합니다. Rails 내부, Puma 위, Workhorse 뒤에서 실행되므로 디코딩된 URL 경로를 봅니다. |
| Rails | 전체 웹 프레임워크(Ruby on Rails). GitLab은 Rails 모놀리스로, 모델, 서비스, 미들웨어가 모두 Puma 내부에서 실행됩니다. |
이 취약점은 Workhorse(인코딩된 경로로 라우트를 매칭)와 Grape/Puma(디코딩된 경로로 라우팅) 사이의 간극에 존재합니다. 아래를 참조하세요.
r.URL.EscapedPath()(인코딩됨)로 라우트를 매칭하고,
Puma/Grape는 디코딩된 경로로 라우팅합니다. Workhorse에게 %66iles ≠ 정규식 files이지만,
Rails에게는 files로 디코딩됩니다.file.path를 서명된 임시 경로로 재작성하지 않으며,
Gitlab-Workhorse-Multipart-Fields 헤더를 설정하지 않습니다 — 하지만 여전히 요청을
프록시합니다(유효한 Gitlab-Workhorse-Api-Request JWT와 함께).authenticate!가 없었고, 핸들러는
Workhorse가 검증하고 경로가 제한된 params[:file] UploadedFile 대신 params['file.path']
(공격자의 문자열)를 직접 읽었습니다.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
이것은 방어 및 교육 목적으로 공개되었습니다: CISA KEV 목록에 있는 공개되고 패치된 CVE를 이해하고, 탐지하고, 패치하기 위함입니다. 여기의 스크립트는 단일 대상이며 대상을 명시적으로 전달해야 합니다.
localhost에서 실행되도록 설계되었습니다. 제3자 호스트를 향하게 하지 마세요.GitLab을 운영 중이라면, 수정된 버전으로 업그레이드하세요 — 그것이 유일한 실질적 해결책입니다.
gitlab-org/gitlab @ v19.3.1-ee vs v19.3.2-eeMIT — 분석 및 PoC 코드에만 해당. GitLab은 GitLab Inc.의 상표이며, 이 저장소는 GitLab Inc.와 제휴하거나 보증받지 않았습니다.