X-Ops

The GitLab instance that read its own secrets: a 10.0 path traversal with a twenty-hour window

The GitLab instance that read its own secrets

On 10 September 2026, GitLab shipped security releases for 18 vulnerabilities. One of them, CVE-2026-85706, GitLab scored CVSS 3.1 base **10.0 Critical** with the vector `AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N`. It is a path traversal in the repository commits API of self-managed GitLab, and under certain conditions an unauthenticated user could read arbitrary files from the GitLab server.

Read that again. Not "a user could read files they should not". An **unauthenticated** user could read **arbitrary files from the server** — with the single precondition that the instance hosted at least one anonymously readable project. One public project. That is the whole condition. No account, no token, no user interaction, no phishing, no malware, no second step.

Within roughly twenty hours of the patch shipping, watchTowr Labs was seeing internet-wide scanning for this CVE. CISA added it to the Known Exploited Vulnerabilities catalogue on 11 September with a remediation deadline of 14 September for federal civilian agencies, and marked forensic triage as mandatory. And on 14 September, a self-managed GitLab administrator reported on the official GitLab forum that he had found in his own instance logs that an attacker had accessed his `gitlab-secrets.json` before he patched.

This is an operations story more than a technical one, and the technical part is the easy part.

How it works, briefly

The mechanism is a handshake between two layers of GitLab that disagree about what a request means. On multipart uploads, Workhorse — the Go layer in front of Rails — buffers the request body to disk and hands Rails a set of generated parameters: `file.path` for the uploaded body, `metadata.path` for a generated metadata file. Rails then opens whatever path the caller supplied. Two defects combine: the path is not confined to the repository directory, and authentication is not enforced before that file-read happens.

The public exploits reach it by URL-encoding a single character of the word "commits" — `%63ommits`. Workhorse's route sanitisation misses the encoded route, so it never overwrites `file.path`; Rails still decodes it and dispatches to the handler, which then accepts the path from the query string. The endpoint is `POST /api/v4/projects/:id/repository/commits`, and independent analyses reproduced the same behaviour through the repository files API on POST and PUT.

There is a second, quieter channel for the data. With `Content-Type: application/x-www-form-urlencoded`, the body-upload helper re-parses the file content, and a percent sign not followed by two hex digits gets reflected back in the HTTP 400 response body. Files without an invalid percent byte only yield a 401 existence oracle — which is exactly the problem for `gitlab-secrets.json`, since that file is hex and base64 all the way down.

The read primitive runs with the permissions of the GitLab service process, generally the `git` user. So the reachable set is every file that account can read: `gitlab.yml`, `secrets.yml`, `gitlab-secrets.json`, `database.yml`, SSH private keys, `.pem` certificates, `.env` files, runner and deploy tokens, CI/CD variables, application logs, plus reconnaissance paths like `/etc/passwd` or `/proc/self/environ`.

Why a secrets leak is worse than an information leak

This is the point where the incident changes category, and it is the part executives should sit with.

An unauthenticated file read on a GitLab instance is bad. But a GitLab instance is not a document store. It is the place where the secrets of your software supply chain live. If the attacker read `gitlab-secrets.json` or `secrets.yml`, they did not just learn something about your infrastructure — they obtained the Rails `secret_key_base` and the encryption keys.

With the secret key base, an attacker can **forge sessions and tokens** and **decrypt stored CI/CD variables and multi-factor authentication data**. The CI/CD variables are the crown jewels of most engineering organisations: the registry credentials, the cloud keys, the signing keys, the API tokens for payment providers, the deploy credentials. They are stored encrypted, and the key to that encryption was sitting in a file reachable by an anonymous HTTP request.

From there the pivot is mechanical. Leaked deploy tokens, personal access tokens, cloud keys and registry credentials give an attacker movement into cloud accounts, container registries and deployment pipelines. That is software supply-chain exposure downstream of the GitLab instance, and it is the part that turns an infrastructure incident into a question about your products.

Availability is not affected here — the CVSS vector scores the availability impact as None, and this is a confidentiality and integrity event rather than direct remote code execution. That is worth noting too, because it is exactly the kind of incident that gets downgraded on a risk register: nothing is down, nothing is visibly broken, and the loss is entirely in things that were quietly copied.

