Someone left the master key cabinet unlocked and the front door open — on your CI/CD platform
CVE-2026-85706 is an unauthenticated arbitrary file read in self-managed GitLab CE/EE versions 18.7 through 19.3.1. A parser differential between GitLab Workhorse (Go reverse proxy) and Puma/Grape (Ruby backend) lets an attacker percent-encode one character in the URL path (%66iles instead of files) to bypass Workhorse's route matching while Rails decodes it and processes the request through the repository commits API's upload handler — which opens the attacker-supplied file.path *before* authentication runs. The result: any file readable by the git process user is returned to an unauthenticated caller in a single HTTP request. Targeted files include gitlab-secrets.json (which encrypts all CI/CD variables and signs user sessions) and gitlab.rb (containing database, LDAP, SMTP, and object-storage credentials).
GitLab's CVSS 10.0 CRITICAL rating is fully justified and, if anything, undersells the post-exploitation blast radius. This is not a theoretical bug — CISA added it to the KEV catalog on September 11, 2026, one day after the patch shipped, and CrowdSec recorded 1,022 unique attacking IPs and 2,835 exploitation signals in the week of September 28–October 4 alone. The attack requires zero authentication, zero user interaction, low complexity, and only that the target instance hosts at least one public project — a condition met by effectively every production GitLab deployment. Because GitLab *is* the CI/CD and source code management platform, successful exploitation hands an attacker the keys to forge admin sessions, decrypt CI/CD secrets, and pivot into production infrastructure — a supply-chain-scale outcome.
6 steps from start to impact.
Identify internet-facing GitLab instance
/api/v4/version endpoint or the login page footer.- Internet-facing GitLab instance on an affected version
- Instances behind VPN or Zero Trust proxies are not reachable
Confirm a public project exists
/api/v4/projects?visibility=public or browses the GitLab explore page to confirm at least one public project exists. This is required because the exploit targets the repository commits API, which needs a valid project ID. Nearly all production GitLab instances host at least one public project.- At least one public project on the instance
- Rare instances with zero public projects are not exploitable via this path
Send the Workhorse-bypass request
/api/v4/projects/1/repository/%66iles/x?file=&file.path=/etc/passwd&file.size=1. The %66iles encoding causes Workhorse to miss its upload route match (it pattern-matches on the encoded path), while Rails URL-decodes it back to files and routes to the real upload handler. The handler opens the attacker-supplied file.path *before* the authentication middleware runs. Tools: Nuclei template (FLX-0x00), gitread (plur1bu5), Metasploit auxiliary/scanner/http/gitlab_file_read (jheysel-r7).- Network access to the GitLab web interface
- None — single unauthenticated HTTP request, no special tooling required
%66iles, %2e%2e, or .. in /api/v4/projects/.*/repository/ paths; Nuclei template available; CrowdSec and Suricata rules published.Exfiltrate high-value secrets
/etc/passwd (proof of access) to high-value targets: /var/opt/gitlab/gitlab-rails/etc/gitlab-secrets.json (the secret_key_base encrypting CI/CD variables and signing sessions), /etc/gitlab/gitlab.rb (database, LDAP, SMTP, object-storage passwords), and SSH keys. The gitread PoC automates exfiltration across 36 files in 7 priority tiers including Kubernetes tokens and AWS credentials.- Successful Workhorse bypass (Step 3)
- None — same request pattern, different file path
Forge admin session or decrypt CI/CD secrets
secret_key_base from gitlab-secrets.json, the attacker forges a valid Rails session cookie for any user including the admin, or decrypts CI/CD variables containing cloud credentials, deployment tokens, and API keys. This technique was demonstrated in prior GitLab exploitation chains (CVE-2020-10977). Tooling for Rails cookie forging is publicly available.- Possession of
gitlab-secrets.jsoncontents
- Requires knowledge of Rails cookie signing, but tooling exists and is documented
Supply-chain compromise
- Admin session or valid deployment credentials from Step 5
- Organizational — depends on what credentials were stored in CI/CD variables
gitlab-secrets.json and gitlab.rb as compromised. Regenerate secret_key_base, otp_key_base, and db_key_base. Rotate database passwords, LDAP bind credentials, SMTP credentials, object-storage keys, and all CI/CD variables. Revoke and reissue all runner registration tokens and personal/project/group access tokens. Deploy within 3 days of patching.%66iles, %46iles, double-encoded traversal sequences (%252e), or .. in any /api/v4/projects/.*/repository/ path. CrowdSec and Suricata rules are published. This buys hours while you patch, not days. Deploy immediately./api/v4/projects/.*/repository/ containing %66, %46, %2e, .., or absolute paths like /etc/ or /var/opt/gitlab/. Check for source IPs with no prior authenticated session. If exploitation is confirmed, invoke your incident response process. Deploy immediately.- GitLab's built-in rate limiting does not help — the exploit requires a single request, so rate limits never trigger.
- Disabling the GitLab API entirely breaks all Git operations, CI/CD pipelines, and integrations; this is not a viable compensating control for any production instance.
- Making all projects private would block the known exploit path but is operationally impractical for most organizations and may not block all Workhorse-bypass variants.
- Upgrading without rotating secrets leaves you exposed — as CrowdSec documented: 'upgrading closes the hole, not what already leaked through it.' Stolen
secret_key_baseremains valid indefinitely until regenerated.
The supporting signals.
| In-the-wild status | Actively exploited. CISA KEV-listed Sept 11, 2026. CrowdSec recorded 1,022 unique attacking IPs and 2,835 signals (Sept 28–Oct 4). Primary sources: Lithuania (37%), Latvia (28%), Ireland (14%). 89% classified as infrastructure-takeover intent. Targets: Germany (33%), Russia (19%), France (14%), Japan (14%). |
|---|---|
| Proof-of-concept availability | Weaponized and public. GitHub PoCs: 0xenesbayram/cve-2026-85706 (root-cause analysis + Docker lab), plur1bu5/gitread (oracle mode, fd enumeration, 36-file tiered loot), gagaltotal/CVE-2026-85706-gitlab-poc, EQSTLab/CVE-2026-85706, mhtsec/CVE-2026-85706, guneykabel/cve-2026-85706. Nuclei templates merged: PR #17231 by FLX-0x00, PR #17268 by comradezephyr. Metasploit module: PR #21910 by jheysel-r7. |
| EPSS score | 0.92956 — top ~7th percentile, indicating extremely high predicted exploitation probability within 30 days. |
| KEV status | Listed September 11, 2026. Federal remediation due date: September 14, 2026 (BOD 26-04). Added one day after the patch release. |
| CVSS vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N = 10.0. Network-attackable, low complexity, no privileges, no user interaction, changed scope, high confidentiality and integrity impact. The Integrity:High reflects the ability to forge sessions and inject pipeline code using stolen signing keys. |
| Affected versions | GitLab CE/EE 18.7.0–18.11.11, 19.0.0–19.0.8, 19.1.0–19.1.7, 19.2.0–19.2.5, 19.3.0–19.3.1. |
| Fixed versions | 19.3.2, 19.2.6, 19.1.8 (September 10, 2026); 19.0.9, 18.11.12 (September 23, 2026). |
| Scanning / exposure | Censys: 86,000+ exposed hosts (208,186 web properties). CyCognito: 20,000+ on standard web ports. ZoomEye: 1.26M fingerprint matches (not all vulnerable). Exploitation scanning began ~06:00 UTC September 11, hours after patch release. |
| Disclosure date | September 12, 2026 (patches shipped September 10, 2026). Probing began within hours of the advisory. |
| Reporting researcher | s3ntago via GitLab's HackerOne bug bounty program. |
Sources.
- Rapid7 ETR: CVE-2026-85706 Critical GitLab Path Traversal Exploited in the Wild
- CrowdSec: CVE-2026-85706 — Upgrading GitLab Won't Save Leaked Secrets
- CISA Adds One Known Exploited Vulnerability to Catalog (Sept 11, 2026)
- watchTowr: Rapid Reaction — GitLab Critical Path Traversal CVE-2026-85706
- 0xenesbayram/cve-2026-85706 — Root-cause analysis and PoC with Docker lab
- plur1bu5/gitread — Research PoC with oracle mode and tiered loot
- Nuclei template PR #17231 by FLX-0x00
- Metasploit module PR #21910 by jheysel-r7
Why this verdict
- Zero friction on the primary chain. The exploit requires no authentication, no user interaction, and reduces to a single HTTP request. The only prerequisite — a public project on the instance — is met by effectively every production GitLab deployment. No friction adjustment downward.
- Canonical high-value role component. GitLab *is* the CI/CD and source code management platform. It is not sometimes deployed in a high-value role; it exists exclusively in that role. ≥90% of installs occupy the supply-chain-critical role by definition. The verdict floor is CRITICAL under the deployment-role blast radius rule.
- Role multiplier: CI/CD + source code + secrets vault. The documented chain succeeds in the canonical deployment role. Blast radius is supply-chain-scale: stolen
secret_key_baseenables admin session forging → pipeline injection → backdoored artifacts shipped to every downstream consumer. Stolen CI/CD variables expose cloud credentials, deployment tokens, and API keys reaching production infrastructure. Outcome: supply-chain compromise. Floor holds at CRITICAL. - Active mass exploitation with weaponized tooling. KEV-listed within 24 hours of the patch. 1,022 unique IPs exploiting in the wild. Nuclei templates, 6+ GitHub PoCs, and a Metasploit module are public. EPSS at 0.93 confirms near-certain exploitation probability. No downward adjustment for theoretical-only risk.
- Post-patch residual risk amplifies urgency. Patching closes the file-read hole but does not invalidate secrets already exfiltrated. Any instance that was vulnerable between September 10 and the time of patching must assume
gitlab-secrets.jsonandgitlab.rbwere compromised and rotate all secrets — making this a two-step remediation, not a simple patch-and-forget.
Why not higher?
CVSS 10.0 is the maximum score on the scale. There is no higher severity rating to assign. The vendor assessment is at the ceiling.
Why not lower?
Every friction-reducing factor compounds *upward* here. The attack is unauthenticated, single-request, targets a canonically supply-chain-critical platform, has mass exploitation evidence, and weaponized public tooling. The only potential downward pressure — instances behind VPN — does not represent a majority of the installed base: Censys shows 86,000+ internet-facing instances, and GitLab's self-managed market inherently skews toward external accessibility for distributed development teams. Downgrading below CRITICAL would require evidence that <1% of GitLab installs are reachable, which the exposure data flatly contradicts.
Crowdsourced verification payload.
Run from any machine with network access to the target GitLab instance. Invoke with: bash check_cve_2026_85706.sh https://gitlab.example.com. No authentication or special privileges required. Checks the version reported by the GitLab API or login page against affected ranges.
#!/usr/bin/env bash
# check_cve_2026_85706.sh — Detect CVE-2026-85706 (GitLab unauthenticated file read)
# Usage: bash check_cve_2026_85706.sh <GITLAB_BASE_URL>
# Exit codes: 0 = VULNERABLE, 1 = PATCHED, 2 = UNKNOWN
set -euo pipefail
BASE_URL="${1:?Usage: $0 <GITLAB_BASE_URL>}"
BASE_URL="${BASE_URL%/}"
# Step 1: Get GitLab version
VERSION_JSON=$(curl -sk --max-time 10 "${BASE_URL}/api/v4/version" 2>/dev/null || true)
if [ -z "$VERSION_JSON" ]; then
# Try extracting from login page if API is restricted
VERSION_RAW=$(curl -sk --max-time 10 "${BASE_URL}/users/sign_in" 2>/dev/null | grep -oP 'gitlab_version[":]\s*["]*\K[0-9]+\.[0-9]+\.[0-9]+' || true)
else
VERSION_RAW=$(echo "$VERSION_JSON" | grep -oP '"version"\s*:\s*"\K[0-9]+\.[0-9]+\.[0-9]+' || true)
fi
if [ -z "$VERSION_RAW" ]; then
echo "UNKNOWN — could not determine GitLab version at ${BASE_URL}"
exit 2
fi
echo "Detected GitLab version: ${VERSION_RAW}"
# Step 2: Parse version components
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION_RAW"
# Step 3: Check against affected ranges
# Affected: 18.7.0–18.11.11, 19.0.0–19.0.8, 19.1.0–19.1.7, 19.2.0–19.2.5, 19.3.0–19.3.1
# Fixed: 18.11.12, 19.0.9, 19.1.8, 19.2.6, 19.3.2+
VULNERABLE=false
if [ "$MAJOR" -eq 18 ]; then
if [ "$MINOR" -ge 7 ] && [ "$MINOR" -le 10 ]; then
VULNERABLE=true
elif [ "$MINOR" -eq 11 ] && [ "$PATCH" -le 11 ]; then
VULNERABLE=true
fi
elif [ "$MAJOR" -eq 19 ]; then
if [ "$MINOR" -eq 0 ] && [ "$PATCH" -le 8 ]; then
VULNERABLE=true
elif [ "$MINOR" -eq 1 ] && [ "$PATCH" -le 7 ]; then
VULNERABLE=true
elif [ "$MINOR" -eq 2 ] && [ "$PATCH" -le 5 ]; then
VULNERABLE=true
elif [ "$MINOR" -eq 3 ] && [ "$PATCH" -le 1 ]; then
VULNERABLE=true
fi
fi
if [ "$VULNERABLE" = true ]; then
echo "VULNERABLE — GitLab ${VERSION_RAW} is in the affected range for CVE-2026-85706"
echo "Fix: upgrade to 18.11.12, 19.0.9, 19.1.8, 19.2.6, or 19.3.2+"
exit 0
else
echo "PATCHED — GitLab ${VERSION_RAW} is not in the affected range for CVE-2026-85706"
exit 1
fi