Back to Intelligence

CVE-2026-85706: GitLab Path Traversal Exploited in the Wild — Detection and Remediation Guide

SA
Security Arsenal Team
September 14, 2026
11 min read

On September 10, 2026, GitLab shipped a critical patch release for Community Edition (CE) and Enterprise Edition (EE) addressing CVE-2026-85706 — a path traversal vulnerability (CWE-22) in the repository commits API carrying a CVSSv3.1 score of 10.0. Within 24 hours, CISA added it to the Known Exploited Vulnerabilities (KEV) catalog with a remediation deadline of September 14, 2026 for Federal Civilian Executive Branch agencies. That timeline — patch to KEV in one day — tells you everything about how fast exploitation scaled.

If you run a self-managed GitLab instance exposed to the internet, you should assume it has been probed, and potentially read, by now. This flaw requires no authentication. An attacker can pull arbitrary files from the underlying server: configuration secrets, SSH keys, CI/CD tokens, database credentials, and anything else the GitLab service account can read. For most organizations, GitLab is the crown-jewel repository — source code, pipeline secrets, signing keys, infrastructure-as-code. A successful read of /etc/gitlab/gitlab-secrets.json or CI variables can cascade into full supply-chain compromise.

This post gives you the technical breakdown, detection content you can deploy today, and the remediation path.


Technical Analysis

What is affected

  • Product: GitLab Community Edition (CE) and Enterprise Edition (EE), self-managed instances
  • Component: Repository commits API (/api/v4/projects/:id/repository/commits)
  • Vulnerability class: CWE-22 — Improper Limitation of a Pathname to a Restricted Directory (Path Traversal)
  • CVSSv3.1: 10.0 (Critical) — network exploitable, no privileges, no user interaction
  • Exploitation status: Confirmed active exploitation in the wild. Added to CISA KEV on September 11, 2026, with a federal remediation due date of September 14, 2026.

GitLab.com-hosted instances are managed by GitLab and were remediated by the vendor; the exposure here is self-managed deployments.

How the vulnerability works — defender's view

Two failures compound into the critical score:

  1. Improper path confinement. The repository commits API accepts parameters that influence the on-disk path the GitLab Rails backend resolves when serving commit data. The application fails to canonicalize and confine that input to the repository storage root (/var/opt/gitlab/git-data/repositories in Omnibus installs). Classic ../ sequences — including URL-encoded variants (%2e%2e%2f, ..%2f, double-encoded %252e) — let a request traverse out of the repository tree and reference arbitrary absolute or relative filesystem paths.

  2. Missing authentication enforcement. Under specific conditions, the vulnerable endpoint can be reached without a valid session or API token. That combination — unauthenticated reachability plus arbitrary file read — is what earns the 10.0.

Realistic attack chain

  1. Attacker scans internet-facing GitLab instances (Shodan/Censys fingerprinting on the GitLab login page is trivial).
  2. Crafted GET request to the repository commits API with traversal sequences targeting a known-readable file, e.g. /etc/passwd as a proof of access.
  3. Escalation to high-value reads: gitlab-secrets.json (secret key base, DB encryption keys), /etc/gitlab/gitlab.rb, CI/CD job tokens, deploy keys, runner registration tokens, and stored OAuth credentials.
  4. Post-exploitation: forge encrypted session cookies (with leaked secret_key_base), authenticate as any user, push malicious commits, poison CI pipelines, or register a rogue runner for code execution.

The jump from file read to full instance compromise is short and well-trodden. Treat any confirmed traversal hit as an incident, not a vulnerability.

Exploitation indicators to expect

  • Requests to /api/v4/projects/.../repository/commits containing .., %2e, %252e, or absolute paths (/etc/, /var/opt/gitlab/) in query parameters
  • Requests from source IPs with no prior authenticated session activity against the instance
  • HTTP 200 responses on those crafted requests where the response body contains file content (e.g. root:x:0:0:)
  • Follow-on authentication anomalies: valid sessions from IPs that never logged in (forged cookies), new personal access tokens, new runners registered

Detection & Response

