← Back to Feed CACHED · 2026-10-15 17:07:27 · CACHE_KEY CVE-2026-97229
CVE-2026-97229 · CWE-190 · Disclosed 2026-10-13

GitLab CE/EE Unauthenticated RCE via Integer Overflow in CI/CD Regular Expression Compiler

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

Someone left a loaded gun in the regex compiler and 25,000 GitLab servers are standing in front of it

CVE-2026-97229 is an integer overflow (CWE-190) in the Onigmo regular-expression compiler used by GitLab's CI/CD pipeline engine. A crafted regex in a .gitlab-ci.yml file passes initial US-ASCII validation, but when the CI worker re-evaluates it against a UTF-8 repository path the compiler's program-size calculation wraps, producing invalid VM instructions. Control-flow integrity is lost and the attacker lands arbitrary OS command execution as the git user — the identity that owns every repository, CI variable, deploy key, and runner token on the box. Affected versions span GitLab CE/EE 19.2.0–19.2.7, 19.3.0–19.3.4, and 19.4.0–19.4.1. Patched releases are 19.2.8, 19.3.5, and 19.4.2, shipped October 12 2026.

GitLab scored this a perfect CVSS 10.0 with vector AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H. The vendor severity is almost exactly right. The one nuance is that the 'no privileges required' claim assumes open self-registration, which is GitLab's *default* for self-managed instances but not universal. On locked-down instances where registration is disabled and project creation is restricted, the chain requires an existing low-privilege account — making the realistic vector closer to PR:L (CVSS ~9.9). That distinction costs the vendor score 0.2 points at most. The blast radius — source code, secrets, runners, connected cloud accounts, downstream supply chain — justifies keeping this firmly in CRITICAL territory.

"Unauthenticated RCE on self-managed GitLab via CI/CD regex — PoC is live, patch now."
02 · The Attack Path

5 steps from start to impact.

STEP 01

Self-register on target GitLab instance

The attacker creates a new account on a self-managed GitLab server. GitLab ships with open registration enabled by default (signup_enabled: true). No CAPTCHA or admin approval is required in the default configuration. This gives the attacker an authenticated session in under 30 seconds.
Conditions required:
  • Internet-facing self-managed GitLab instance
  • Self-registration enabled (default)
Where this breaks in practice:
  • Enterprises that disabled self-registration or require admin approval block this step entirely
  • Instances behind VPN/SSO gating are unreachable
Detection/coverage: Monitor users.created audit events for unusual sign-up patterns; GreyNoise tags for GitLab reconnaissance scans.
STEP 02

Create a public project with UTF-8 path

The attacker creates a new project whose namespace or path contains specific multi-byte UTF-8 characters. This is required so that the CI worker evaluates the regex against a UTF-8 string, triggering the encoding-switch code path in Onigmo. GitLab's default configuration allows any authenticated user to create public projects.
Conditions required:
  • Authenticated session from Step 1
  • Public project creation not restricted
Where this breaks in practice:
  • Organizations that restrict project visibility to Internal/Private block the public-project requirement
  • Namespace-level restrictions on project creation can limit this
Detection/coverage: Audit log entries for project creation with unusual Unicode characters in path; monitor projects.created events.
STEP 03

Commit malicious .gitlab-ci.yml with crafted regex

The attacker pushes a .gitlab-ci.yml containing a specially constructed regular expression designed to pass GitLab's ASCII-mode validation but overflow the Onigmo compiler's integer size calculation when recompiled for UTF-8. The regex pattern is compact — a few hundred bytes — and uses legal regex syntax that evades naive static analysis.
Conditions required:
  • Write access to the project (satisfied by project ownership from Step 2)
  • CI/CD enabled for the project (default)
Where this breaks in practice:
  • Push rules or pre-receive hooks that reject .gitlab-ci.yml changes from non-maintainers would block this, but the attacker owns the project
  • CI lint validation does not catch the malicious pattern because it compiles cleanly in ASCII mode
Detection/coverage: File-level audit on .gitlab-ci.yml changes; regex-complexity scanners (not standard in GitLab).
STEP 04

Trigger CI/CD pipeline evaluation

When the branch is pushed, GitLab's CI subsystem automatically parses the .gitlab-ci.yml. The regex is compiled by the Onigmo engine in the Ruby worker process. Upon encountering the UTF-8 repository path, Onigmo re-enters its compiler with the new encoding, and the integer overflow in the program-size calculation produces a truncated bytecode buffer. This is fully automated — no additional attacker interaction is needed after the push.
Conditions required:
  • CI/CD runners or Sidekiq workers are processing jobs (default active state)
  • The pipeline is not blocked by manual approval gates
