Blog
GitLabCVE-2026-85706CI/CD SecuritySupply Chain

GitLab CVE-2026-85706: Unauthenticated File Read Empties Your CI/CD Secrets

A CVSS 10 path-traversal in GitLab's commits API lets anyone read secrets.yml, deploy tokens and CI/CD variables — no login, no auth log. Patch, then rotate.

Zero Hunt Research··8 min read

The federal patch deadline for CVE-2026-85706 is today. That is the wrong deadline to worry about. GitLab shipped the fix on 10 September; CISA added the bug to its Known Exploited Vulnerabilities catalog on 11 September with a three-day remediation clock for federal agencies. But watchTowr recorded in-the-wild probes against the flaw starting at 06:00 UTC on 11 September — hours after the patch went public, on instances that had not yet applied it. If your self-managed GitLab was internet-facing and unpatched in that window, patching now closes the door on an empty vault. The secrets it protected have already been read.

This is the property that makes CVE-2026-85706 worse than its CVSS 10.0 suggests: the vulnerability is an unauthenticated arbitrary file read. There is no login, so there is no authentication event to log. The only record that the attacker walked off with your secret_key_base is the HTTP request itself — and most teams do not retain or inspect request bodies to the API.

What CVE-2026-85706 actually does

The flaw lives in the repository commits API. A POST to /api/v4/projects/{id}/repository/commits/ carrying a file.path parameter with directory-traversal sequences causes GitLab to return the contents of arbitrary files on the server — anything readable by the gitlab service user. GitLab's own root-cause description is "improper path confinement and missing authentication enforcement in the repository commits API." Two words in that sentence do the damage: missing authentication. The endpoint that normally requires a project token here requires nothing.

There is exactly one precondition, and it is trivially met: at least one public project must exist on the instance. That is the default posture for most GitLab deployments that host any open-source mirror, template, or demo repo. watchTowr's summary is blunt — "exploitation requires just one requirement, at least one public project must exist."

The affected range is broad. Every self-managed Community and Enterprise Edition instance is vulnerable:

  • from 18.7 before 19.1.8
  • from 19.2 before 19.2.6
  • from 19.3 before 19.3.2

GitLab.com and GitLab Dedicated are not affected. The same 10 September release also fixed CVE-2026-87719 (CVSS 9.9), an insecure-deserialization bug reachable by authenticated users with Duo Chat access — a second reason to take the whole release, not just cherry-pick the traversal fix.

Why an arbitrary file read is a full compromise

"File read" sounds bounded. On a GitLab server it is not, because the files readable by the gitlab user are the keys to the entire instance and everything it can reach. The single most valuable target is secret_key_base — the Rails master secret, stored at /etc/gitlab/gitlab-secrets.json on Omnibus installs (or config/secrets.yml on source installs). It signs every session cookie and signed verifier GitLab issues, so recovering it turns a file read into account takeover and, from there, code execution:

  1. Forge a session cookie for any user, including an admin — full console access with no credentials.
  2. Forge a runner registration token, register an attacker-controlled CI runner, and run arbitrary commands on your build infrastructure.
  3. Forge password-reset tokens to take over accounts, 2FA included.

That is why the CVSS vector carries changed scope: the impact crosses out of GitLab into the runners, cloud providers, and registries GitLab authenticates to. The rest of the loot is the CI/CD supply chain in a bucket:

What the read exposes Where it lives What it grants
secret_key_base gitlab-secrets.json / secrets.yml Session + token forgery → admin, then RCE via a rogue runner
.gitlab_shell_secret .../gitlab-shell/.gitlab_shell_secret Forged internal API calls to the Git backend
SSH host keys /etc/gitlab/ssh/ Server impersonation, man-in-the-middle on Git-over-SSH
CI/CD variables DB / rendered config Cloud keys, registry creds, signing keys used by pipelines
Deploy tokens & runner tokens DB / GitLab config Persistent repo access, push to registries, rogue runners
Database credentials gitlab.rb / database.yml Direct DB read — every user hash, token, project

Read secret_key_base and the CI/CD variables and the attacker does not need to touch GitLab again. They own your build pipeline — the place where code becomes signed, trusted artifacts that flow to production and to customers. This is the exact shape of a software-supply-chain incident, initiated by a single unauthenticated GET-shaped POST.

"We patched inside the deadline." "When was the instance reachable and unpatched?" "The 10th and 11th, before the change window." "Then assume secret_key_base, every deploy token, and every CI/CD variable was read on the 11th. Patching stopped the read. It did not un-read them."

The exploitation window is already open

Line up the timeline and the problem is obvious. Patch released 10 September. Public disclosure and KEV listing 11 September. Active probing observed from 06:00 UTC on 11 September. The gap between a fix landing and a working exploit being sprayed at exposed instances is now measured in hours, not weeks — a pattern that has held for every high-value pre-auth flaw this year, from edge VPNs to CI/CD.

