← Back to Feed CACHED · 2026-10-08 11:41:47 · CACHE_KEY CVE-2026-85706
CVE-2026-85706 · CWE-22 · Disclosed 2026-09-12

GitLab CE/EE Unauthenticated Arbitrary File Read via Repository Commits API Workhorse Bypass

ASSESSED — NOISGATE
Vendor
—
—
—
Reassessed
—
—
—
Verdict: —
Do you agree?
01 · The Real Story

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.

"Single unauthenticated request reads any file off your GitLab server. Patch and rotate secrets now."
02 · The Attack Path

6 steps from start to impact.

STEP 01

Identify internet-facing GitLab instance

The attacker scans for self-managed GitLab instances using Shodan, Censys, or FOFA fingerprinting on the GitLab login page. Censys shows 86,000+ exposed hosts. The attacker confirms the instance runs an affected version (18.7–19.3.1) via the /api/v4/version endpoint or the login page footer.
Conditions required:
  • Internet-facing GitLab instance on an affected version
Where this breaks in practice:
  • Instances behind VPN or Zero Trust proxies are not reachable
Detection/coverage: Shodan/Censys scans are passive from the defender's view; version endpoint may be restricted.
STEP 02

Confirm a public project exists

The attacker queries /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.
Conditions required:
  • At least one public project on the instance
Where this breaks in practice:
  • Rare instances with zero public projects are not exploitable via this path
Detection/coverage: Standard API call; indistinguishable from legitimate browsing.
STEP 03

Send the Workhorse-bypass request

The attacker sends a crafted POST to /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).
Conditions required:
  • Network access to the GitLab web interface
Where this breaks in practice:
  • None — single unauthenticated HTTP request, no special tooling required
Detection/coverage: WAF rules matching %66iles, %2e%2e, or .. in /api/v4/projects/.*/repository/ paths; Nuclei template available; CrowdSec and Suricata rules published.
STEP 04

Exfiltrate high-value secrets

The attacker pivots from /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.
Conditions required:
  • Successful Workhorse bypass (Step 3)
Where this breaks in practice:
  • None — same request pattern, different file path
Detection/coverage: Log analysis for repeated requests with path traversal patterns against different file paths.
STEP 05

Forge admin session or decrypt CI/CD secrets

With 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.
Conditions required:
  • Possession of gitlab-secrets.json contents
Where this breaks in practice:
  • Requires knowledge of Rails cookie signing, but tooling exists and is documented
Detection/coverage: Anomalous session origins; CI/CD variable usage from unexpected source IPs.
STEP 06

Supply-chain compromise

With admin access or decrypted deployment credentials, the attacker modifies CI/CD pipelines to inject malicious code into builds, pushes backdoored commits, or pivots into production infrastructure using stolen cloud credentials. The blast radius extends to every downstream consumer of artifacts built by the compromised GitLab instance.
Conditions required:
  • Admin session or valid deployment credentials from Step 5
Where this breaks in practice:
  • Organizational — depends on what credentials were stored in CI/CD variables
Detection/coverage: Code review, pipeline audit logs, cloud IAM anomaly detection, artifact integrity verification.
03 · Compensating Control

1
CRITICAL 10.0→HIGH 6.5
SEVERITY REDUCED
Patch to a fixed version immediately — KEV-listed and actively exploited — this overrides the standard noisgate CRITICAL 3-day mitigation SLA. Upgrade to 19.3.2, 19.2.6, 19.1.8, 19.0.9, or 18.11.12 depending on your release train. Deploy within hours, not days.
2
CRITICAL 10.0→MEDIUM 4.0
SEVERITY REDUCED
Rotate all secrets stored on the GitLab host — After patching, treat 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.
3
CRITICAL 10.0→HIGH 7.5
SEVERITY REDUCED
Block the exploit at the WAF/reverse proxy — Deploy WAF rules to reject requests containing %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.
4
CRITICAL 10.0→LOW 3.0
SEVERITY REDUCED
Restrict network access to the GitLab API — Place the GitLab instance behind a VPN, Zero Trust proxy (e.g., Cloudflare Access, Zscaler ZPA), or IP allowlist. This eliminates the unauthenticated remote attack vector for external attackers. Deploy within 3 days per the noisgate mitigation SLA.
5
CRITICAL 10.0→CRITICAL 10.0
Audit logs for exploitation indicators — Search GitLab production logs and reverse-proxy access logs for requests to /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.
What doesn't work
  • 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_base remains valid indefinitely until regenerated.
04 · Intelligence Metadata

The supporting signals.

In-the-wild statusActively 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 availabilityWeaponized 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 score0.92956 — top ~7th percentile, indicating extremely high predicted exploitation probability within 30 days.
KEV statusListed September 11, 2026. Federal remediation due date: September 14, 2026 (BOD 26-04). Added one day after the patch release.
CVSS vectorCVSS: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 versionsGitLab 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 versions19.3.2, 19.2.6, 19.1.8 (September 10, 2026); 19.0.9, 18.11.12 (September 23, 2026).
Scanning / exposureCensys: 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 dateSeptember 12, 2026 (patches shipped September 10, 2026). Probing began within hours of the advisory.
Reporting researchers3ntago via GitLab's HackerOne bug bounty program.

Sources.

  1. Rapid7 ETR: CVE-2026-85706 Critical GitLab Path Traversal Exploited in the Wild
  2. CrowdSec: CVE-2026-85706 — Upgrading GitLab Won't Save Leaked Secrets
  3. CISA Adds One Known Exploited Vulnerability to Catalog (Sept 11, 2026)
  4. watchTowr: Rapid Reaction — GitLab Critical Path Traversal CVE-2026-85706
  5. 0xenesbayram/cve-2026-85706 — Root-cause analysis and PoC with Docker lab
  6. plur1bu5/gitread — Research PoC with oracle mode and tiered loot
  7. Nuclei template PR #17231 by FLX-0x00
  8. Metasploit module PR #21910 by jheysel-r7
05 · The Call

Final Verdict
= UNCHANGED to CRITICAL (10.0/10)

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_base enables 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.json and gitlab.rb were 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.

06 · Verification

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.

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/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