Where this breaks in practice:
  • Instances with CI/CD disabled at the instance level are not vulnerable
  • Protected-branch rules do not apply — the attacker's own branch triggers the pipeline
Detection/coverage: Sidekiq worker crash logs; abnormal Ruby process memory patterns; Sentry/error-tracking alerts for Onigmo segfaults.
STEP 05

Heap corruption → arbitrary code execution as git user

The truncated bytecode causes Onigmo's VM to execute attacker-controlled instructions, breaking control-flow integrity in the Ruby process. The attacker pivots from regex-VM control to OS command execution, running arbitrary commands as the git user. Rapid7's PoC demonstrates this works reliably on GitLab CE 19.4.1 with ASLR enabled. The git user has read/write access to all repositories, CI/CD secrets, runner registration tokens, and deploy keys on the instance.
Conditions required:
  • Steps 1–4 completed successfully
  • Target is a Linux amd64 self-managed instance (PoC confirmed on this platform)
Where this breaks in practice:
  • SELinux in enforcing mode may constrain post-exploitation lateral movement
  • Container-isolated GitLab deployments (Kubernetes Helm chart) limit blast radius to the container, though secrets and repo data are still accessible
Detection/coverage: EDR alerts for unexpected child processes of the git user; auditd rules for execve from gitlab-rails worker PIDs; network-level detection of reverse shells or unusual egress from the GitLab host.
03 · Compensating Control

1
CRITICAL 9.8→HIGH 8.1
SEVERITY REDUCED
Disable self-registration immediately — Set gitlab_rails['gitlab_signup_enabled'] = false in gitlab.rb and run gitlab-ctl reconfigure. This breaks Step 1 of the attack chain by preventing unauthenticated attackers from obtaining the account needed to create projects and push code. Deploy within 3 days per the noisgate mitigation SLA for CRITICAL findings. Existing authenticated insiders could still exploit this, so pair with project-creation restrictions.
2
CRITICAL 9.8→MEDIUM 6.5
SEVERITY REDUCED
Restrict public project creation to admins only — Under Admin → Settings → General → Visibility, set default project visibility to Private and restrict project creation to administrators or specific groups. This blocks Step 2 even for authenticated users who cannot create the UTF-8-path public project needed for the exploit. Combined with disabling self-registration, this reduces the attack surface to trusted insiders with admin or maintainer roles.
3
CRITICAL 9.8→MEDIUM 5.5
SEVERITY REDUCED
Apply network-level access controls — Place GitLab behind a VPN, Zero Trust proxy (e.g., Cloudflare Access, Zscaler ZPA), or IP-allowlist firewall rule. This eliminates the internet-facing attack surface entirely and is the single most effective control if patching requires a maintenance window. The 25,766 Censys-exposed hosts should be the first targets for this control.
4
CRITICAL 9.8→IGNORE 0.0
SEVERITY REDUCED
Patch to 19.2.8, 19.3.5, or 19.4.2 — The definitive fix. Apply the vendor patch within the 3-day noisgate mitigation SLA if possible; no later than the 90-day noisgate remediation SLA for CRITICAL. Given the public PoC and supply-chain blast radius, treat this as a same-week emergency patch. Test in staging first — the patch modifies the regex compilation path and could theoretically affect complex CI/CD configurations. Back up gitlab-secrets.json before upgrading.
5
CRITICAL 9.8→CRITICAL 9.0
Deploy detection rules for post-exploitation indicators — Add auditd rules for unexpected execve calls from the git user, monitor Sidekiq worker crash rates, alert on new SSH keys or personal access tokens created by recently registered accounts, and watch for reverse-shell egress patterns from the GitLab host. This does not prevent exploitation but dramatically shortens dwell time.
What doesn't work
  • WAF regex-payload rules — the malicious regex is embedded in a .gitlab-ci.yml file committed via Git push (SSH or HTTPS), not in an HTTP request parameter. Standard WAF signatures cannot inspect Git pack data.
  • Rate limiting on the CI lint API — the exploit does not use the lint endpoint directly; it triggers compilation via a normal git push that activates the CI pipeline. Rate-limiting API calls has no effect on this path.
  • MFA enforcement — the attacker creates their own account and enrolls their own MFA device. MFA prevents account takeover, not self-registration abuse.
  • Container image scanning — the vulnerability is in GitLab's Ruby application layer, not in a base image dependency. Standard container vulnerability scanners will not flag it unless they specifically check the GitLab application version.