The twenty-hour window, and what it tells you about your patching process

The timeline is the operational lesson:

| Date | Event | |---|---| | 2026-09-04 | CVE reserved | | 2026-09-10 | Patch releases ship (19.3.2, 19.2.6, 19.1.8) | | 2026-09-11, ~06:00 UTC | Internet-wide scanning detected by watchTowr Labs | | 2026-09-11 | CISA KEV listing, deadline 14 September | | 2026-09-12 | CVE published, EPSS 11.1% (96th percentile) | | 2026-09-14 | Federal deadline; first confirmed victim reports on the GitLab forum | | 2026-09-23 | Backports to 19.0.9 and 18.11.12 ship; affected ranges updated |

Twenty hours. That is the gap between the patch being available and the whole internet looking for a way in. A quarter of a day is not a lot of time to discover that your self-managed instance is a GitLab instance, schedule a maintenance window, and run an upgrade that — and this matters operationally — **includes database migrations**. Single-node installations take downtime while those migrations run; multi-node deployments can use GitLab's zero-downtime upgrade procedure, and of the three fixed releases only 19.3.2 carries post-deployment migrations.

Ask yourself honestly: if you had to move a self-managed GitLab instance to a patched version within 24 hours, could you? Not "would you like to" — could you, with the change window you actually have, on the instance you actually have, on the branch you actually run? Most organisations cannot answer that question about their own GitLab before they are asked. The organisations that could answer it are the ones that did not have a bad week.

There is a second thing about the timeline that should inform how you plan. GitLab shipped the initial fixes for 19.3, 19.2 and 19.1 on 10 September. The backports to **19.0.9 and 18.11.12 did not ship until 23 September**, two weeks later, because they are maintained branches. If you were on 18.11, the patch you could apply on the day of disclosure did not exist yet. Branches with a long support window are exactly the branches where enterprises live, and exactly the branches where patching takes longest. Any process that assumes "the fix is out" is wrong for two weeks of every month.

The question the vulnerability actually asks you

Here is the uncomfortable finding. **The precondition for this attack is that your instance has at least one public project.** That is not a bug. That is a deliberate product decision — a public, open-source project needs anonymous read access to its repository, and GitLab supports that perfectly well.

Which means the technical control that would have prevented the exploitation was a product feature working as intended, and the administrative control that would have prevented it was knowing which of your instances had a public project on it.

For most organisations, the honest answer to "do we have any public projects on our self-managed GitLab?" is that nobody has asked in a long time. And that question is not a security quiz: an open-source contribution workflow, a public documentation repository, a customer-facing status page that happens to live in the same instance, a mirror of a public project — any of them is enough. All of them are reasonable. All of them are a valid path to reading your instance secrets.

GitLab describes itself as a DevOps platform with more than 30 million registered users, deployed by more than 50% of Fortune 100 companies. And CISA has flagged four GitLab vulnerabilities as exploited since November 2021, two of them in February 2026 alone. The 2023 predecessor CVE-2023-2825 was also a max-severity path traversal, and it was exploited within days of its fix. This is not a one-off.

The strategic question for a CTO or CISO is therefore not "did we patch". It is: **what is the blast radius of a single unauthenticated HTTP request against the system that holds our delivery pipeline, and how would we know if it had already happened?**

What to do in the first hour, and what to do in the first week

The first hour is about establishing whether you were a victim. GitLab itself published three detections in its open-source TLDR framework — `local_file_inclusion_gitlab_yml_read.md`, `local_file_inclusion_metadata_path.md` and `local_file_inclusion_path_enumeration.md` — documented to fire on `api_json.log` events. The logic is: a POST request whose parameters contain `file.path`, `file%2epath`, `metadata.path` or `metadata%2epath`, with `meta.client_id` starting with `ip/` (an unauthenticated caller), and a value that does not point into `/uploads/tmp/` or `/tmp/gitlab/` — the legitimate upload sandbox and Workhorse scratch directory.

``` # en la capa web, proxy o WAF: estos parametros nunca aparecen en una URL legitima grep -E "url\.path=.*/api/v4/projects/.*/repository/.*(file\.path|metadata\.path|file%2epath|metadata%2epath)" access.log ```