For defenders the operational consequence is specific: the absence of an authentication log is not evidence of safety. A successful exploit of CVE-2026-85706 generates no failed-login, no session, no audit event of the kind SIEM correlation rules are built around. What it generates is an HTTP request with a recognizable shape. If you were not capturing and inspecting request URIs and bodies to /api/v4/projects/*/repository/commits/, you have no host-side record of whether you were hit — and the attacker, if they were paying attention, read your logging config too.

Remediation

Treat this as a credential-exposure incident, not a patch ticket. The fix stops future reads; it does nothing about what was already taken.

1. Am I affected? Check the version and the exposure:

# Version (self-managed)
sudo gitlab-rake gitlab:env:info | grep -i version
# or, containerized:
cat /opt/gitlab/version-manifest.txt | head -1

# Any public project = exploitable precondition
sudo gitlab-rails runner 'puts Project.where(visibility_level: 20).count'

Any version from 18.7 up to (not including) 19.1.8 / 19.2.6 / 19.3.2 is vulnerable. GitLab.com and Dedicated are not.

2. Patch — exact fixed versions. Upgrade to 19.1.8, 19.2.6, or 19.3.2 on the matching release branch (verbatim from the GitLab security release). Take the whole release so you also close CVE-2026-87719.

3. Can't patch this hour? Compensating controls. At the reverse proxy or WAF, block or hard-fail requests to /api/v4/projects/*/repository/commits whose body contains a file.path parameter with traversal sequences (../, %2e%2e, encoded variants). This is a virtual patch, not a fix — it buys the change window, nothing more. Restrict API exposure to trusted networks where feasible.

4. Hunt for compromise. Search access logs and any captured request bodies for POSTs to the commits API carrying file.path. Concretely:

grep -E 'POST .*/api/v4/projects/[0-9]+/repository/commits' \
  /var/log/gitlab/nginx/gitlab_access.log* | grep -i 'file%2epath\|file.path'

Map the activity to MITRE ATT&CK: initial access via T1190 (Exploit Public-Facing Application), credential theft via T1552.001 (Unsecured Credentials: Credentials in Files), and — for any follow-on forged sessions — T1078 (Valid Accounts). Any hit — even a single request that returned a 200 with a large body — means assume disclosure. IOCs to prioritize: requests from cloud/VPS ASNs you do not normally see, bursts of commits-API POSTs across many project IDs, and any subsequent authenticated activity using a token you did not issue.

5. Eradicate + verify. Because the read is silent, the only safe assumption for an exposed instance is total secret disclosure. After patching:

  • Rotate secret_key_base following GitLab's documented secret-rotation procedure. This invalidates every forged session and signed token — all users are logged out, which is the point.
  • Rotate .gitlab_shell_secret, SSH host keys, and the database password.
  • Revoke and reissue every deploy token, runner registration token, and CI/CD variable — especially cloud provider keys, registry credentials, and signing keys. Rotate them at the source (AWS, GCP, the registry), not just in GitLab.
  • Re-register runners with fresh tokens; deregister any runner you do not recognize.
  • Confirm clean after patching: re-run the log hunt, verify no unknown tokens or runners persist, and diff CI/CD configuration for injected steps.

Rotation is the step most write-ups skip, and it is the one that actually ends the incident. A patched GitLab still using a secret_key_base that was read on 11 September is a patched, compromised GitLab.

Where Zero Hunt fits

The hard question this bug forces is not "is a patch available" — it was available before the exploitation started. It is "is my specific instance actually reachable and leaking, and does the leak chain all the way to RCE on my configuration?" That is a question you answer by attacking your own surface, on the clock the attacker is running.

Zero Hunt's change-triggered campaigns exist for exactly this moment: a new CVE against software on your perimeter kicks off a full campaign within the hour, without waiting for a scheduled quarterly test. The 10-agent generative swarm does not fetch a public proof-of-concept — in the zero-day-to-patch window there often isn't one. A local LLM writes a per-target exploit against your GitLab, confirms whether the commits API returns secrets.yml, and whether the recovered secret_key_base forges a session that reaches code execution — the reachability that a CVSS 10 only implies. Every skill the swarm uses is backtested in the AI Gym against corpora like Vulhub and the 314-CVE black-box set before it runs in production, and every action is ECDSA-signed into a chain-of-custody you can hand an auditor who asks what you did on 14 September.

The read itself, and the credential use that follows it, are also a network story — which is where the AI Traffic Analysis engine earns its place on this incident. The exploit leaves no authentication log, but the request crosses the wire, and so does everything the attacker does with the stolen deploy tokens and CI/CD keys: the pushes to your registry, the rogue runner checking in, the egress to a never-seen ASN. A deep-learning model with four inference heads running on the appliance GPU reads that behavior as it happens — not in the next morning's SIEM digest that, for an unauthenticated file read, would have nothing to show. On-premise, no cloud callbacks: your GitLab secrets are the last thing you want to route through someone else's model to find out they were stolen.