04 · Intelligence Metadata

The supporting signals.

In-the-wild exploitationNo confirmed exploitation as of 2026-10-16. Neither GitLab, CISA, nor Censys have observed active campaigns. GreyNoise has not published a dedicated tag. SOCRadar and Feedly both report 'no confirmed attribution' with low confidence.
Proof-of-concept availabilityPublic PoC exists. Rapid7 published jburgess-r7/CVE-2026-97229-PoC on GitHub — validated against GitLab CE 19.4.1 on Linux amd64 with ASLR. A second repo AmpedWasTaken/CVE-2026-97229-GitLab-CI-CD-Regex-Compiler-RCE is indexed on Sploitus. Both demonstrate full RCE.
EPSS score0.00547 (0.547%) — low exploitation probability per the FIRST model, placing this in roughly the 80th percentile. This reflects the recency of disclosure (3 days) and absence of ITW activity; expect this to climb as scanners propagate.
CISA KEV statusNot listed in the Known Exploited Vulnerabilities catalog as of 2026-10-16.
CVSS vector interpretationCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H — Network-attackable, low complexity, no privileges, no user interaction, scope change (escapes the regex sandbox into the OS), full CIA impact. The PR:N is accurate for default-config self-managed instances with open registration.
Affected version rangesGitLab CE/EE 19.2.0–19.2.7, 19.3.0–19.3.4, 19.4.0–19.4.1. Versions before 19.2.0 are not affected — the vulnerable regex compiler code path was introduced in the 19.2 release.
Fixed versions19.2.8, 19.3.5, 19.4.2 — released October 12, 2026. GitLab.com was patched same day. GitLab Dedicated environments patched automatically.
Internet exposureCensys identifies 25,766 internet-facing hosts running vulnerable versions out of 82,454 total detected GitLab instances (~31% unpatched). Geographic concentration: Germany (6,469), Russia (4,173), United States (2,927).
Disclosure timelineReported by Johan Carlsson (joaxcar) via GitLab's HackerOne bug bounty program. Patch released 2026-10-12; CVE published to NVD 2026-10-13; Rapid7 PoC published 2026-10-13.
ReporterJohan Carlsson (joaxcar) — independent security researcher, HackerOne contributor.

Sources.

  1. GitLab Critical Patch Release 19.4.2, 19.3.5, 19.2.8
  2. Rapid7 ETR: CVE-2026-97229 Unauthenticated RCE in GitLab
  3. SecurityOnline: Public PoC Disclosed for CVSS 10.0 GitLab RCE
  4. Help Net Security: GitLab admins urged to patch critical RCE flaw
  5. SOCRadar: CVE-2026-97229 Critical GitLab RCE
  6. The Hacker News: GitLab Patches CVSS 10.0 Flaw
  7. CERT-EU Advisory: Critical Vulnerability in GitLab CE/EE
  8. Feedly CVE Intelligence: CVE-2026-97229
05 · The Call

Final Verdict
= UNCHANGED to CRITICAL (9.8/10)

Why this verdict

  • Self-registration prerequisite (−0.2): The CVSS vector claims PR:N, but the chain requires creating an account and a project. On default-config self-managed GitLab this is trivially automatable, but instances with registration disabled or admin-approval gates are shielded. This narrows the truly zero-auth population by an estimated 15–25% of enterprise deployments, warranting a minor deduction from the perfect 10.
  • Self-managed only scope: GitLab.com (the largest single deployment) and GitLab Dedicated are already patched. The vulnerability only threatens self-managed instances, which is the majority of enterprise deployments but excludes the SaaS population entirely.
  • No ITW exploitation yet: Despite a public PoC since October 13, no confirmed exploitation has been observed in 3 days. EPSS is 0.547%. This does *not* lower the ceiling — it just means the window for proactive patching is still open.
  • Role multiplier: CI/CD and supply-chain platform (CRITICAL floor enforced). GitLab is a *canonical* high-value-role component — it is literally named in the CI/CD + artifact stores catalog. ≥90% of GitLab self-managed installations serve as the primary source code repository and CI/CD engine for their organization. A compromised GitLab instance gives the attacker: (1) read/write to all source code, (2) all CI/CD secrets and environment variables, (3) runner registration tokens for lateral movement into build infrastructure, (4) deploy keys for production environments, (5) supply-chain injection capability by modifying any pipeline. Blast radius is supply-chain-scale. This floors the verdict at CRITICAL regardless of other friction.
  • PoC maturity is high: Rapid7's published exploit works with ASLR enabled on the current release. The attack is fully automatable — from registration to shell in a single script. The gap between 'PoC published' and 'mass scanning begins' is historically 3–7 days for GitLab vulnerabilities (cf. CVE-2023-7028 timeline).