The detection surface for this attack is your web tier: GitLab's bundled nginx (Omnibus), any upstream reverse proxy/WAF, and load balancer logs. All of the following assume you are shipping those logs to your SIEM — if you are not, that is finding number one.

Sigma Rules

These target the observable exploitation behavior: traversal patterns in requests against the repository commits API. Rule one catches the core attack; rule two catches the post-exploitation follow-on (forged-cookie session usage from previously unseen IPs is best handled as a correlation, so rule two focuses on suspicious unauthenticated API enumeration).

YAML
---
title: GitLab CVE-2026-85706 Path Traversal Attempt on Repository Commits API
id: 9c1e4b7a-2f83-4d5a-b6e9-7f2a1c3d5e01
status: experimental
description: Detects path traversal sequences (raw, URL-encoded, or double-encoded) in requests to the GitLab repository commits API, consistent with exploitation of CVE-2026-85706.
references:
  - https://www.rapid7.com/blog/post/etr-cve-2026-85706-critical-gitlab-path-traversal-exploited-in-the-wild
  - https://about.gitlab.com/releases/
author: Security Arsenal
date: 2026/09/12
tags:
  - attack.initial_access
  - attack.t1190
  - attack.t1083
logsource:
  category: webserver
  product: linux
detection:
  selection_uri:
    c-uri|contains: '/repository/commits'
  selection_traversal:
    c-uri|contains:
      - '../'
      - '..\\'
      - '%2e%2e'
      - '..%2f'
      - '..%5c'
      - '%252e%252e'
      - '..;/'
  selection_absolute:
    c-uri|contains:
      - '/etc/passwd'
      - '/etc/gitlab'
      - '/var/opt/gitlab'
      - 'gitlab-secrets'
      - 'gitlab.rb'
  condition: selection_uri and (selection_traversal or selection_absolute)
falsepositives:
  - Extremely rare; legitimate API clients do not send traversal sequences or absolute host paths in commit API parameters
level: critical
---
title: Unauthenticated GitLab API Enumeration of Repository Endpoints
id: 4d8f2a61-9b47-4c3e-a5d2-8e1b6f0c9a37
status: experimental
description: Detects repeated requests to GitLab project/repository API endpoints from sources with no associated authenticated session, a pattern consistent with pre-exploitation reconnaissance for CVE-2026-85706.
references:
  - https://www.rapid7.com/blog/post/etr-cve-2026-85706-critical-gitlab-path-traversal-exploited-in-the-wild
author: Security Arsenal
date: 2026/09/12
tags:
  - attack.reconnaissance
  - attack.t1595
logsource:
  category: webserver
  product: linux
detection:
  selection:
    c-uri|contains:
      - '/api/v4/projects'
      - '/api/v4/repository'
      - '/repository/files'
      - '/repository/commits'
    sc-status:
      - 200
      - 401
      - 403
  filter_authed:
    c-uri|contains:
      - 'private_token='
      - 'PRIVATE-TOKEN'
      - 'oauth_token='
  condition: selection and not filter_authed
falsepositives:
  - Public project browsing on intentionally public instances; baseline and suppress for known public project paths
level: medium

KQL — Microsoft Sentinel

Assumes GitLab nginx access logs ingested via Syslog/CEF (CommonSecurityLog), or proxy/WAF logs. The query looks for traversal indicators against the commits API and surfaces which source IPs to investigate and whether the server returned file content (HTTP 200 with non-trivial response size).

KQL — Microsoft Sentinel / Defender
let TraversalIndicators = dynamic(["../", "%2e%2e", "..%2f", "..%5c", "%252e%252e", "..;/", "/etc/passwd", "/etc/gitlab", "/var/opt/gitlab", "gitlab-secrets"]);
CommonSecurityLog
| where TimeGenerated > ago(14d)
| where RequestURL has "/repository/commits" or RequestURL has "/api/v4/projects"
| where RequestURL has_any (TraversalIndicators)
| extend IsSuccess = toint(HttpStatusCode) == 200
| summarize Requests = count(),
            FirstSeen = min(TimeGenerated),
            LastSeen = max(TimeGenerated),
            SuccessfulReads = countif(IsSuccess),
            SampleURIs = make_set(RequestURL, 5)
      by SourceIP, DestinationHostName, HttpStatusCode
