
Ursachenanalyse, verwundbares Docker-Lab und PoC-Skripte für CVE-2026-85706, einen unauthentifizierten beliebigen Dateizugriff (Arbitrary File Read) in GitLab über eine Parser-Differenz.
CVSS 10.0 · Unauthenticated · Actively exploited (CISA KEV)
Ein Parser-Differential zwischen GitLab Workhorse (Go-Reverse-Proxy) und Puma/Grape
(Ruby) ermöglicht es einem nicht authentifizierten Angreifer, die Accelerated-Upload-Übergabe
von Workhorse zu umgehen und drei Upload-Endpunkte mit einem vom Angreifer kontrollierten
file.path zu erreichen, was zu einem arbitrary file read auf dem GitLab-Host führt.
Die Kodierung eines Zeichens von files als %66iles führt dazu, dass Workhorses
Upload-Route nicht greift (es matcht auf den kodierten Pfad), während Rails ihn
zurückdekodiert und den echten Handler trifft (es routet auf den dekodierten Pfad). Der
Handler vertraut dem rohen file.path-Parameter, den Workhorse eigentlich hätte
überschreiben sollen — also wird file.path=/etc/passwd ohne Anmeldedaten von der Platte
gelesen.
POST /api/v4/projects/1/repository/%66iles/x?file=&file.path=/etc/passwd&file.size=1
^^^^^^ Workhorse misses -> raw file.path survives to Rails
| Version | |
|---|---|
| Betroffen | CE/EE 18.7 → 19.1.8, 19.2 → 19.2.6, 19.3 → 19.3.2 |
| Behoben | 19.1.8 / 19.2.6 / 19.3.2 (2026-09-10) |
| Pfad | Inhalt |
|---|---|
docs/ANALYSIS.md | Vollständige Root-Cause-Analyse — Parser-Differential, die beiden Workhorse-JWTs, der Raw-file.path-Trust-Bug, der Patch-Diff und der Reflected-Error-Exfiltrationskanal. Analyse gegen echten Quellcode durchgeführt (v19.3.1-ee vs v19.3.2-ee). |
lab/ | Docker-basiertes verwundbares Lab (gitlab-ce:19.3.1-ce.0) + Anleitungen zum Hochfahren. |
poc/ | detect.sh (nicht-destruktives Existenz-Orakel) und exploit.sh (Reflected-Error-Dateilesen). Einzelziel, autorisierungsgeschützt. |
Nicht mit GitLab-Interna vertraut? Hier ist, was jede Schicht tut — in der Reihenfolge, in
der eine Anfrage sie durchläuft. Ein tiefergehendes Glossar mit Schlüsselkonzepten findet
sich in docs/ANALYSIS.md §0.
Internet
│
▼
┌────────┐ ┌───────────┐ ┌────────┐ ┌──────────────────────┐
│ NGINX │────▶│ Workhorse │────▶│ Puma │────▶│ Grape / Rails app │
│(proxy) │ │ (Go) │ │ (Ruby) │ │ (Ruby) │
└────────┘ └───────────┘ └────────┘ └──────────────────────┘
| Komponente | Was sie ist |
|---|---|
| NGINX | Äußerster Reverse-Proxy. Terminiert TLS, liefert statische Dateien aus, leitet alles andere nach innen weiter. Nicht direkt an dieser Schwachstelle beteiligt. |
| Workhorse | Ein Go-Reverse-Proxy speziell für GitLab. Seine Hauptaufgabe ist das Auslagern von Arbeit, bei der Ruby langsam ist — insbesondere das Streaming großer Uploads. Für Upload-Endpunkte puffert Workhorse den Body in eine temporäre Datei, signiert ein JWT und schreibt file.path um, sodass Ruby niemals rohe Upload-Bytes sieht. Außerdem versieht es jede weitergeleitete Anfrage mit einem Gitlab-Workhorse-Api-Request-JWT pro Anfrage (was beweist: „dies kam durch den Proxy", nicht „dieser Benutzer ist authentifiziert"). |
| Puma | Der Ruby-Anwendungsserver, der die Rails-App ausführt. Empfängt Anfragen von Workhorse, führt Middleware (Rack) aus und leitet an den Router weiter. |
| Rack | Die Ruby-Webserver-Schnittstellenschicht. Rack-Middleware übernimmt Query-String-Parsing, Session-Management und — hier entscheidend — die Verifikation von Workhorses Upload-JWT und den Aufbau von UploadedFile-Objekten. Rack::Utils.parse_nested_query ist die Funktion, deren Fehlermeldungen in diesem Exploit Dateiinhalte preisgeben. |
| Grape | Ein REST-API-Framework, das von GitLab für alle /api/v4/*-Endpunkte verwendet wird. Stellt Routendefinitionen und Before-Filter wie require_gitlab_workhorse! (Proxy-Prüfung) und authenticate! (Benutzeridentitätsprüfung) bereit. Läuft innerhalb von Rails, auf Puma, hinter Workhorse — sieht also den dekodierten URL-Pfad. |
| Rails | Das übergeordnete Web-Framework (Ruby on Rails). GitLab ist ein Rails-Monolith — Modelle, Services und Middleware laufen alle hier innerhalb von Puma. |
Die Schwachstelle liegt in der Lücke zwischen Workhorse (das Routen auf dem kodierten Pfad matcht) und Grape/Puma (die auf dem dekodierten Pfad routen). Siehe unten.
r.URL.EscapedPath() (kodiert);
Puma/Grape routen auf dem dekodierten Pfad. %66iles ≠ Regex files für Workhorse, aber
dekodiert zu files für Rails.file.path niemals in einen signierten
temporären Pfad um und setzt niemals den Header Gitlab-Workhorse-Multipart-Fields — aber
es proxyt die Anfrage trotzdem (mit einem gültigen Gitlab-Workhorse-Api-Request-JWT).authenticate!, und der Handler las params['file.path'] direkt (den String des
Angreifers) statt der von Workhorse verifizierten, pfadbeschränkten params[:file]
UploadedFile.Rack::Utils.parse_nested_query-Fehlerstrings ("invalid %-encoding (<file bytes>)") nach
außen — die Antwort reflektiert Dateibytes bis zum ersten ungültigen %.Der Patch fügt authenticate! hinzu, wechselt zur verifizierten UploadedFile und
unterlässt das Reflektieren von e.message. Siehe 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
Dies wird für defensive und edukative Zwecke veröffentlicht: das Verstehen, Erkennen und Patchen einer offengelegten, gepatchten CVE, die auf der CISA-KEV-Liste steht. Die Skripte hier sind Einzelziel und erfordern, dass Sie das Ziel explizit angeben.
localhost zu laufen. Richten Sie es nicht auf
Drittanbieter-Hosts.Wenn Sie GitLab betreiben, aktualisieren Sie auf eine behobene Version — das ist die einzige echte Behebung.
gitlab-org/gitlab @ v19.3.1-ee vs v19.3.2-eeMIT — nur Analyse- und PoC-Code. GitLab ist eine Marke von GitLab Inc.; dieses Repository ist nicht mit GitLab Inc. verbunden und wird nicht von GitLab Inc. unterstützt.