Why not higher?

The vendor already assigned the maximum possible score of 10.0. The only conceivable 'higher' would be if we invented a score above 10. We deducted 0.2 because the attack is not truly zero-privilege — self-registration is a prerequisite that some fraction of deployments have disabled. A CVSS 9.8 is still the highest score possible for a PR:L vector with otherwise identical parameters, and CRITICAL is the highest severity bucket.

Why not lower?

Downgrading below CRITICAL would require evidence that either (a) the high-value-role floor does not apply, or (b) the attack chain has a hard prerequisite that fails in >99% of deployments. Neither condition is met. GitLab self-managed is definitionally a CI/CD platform — the high-value-role floor applies to effectively 100% of installs. The self-registration prerequisite is satisfied by default configuration. The PoC is public, functional, and ASLR-resistant. Reducing to HIGH would misrepresent the blast radius of a supply-chain-scale compromise and leave defenders under-prioritizing a vulnerability that is days away from mass exploitation.

06 · Verification

Crowdsourced verification payload.

Run this script on each self-managed GitLab host as root or a user with read access to /opt/gitlab/. No arguments needed. Invoke with: sudo bash check_cve_2026_97229.sh. Exits 1 for VULNERABLE, 0 for PATCHED, 2 for UNKNOWN.

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/bin/bash
# check_cve_2026_97229.sh — Detect CVE-2026-97229 (GitLab CI/CD regex RCE)
# Run on the target GitLab host as root.
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN

set -euo pipefail

VERSION_FILE="/opt/gitlab/embedded/service/gitlab-rails/VERSION"

if [[ ! -f "$VERSION_FILE" ]]; then
  # Try Docker / Kubernetes path
  VERSION_FILE="/opt/gitlab/embedded/service/gitlab-rails/VERSION"
  if command -v gitlab-rake &>/dev/null; then
    RAW_VERSION=$(gitlab-rake gitlab:env:info 2>/dev/null | grep -oP 'GitLab:\s+\K[0-9]+\.[0-9]+\.[0-9]+')
  else
    echo "UNKNOWN — cannot determine GitLab version. Is this a GitLab host?"
    exit 2
  fi
else
  RAW_VERSION=$(cat "$VERSION_FILE" 2>/dev/null)
fi

if [[ -z "${RAW_VERSION:-}" ]]; then
  echo "UNKNOWN — could not read GitLab version."
  exit 2
fi

echo "Detected GitLab version: $RAW_VERSION"

# Parse major.minor.patch
IFS='.' read -r MAJOR MINOR PATCH <<< "$RAW_VERSION"

# Only versions 19.2.x through 19.4.x are affected
if (( MAJOR != 19 )); then
  echo "PATCHED — major version $MAJOR is outside the affected range (19.2–19.4)."
  exit 0
fi

if (( MINOR < 2 )); then
  echo "PATCHED — version $RAW_VERSION predates the vulnerable code (introduced in 19.2.0)."
  exit 0
fi

if (( MINOR > 4 )); then
  echo "PATCHED — version $RAW_VERSION is newer than all affected branches."
  exit 0
fi

# Check each affected branch
case "$MINOR" in
  2)
    if (( PATCH >= 8 )); then
      echo "PATCHED — $RAW_VERSION includes the fix (19.2.8+)."
      exit 0
    else
      echo "VULNERABLE — $RAW_VERSION is affected. Upgrade to 19.2.8 or later."
      exit 1
    fi
    ;;
  3)
    if (( PATCH >= 5 )); then
      echo "PATCHED — $RAW_VERSION includes the fix (19.3.5+)."
      exit 0
    else
      echo "VULNERABLE — $RAW_VERSION is affected. Upgrade to 19.3.5 or later."
      exit 1
    fi
    ;;
  4)
    if (( PATCH >= 2 )); then
      echo "PATCHED — $RAW_VERSION includes the fix (19.4.2+)."
      exit 0
    else
      echo "VULNERABLE — $RAW_VERSION is affected. Upgrade to 19.4.2 or later."
      exit 1
    fi
    ;;
  *)
    echo "UNKNOWN — version $RAW_VERSION is on an unrecognized minor branch."
    exit 2
    ;;
esac