## https://sploitus.com/exploit?id=FDE2E47C-D236-5CCF-8BDD-C05F179A3A2D
# CVE-2026-85706 β GitLab Unauthenticated Arbitrary File Read
> **CVSS 10.0 Β· Unauthenticated Β· Actively exploited (CISA KEV)**
A parser differential between **GitLab Workhorse** (Go reverse proxy) and **Puma/Grape**
(Ruby) lets an unauthenticated attacker bypass Workhorse's accelerated-upload handoff and
reach three upload endpoints with an attacker-controlled `file.path`, yielding **arbitrary
file read** on the GitLab host.
Encoding one character of `files` as `%66iles` makes Workhorse's upload route **miss** (it
matches on the *encoded* path) while Rails decodes it back and hits the real handler (it
routes on the *decoded* path). The handler trusts the raw `file.path` param that Workhorse
was supposed to overwrite β so `file.path=/etc/passwd` is read from disk with no credentials.
```
POST /api/v4/projects/1/repository/%66iles/x?file=&file.path=/etc/passwd&file.size=1
^^^^^^ Workhorse misses -> raw file.path survives to Rails
```
## Affected / fixed
| | Version |
|---|---|
| **Affected** | CE/EE `18.7 β 19.1.8`, `19.2 β 19.2.6`, `19.3 β 19.3.2` |
| **Fixed** | `19.1.8` / `19.2.6` / `19.3.2` (2026-09-10) |
## What's in this repo
| Path | Contents |
|---|---|
| [`docs/ANALYSIS.md`](docs/ANALYSIS.md) | Full root-cause analysis β parser differential, the two Workhorse JWTs, the raw-`file.path` trust bug, the patch diff, and the reflected-error exfiltration channel. Analysis done against real source (`v19.3.1-ee` vs `v19.3.2-ee`). |
| [`lab/`](lab/) | Docker-based vulnerable lab (`gitlab-ce:19.3.1-ce.0`) + bring-up instructions. |
| [`poc/`](poc/) | `detect.sh` (non-destructive existence oracle) and `exploit.sh` (reflected-error file read). Single-target, authorization-gated. |
## Background β GitLab's request pipeline
Not familiar with GitLab internals? Here's what each layer does β in the order a request
passes through them. A deeper glossary with key concepts is in
[`docs/ANALYSIS.md` Β§0](docs/ANALYSIS.md#0-glossary--gitlabs-request-pipeline).
```
Internet
β
βΌ
ββββββββββ βββββββββββββ ββββββββββ ββββββββββββββββββββββββ
β NGINX ββββββΆβ Workhorse ββββββΆβ Puma ββββββΆβ Grape / Rails app β
β(proxy) β β (Go) β β (Ruby) β β (Ruby) β
ββββββββββ βββββββββββββ ββββββββββ ββββββββββββββββββββββββ
```
| Component | What it is |
|---|---|
| **NGINX** | Outermost reverse proxy. Terminates TLS, serves static files, forwards everything else inward. Not directly involved in this vulnerability. |
| **Workhorse** | A Go reverse proxy specific to GitLab. Its main job is offloading work Ruby is slow at β especially **streaming large uploads**. For upload endpoints, Workhorse buffers the body to a temp file, signs a JWT, and rewrites `file.path` so Ruby never sees raw upload bytes. It also stamps a per-request `Gitlab-Workhorse-Api-Request` JWT on *every* request it forwards (proving "this came through the proxy," **not** "this user is authenticated"). |
| **Puma** | The Ruby application server that runs the Rails app. Receives requests from Workhorse, runs middleware (Rack), and dispatches to the router. |
| **Rack** | The Ruby web-server interface layer. Rack middleware handles query-string parsing, session management, and β critically here β verifying Workhorse's upload JWT and building `UploadedFile` objects. `Rack::Utils.parse_nested_query` is the function whose error messages leak file content in this exploit. |
| **Grape** | A REST API framework used by GitLab for all `/api/v4/*` endpoints. Provides route definitions and before-filters like `require_gitlab_workhorse!` (proxy check) and `authenticate!` (user identity check). Runs inside Rails, on Puma, behind Workhorse β so it sees the **decoded** URL path. |
| **Rails** | The overall web framework (Ruby on Rails). GitLab is a Rails monolith β models, services, and middleware all run here inside Puma. |
The vulnerability lives in the gap between **Workhorse** (which matches routes on the
*encoded* path) and **Grape/Puma** (which route on the *decoded* path). See below.
## TL;DR of the mechanism
1. **Parser differential.** Workhorse matches routes on `r.URL.EscapedPath()` (encoded);
Puma/Grape route on the decoded path. `%66iles` β regex `files` for Workhorse, but
decodes to `files` for Rails.
2. **Handoff skipped.** Because the upload route missed, Workhorse never buffers the body
to a temp file, never rewrites `file.path` to a signed temp path, and never sets the
`Gitlab-Workhorse-Multipart-Fields` header β but it *still proxies* the request (with a
valid `Gitlab-Workhorse-Api-Request` JWT).
3. **Missing auth + raw-param trust.** The endpoint had no `authenticate!`, and the handler
read `params['file.path']` directly (the attacker's string) instead of the
Workhorse-verified, path-confined `params[:file]` `UploadedFile`.
4. **Exfil via error messages.** Content leaks through `Rack::Utils.parse_nested_query`
error strings (`"invalid %-encoding ()"`) β the response reflects file bytes
up to the first invalid `%`.
The patch adds `authenticate!`, switches to the verified `UploadedFile`, and stops
reflecting `e.message`. See [`docs/ANALYSIS.md`](docs/ANALYSIS.md) Β§5.
## Quick start
```bash
# 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
```
## β οΈ Responsible use
This is published for **defensive and educational purposes**: understanding, detecting, and
patching a disclosed, patched CVE that is on the CISA KEV list. The scripts here are
**single-target** and require you to pass the target explicitly.
- Only run these against systems **you own or are explicitly authorized to test.**
- The lab is designed to run on `localhost`. Do not point it at third-party hosts.
- Unauthorized access to computer systems is illegal in most jurisdictions.
If you run GitLab, **upgrade to a fixed version** β that is the only real remediation.
## References
- watchTowr β [Rapid Reaction: GitLab Path Traversal (CVE-2026-85706)](https://watchtowr.com/resources/rapid-reaction-gitlab-critical-path-traversal-vulnerability-cve-2026-85706/)
- Nuclei Templates β [PR #17231](https://github.com/projectdiscovery/nuclei-templates/pull/17231)
- Primary source diff: `gitlab-org/gitlab` @ `v19.3.1-ee` vs `v19.3.2-ee`
## License
[MIT](LICENSE) β analysis and PoC code only. GitLab is a trademark of GitLab Inc.; this
repository is not affiliated with or endorsed by GitLab Inc.