``` # en api_json.log del propio GitLab: cliente anonimo pidiendo rutas fuera del sandbox grep -E "\"meta\":\{[^}]*\"client_id\":\"ip/" api_json.log \ | grep -E "file\.path|file%2epath|metadata\.path|metadata%2epath" \ | grep -vE "/uploads/tmp/|/tmp/gitlab/" ```

The semantics of the response code matter, and they are counterintuitive. **HTTP 400 is the expected rejection** for this request shape. A 2xx from the same anonymous caller on the same route means the file may have been served — a confirmed read. Other status codes still confirm the parameter is reachable and being probed. And five or more distinct out-of-sandbox paths requested from a single source address within one hour indicates a filesystem sweep, not a stray scanner.

From there, the first week is about rotation, and this is the part most organisations skip because it feels like busywork after a patch. Patching stops new reads. It does not revoke material that was already copied. The material that could have been copied from a GitLab instance is: database credentials, SSH keys, deploy tokens, personal access tokens, CI/CD variables and cloud keys. Multiple vendors, including Rapid7 and Abstract Security, are explicit that these need rotating. CISA's KEV entry carries forensic-triage requirements under Binding Operational Directive 26-04, which is the federal formulation of the same instruction: do not close this with a version number.

So the sequence is: upgrade to a fixed release, then rotate the credentials, then hunt across the whole exposure window for signs of compromise — personal access tokens and deploy keys created or modified, unexpected pipeline runs, new runner registrations, SSH authentication from unknown sources, and logins to cloud or registry accounts whose credentials lived on that host.

And if you cannot upgrade immediately, the mitigations are unglamorous and effective: remove the instance from public internet exposure or restrict it to trusted IP ranges, audit which projects are public, and filter Commits API and Repository Files API traffic at a WAF. GitLab proposed no workaround of its own, which in the vendor's own words means the only sanctioned answer is the upgrade.

The decision this vulnerability is really testing

Here is the framing that I would take to a board or an executive committee, because it is the part that does not fit in a ticket.

GitLab is not a piece of software your developers like. It is the place where your organisation holds its delivery capability: the source, the history, the review trail, the pipelines, the variables, the artifacts, the audit log that your auditors and your regulators will ask for. In a self-managed deployment, that asset has exactly one copy, sits on infrastructure your team operates, and is reachable — or not — according to decisions made by people who had no reason to think about it.

The CVE-2026-85706 question was not "can you patch GitLab". It was "do you know what your GitLab instance would give away, and would you find out?". The answer for a great many organisations was: no, and no.

That is fixable this quarter, and it is a better use of the next two weeks than reading another advisory. Inventory every self-managed GitLab instance, including the ones in subsidiaries, the one in the lab, the one a contractor deployed and never handed over. Establish which are internet-facing, which have public projects, and what the upgrade path and the downtime cost would be for each. Then decide — deliberately, in writing — whether each one should be patched, air-gapped, or migrated to a managed offering, and who owns that decision.

Because the organisations that answer these questions in advance are the ones for whom the next twenty-hour window is a scheduled change rather than an incident.

**If you run self-managed GitLab: identify every instance you own today, check the API logs for anonymous path reads, and treat the credential rotation as part of the incident — not as follow-up. And answer one question this week: if we had to upgrade all of them tomorrow, how long would it take?**

---

*Sources: GitLab security release of 10 September 2026 and the associated documentation update of 23 September 2026; NIST National Vulnerability Database record for CVE-2026-85706 (published 12 September 2026, modified 24 September 2026); MITRE CVE List record (updated 23 September 2026); CISA Known Exploited Vulnerabilities catalogue, catalog version 2026.10.23; watchTowr, Attacker Eye honeypot detection and advisory of 10 September 2026, and the watchTowr Labs scanning detection of 11 September 2026; Greenbone (17 September 2026); Abstract Security (17 September 2026); EQSTLab proof-of-concept analysis and public lab; Truesec (29 September 2026); Rapid7 (14 September 2026); Orca Security (14 September 2026); SOC Prime (14 September 2026); SecurityAffairs (13 September 2026); InfoQ (3 October 2026); BleepingComputer; GitLab forum thread 135308, 14 September 2026; GitLab TLDR detection rules; HackerOne report 3909881 and GitLab work item 627748.*