
CVE-2026-85706에 대한 근본 원인 분석, 취약한 Docker 랩, PoC 스크립트로, Workhorse/Puma 파서 차이를 통해 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. 취약 랩 구동 (자세한 내용은 lab/README.md 참조)
cd lab && docker compose up -d # GitLab이 정상 상태가 될 때까지 약 5분 대기
# 2. 비파괴적 탐지
../poc/detect.sh http://localhost:8929
# 3. 자신의 랩에서 읽을 권한이 있는 파일에 대한 파일 읽기 PoC
../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.와 제휴하거나 승인받지 않았다.