| order by SuccessfulReads desc, Requests desc;

// Follow-on: hunt for session/cookie authentication from IPs that previously sent traversal probes
let SuspectIPs = CommonSecurityLog
| where TimeGenerated > ago(14d)
| where RequestURL has_any (TraversalIndicators)
| summarize by SourceIP;
CommonSecurityLog
| where TimeGenerated > ago(14d)
| where SourceIP in (SuspectIPs)
| where RequestURL has "api/v4" and HttpStatusCode == 200
| summarize AuthFollowOnRequests = count(), DistinctEndpoints = dcount(RequestURL), Endpoints = make_set(RequestURL, 10)
      by SourceIP, bin(TimeGenerated, 1h)
| order by TimeGenerated desc;

If your GitLab access logs arrive as raw Syslog instead of CEF, swap the table to Syslog and parse SyslogMessage with extract() — the traversal substrings remain identical.

Velociraptor VQL

For DFIR teams validating a specific GitLab server: this artifact parses the Omnibus nginx access logs directly on the host and pulls every request containing traversal indicators against repository API paths, with response codes — response 200s on these requests are your confirmed reads.

VQL — Velociraptor
-- GitLab CVE-2026-85706: Hunt traversal exploitation in Omnibus nginx access logs
LET logs = SELECT FullPath
FROM glob(globs=['/var/log/gitlab/nginx/gitlab_access.log*', '/var/log/gitlab/nginx/*access*.log*'])

LET hits = SELECT FullPath, Line
FROM parse_lines(filename=logs.FullPath, accessor='file')
WHERE Line =~ '/repository/(commits|files|tree)'
  AND Line =~ '(\\.\\./|%2e%2e|\\.\\.%2f|%252e%252e|/etc/passwd|/etc/gitlab|/var/opt/gitlab|gitlab-secrets)'

SELECT FullPath AS LogFile,
       Line AS RawLogEntry,
       count() AS HitCount
FROM hits
GROUP BY RawLogEntry, LogFile
ORDER BY HitCount DESC

Correlate HitCount entries returning HTTP 200 against your patch timeline. Any 200 response before your patch timestamp means the file content left your network — scope the IR accordingly (secrets rotation, session invalidation, runner audit).

Verification & Hardening Script

Run this on an Omnibus-based GitLab server to check your installed version against the vulnerable condition, review logs for exploitation attempts, and confirm service account file exposure. Adapt paths for source installations.

Bash / Shell
#!/bin/bash
# CVE-2026-85706 verification script - Omnibus GitLab
# Run as root on the GitLab server

echo "=== [1] Installed GitLab version ==="
if command -v gitlab-rake >/dev/null 2>&1; then
  gitlab-rake gitlab:env:info 2>/dev/null | head -20
else
  cat /opt/gitlab/version-manifest.txt 2>/dev/null | head -5
fi

echo ""
echo "=== [2] External reachability check (is this instance internet-facing?) ==="
curl -sk --max-time 5 https://ifconfig.me 2>/dev/null && echo "" || echo "No direct egress"

echo ""
echo "=== [3] Scanning nginx access logs for traversal exploitation attempts ==="
for log in /var/log/gitlab/nginx/gitlab_access.log* /var/log/gitlab/nginx/*access*.log*; do
  [ -f "$log" ] || continue
  echo "--- $log ---"
  zgrep -Eh 'repository/(commits|files|tree)' "$log" 2>/dev/null \
    | grep -Ei '(\.\./|%2e%2e|\.\.%2f|%252e%252e|/etc/passwd|/etc/gitlab|/var/opt/gitlab|gitlab-secrets)' \
    | tail -50
done

echo ""
echo "=== [4] Scanning GitLab Rails production log for suspicious API access ==="
zgrep -Ei 'repository.*(\.\.|%2e)' /var/log/gitlab/gitlab-rails/production_json.log* 2>/dev/null | tail -20

echo ""
echo "=== [5] Sensitive file exposure check (readable by git user?) ==="
sudo -u git test -r /etc/gitlab/gitlab-secrets.json && echo "[!] gitlab-secrets.json readable by git user - ROTATE SECRETS if exploited" || echo "[OK] secrets not readable"
sudo -u git test -r /etc/gitlab/gitlab.rb && echo "[!] gitlab.rb readable by git user"

echo ""
echo "=== [6] Runner and token audit pointers ==="
echo "Check Admin > Runners for unknown runners: gitlab-rails runner 'puts Ci::Runner.order(created_at: :desc).limit(10).pluck(:id,:description,:created_at)'"
echo "Review recent PATs: gitlab-rails runner 'puts PersonalAccessToken.where('created_at > ?', 7.days.ago).pluck(:user_id,:name,:created_at)'"

echo ""
echo "=== REMEDIATION ==="
echo "Patch immediately per https://about.gitlab.com/releases/ (Sept 10, 2026 critical release)"
echo "Ubuntu/Debian:  apt-get update && apt-get install gitlab-ce   (or gitlab-ee)"
echo "RHEL/CentOS:    yum update gitlab-ce   (or gitlab-ee)"
echo "Then: gitlab-ctl reconfigure && gitlab-ctl restart"

Remediation

Priority one — patch. Apply the GitLab critical patch release published September 10, 2026. CISA's KEV deadline for federal agencies was September 14, 2026; private-sector organizations should treat that as their own deadline, not a suggestion.

  • Omnibus (Debian/Ubuntu): apt-get update && apt-get install gitlab-ee (or gitlab-ce)
  • Omnibus (RHEL/CentOS): yum update gitlab-ee
  • Docker: pull the patched image tag from the release announcement and redeploy
  • Verify the running version post-patch at https://<your-gitlab>/help

Official sources:

Priority two — assume breach and rotate. Given the unauthenticated file-read primitive, any internet-exposed unpatched instance should be treated as compromised until log review proves otherwise:

  1. Review access logs for traversal attempts (script and VQL above). Any HTTP 200 on a crafted request = data exfiltrated.
  2. If exploitation is confirmed or cannot be ruled out: rotate secret_key_base and all values in gitlab-secrets.json, revoke all personal access tokens and OAuth tokens, rotate CI/CD variables and deploy keys, and invalidate all sessions (a full secrets rotation forces re-authentication).
  3. Audit registered runners and project webhooks for unauthorized additions in the exposure window.
  4. Audit recent commits and pipeline definitions (gitlab-ci.yml changes) for tampering.

Priority three — reduce the blast radius going forward:

  • Do not expose GitLab directly to the internet unless there is a hard business requirement. VPN or zero-trust access gateway in front of the UI and API.
  • Place a WAF in front of GitLab with a rule blocking requests containing traversal sequences to /api/* — useful as a compensating control, but never a substitute for the patch.
  • Ensure GitLab access logs (nginx + production_json.log) ship to your SIEM in near-real-time with 90+ day retention. This incident is un-investigable without them.
  • Enforce least privilege on the git service account's filesystem read access where your deployment model allows.

Closing

CVE-2026-85706 follows a pattern we have watched repeat across self-hosted DevOps infrastructure: the platform holding your source code and pipeline secrets is internet-reachable, a pre-authentication flaw drops, and exploitation starts within hours of the advisory. The window between disclosure and mass exploitation is now measured in hours, not weeks. If GitLab is in your environment, it belongs in your highest patch SLA tier, your external attack surface monitoring, and your assumed-breach runbooks — this week.

Related Resources

Security Arsenal Penetration Testing Services AlertMonitor Platform Book a SOC Assessment vulnerability-management Intel Hub

Is your security operations ready?

Get a free SOC assessment or see how AlertMonitor cuts through alert noise with automated triage.

CVE-2026-85706: GitLab Path Traversal Exploited in the Wild — Detection and Remediation Guide | Security Arsenal | Security Arsenal