Like handing every intern a key that lets them rewrite the blueprints for the entire factory
CVE-2026-89078 is a double-free memory corruption vulnerability in Ruby's Onigmo regular expression engine, exposed through GitLab CE/EE's CI/CD pipeline configuration parser. Any authenticated user with Developer-level access to *any* project can craft a malicious regular expression inside .gitlab-ci.yml that triggers the double-free during Regexp.new compilation. The resulting heap corruption can be weaponized into arbitrary code execution on the GitLab server process, running as the git user. Affected versions span GitLab 19.2.0–19.2.6, 19.3.0–19.3.2, and 19.4.0. The underlying Onigmo bug (PR #13332) was also patched upstream in Ruby 4.0.9, 3.4.12, and 3.3.13 on October 23, 2026. The researcher joaxcar reported this and three sibling regex-engine CVEs through HackerOne in a single month.
The vendor rates this CVSS 9.9 CRITICAL with Scope Changed (S:C), and for once the vendor score is close to reality. The PR:L (low-privilege authentication) is real friction — an external attacker does need *some* credentials — but the barrier is thin: GitLab instances routinely grant Developer access to hundreds or thousands of users, many have self-registration enabled, and federated SSO means a compromised corporate identity is sufficient. The sibling CVE-2026-97229 was later found exploitable *without* authentication at all (CVSS 10.0), and Rapid7 published a working PoC for it on October 13 demonstrating ASLR bypass and command execution. The same Onigmo code paths are involved. We downgrade only slightly from 9.9 to 9.1 because this specific CVE requires authentication and has no standalone public PoC yet — but the blast radius of a compromised GitLab server (source code, CI/CD secrets, deployment keys, registry tokens) keeps it firmly CRITICAL.
5 steps from start to impact.
Obtain authenticated GitLab access
- Valid GitLab account with Developer role or above
- Target runs GitLab CE/EE 19.2.0–19.4.0 (self-managed)
- Requires authentication — blocks opportunistic unauthenticated scanning
- Organizations with strict onboarding may limit who gets Developer access
Craft malicious regex in .gitlab-ci.yml
.gitlab-ci.yml file containing a specially crafted regular expression that triggers the Onigmo double-free during parsing. The regex must cause a parse failure *after an alternation* in a specific way that frees the same memory region twice. The attacker pushes this to any branch — protected branch restrictions do not prevent pipeline config from being parsed on feature branches.- Write access to any repository branch
- Knowledge of the Onigmo double-free trigger pattern
- No public PoC exists for this specific CVE yet
- Heap exploitation requires understanding Onigmo/Ruby allocator internals
- The sibling CVE-2026-97229 Rapid7 PoC demonstrates the same class of attack is achievable
.gitlab-ci.yml modifications.Pipeline triggers regex compilation
.gitlab-ci.yml and compiles regex patterns using Ruby's Regexp.new. The crafted pattern triggers the double-free in Onigmo's C-level parser, corrupting the heap allocator's metadata. This happens server-side during pipeline validation — the attacker does not need to wait for a pipeline to actually run, just for the config to be parsed.- CI/CD pipelines enabled on the target project (default)
- GitLab server compiles the regex before any sandbox or validation rejects it
- Some organizations disable CI/CD on certain projects, but this is uncommon
- The parse happens early in the pipeline lifecycle before most policy checks
Heap corruption → arbitrary code execution
git user on the GitLab server. The same techniques apply to this double-free primitive.- Successful heap grooming to control freed memory
- ASLR bypass technique (demonstrated feasible by Rapid7 for sibling CVE)
- Heap exploitation is non-trivial and environment-dependent
- Different Ruby/OS/ASLR configurations may require exploit tuning
- No turnkey exploit exists for this specific CVE
Post-exploitation: secrets exfiltration and supply-chain pivot
git user on the GitLab server, the attacker can read all repository contents, CI/CD variables (including protected/masked secrets), Runner registration tokens, and database credentials from /etc/gitlab/gitlab-secrets.json. From here, the attacker can inject backdoors into any repository, compromise CI/CD pipelines to push malicious artifacts downstream, exfiltrate SSH keys and deploy tokens, and pivot to internal infrastructure via Runner networks.- Successful code execution from step 4
- GitLab stores secrets accessible to the git user (default architecture)
- Organizations using external secret managers (Vault, etc.) reduce the blast radius
- Network segmentation may limit lateral movement from the GitLab host
/etc/gitlab/gitlab-secrets.json and /var/opt/gitlab/. Network monitoring for unusual egress from the GitLab server. Git hook modifications or unexpected pushes to protected branches..gitlab-ci.yml definitions to a centrally managed, access-controlled repository using GitLab's external CI/CD configuration feature. This prevents arbitrary users from injecting crafted regex patterns through pipeline configs. Deploy within 3 days as an interim control if patching is delayed..gitlab-ci.yml files with unusually complex or deeply nested regex patterns. This is a best-effort heuristic — evasion is likely — but it raises the cost of exploitation. Deploy within 3 days./bin/sh, curl, wget). Monitor for SIGSEGV/SIGABRT crashes in Ruby processes. Deploy within 3 days.- Rate limiting or IP blocking — the exploit is a single crafted git push, not a brute-force attack. Rate limiting will not prevent a one-shot exploitation attempt from an authenticated user.
- Protected branches alone — GitLab parses
.gitlab-ci.ymlfrom *any* branch push, not just merges to protected branches. The regex compilation happens during pipeline validation on feature branches before any merge request approval. - GitLab's built-in CI/CD linting — the linter validates YAML structure and keyword syntax but does not sanitize regex patterns passed to
Regexp.new. The malicious pattern is syntactically valid YAML. - Ruby-level regex timeout (
Regexp.timeout) — the double-free triggers during *compilation* (parsing), not during *matching*. Regex timeout settings only apply to match execution and do not prevent the memory corruption.
The supporting signals.
| In-the-Wild Exploitation | Not confirmed for CVE-2026-89078 specifically. However, the sibling CVE-2026-97229 (same Onigmo regex engine, integer overflow variant) has active exploitation confirmed within 24 hours of disclosure. CVE-2026-110264 (unauthenticated variant, CVSS 10.0) also has confirmed in-the-wild exploitation per InfoQ. |
|---|---|
| Proof-of-Concept | No public PoC for CVE-2026-89078 specifically. However, Rapid7 published a full PoC for sibling CVE-2026-97229 on Oct 13, 2026, demonstrating ASLR bypass and command execution as the git user. An OppositDeer PoC exists for CVE-2026-110264. The underlying Onigmo fix is tracked in PR #13332. No repos named CVE-2026-89078 found on GitHub or pocindex.io as of Oct 24, 2026. |
| EPSS | 0.0044 (low, ~top 10% percentile). This reflects the authentication requirement and lack of standalone PoC. Expect this to rise as the Rapid7 sibling PoC lowers the bar for adaptation. |
| CISA KEV | Not listed. The sibling CVE-2026-110264 (unauthenticated, CVSS 10.0) is the more likely KEV candidate if CISA acts on this family. |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:L — Network-accessible, low complexity, low privilege required, no user interaction, Scope Changed (compromise extends beyond GitLab process to underlying server and downstream systems). The S:C is earned — code execution on a GitLab server directly exposes secrets, repos, and CI/CD infrastructure. |
| Affected Versions | GitLab CE/EE 19.2.0–19.2.6, 19.3.0–19.3.2, 19.4.0. The underlying Onigmo bug affects Ruby ≤4.0.8, ≤3.4.11, ≤3.3.12 and all unsupported Ruby series (3.2 and older receive no patches). |
| Fixed Versions | GitLab 19.2.7, 19.3.3, 19.4.1 (Sept 23, 2026). Ruby 4.0.9, 3.4.12, 3.3.13 (Oct 23, 2026). A subsequent release (19.4.2, 19.3.4, 19.2.8) also includes these fixes. Note: Ruby 3.2 and older are EOL — no backport available. |
| Exposure Surface | Shodan shows ~60,000 internet-facing GitLab self-managed instances; Censys reports ~86,000. Top countries: China (19K), US (13K), Germany (12K), Russia (8.6K), France (5K). GitLab.com (SaaS) was patched by GitLab Inc. before public disclosure. |
| Researcher | joaxcar via HackerOne. Reported four Onigmo regex-engine CVEs in GitLab within a single month (CVE-2026-89078, CVE-2026-93577, CVE-2026-97229, CVE-2026-110264). |
| Vendor Detection Signatures | GitLab released six threat-detection signatures with the Sept 23 patch to identify exploitation attempts on self-managed instances. No Nuclei template confirmed for this specific CVE as of Oct 24, 2026. |
Sources.
- GitLab Critical Patch Release 19.4.1, 19.3.3, 19.2.7
- Ruby Onigmo Regexp Fixes for GitLab CVEs (ruby-lang.org)
- Strix.ai CVE-2026-89078 Detail
- CiberSafety — Critical RCE in CI/CD, CVE-2026-89078/93577
- nflo.tech — CVE-2026-93577, CVE-2026-89078 Analysis
- Safeguard.sh — Ruby Onigmo Fixes Behind Four GitLab RCE CVEs
- CERT-EU Advisory 2026-016 — Critical GitLab Vulnerability
- Daily Security Review — Public Exploit for CVE-2026-97229
Why this verdict
- Authentication friction (−0.4): PR:L requires a valid GitLab Developer account. This blocks drive-by unauthenticated scanning and narrows the attacker pool — but the barrier is thin. GitLab instances routinely have hundreds to thousands of Developer-level users, self-registration is often enabled by default, and federated SSO means any compromised corporate identity suffices. Downward pressure is real but modest.
- No standalone PoC (−0.3): No public exploit exists for CVE-2026-89078 specifically. However, the Rapid7 PoC for sibling CVE-2026-97229 demonstrates the full chain (ASLR bypass, code execution as
gituser) in the *same Onigmo regex engine*, making adaptation to the double-free primitive a matter of exploit development rather than novel research. This gap is closing. - Heap exploitation complexity (−0.1): Double-free → RCE requires heap grooming and ASLR bypass. This is non-trivial but well-studied, and the sibling PoC proves it's achievable in this specific Ruby/Onigmo environment. Minimal additional friction.
- Role multiplier: GitLab is a canonical high-value-role component. GitLab is by definition a CI/CD platform, source code repository, and secrets store — hitting three entries in the high-value catalog simultaneously (CI/CD + artifact stores, supply-chain registries, and effectively a secret manager via CI/CD variables). The blast radius of a compromised GitLab server is supply-chain scale: the attacker gains access to all source code, CI/CD secrets, deployment keys, Runner tokens, and the ability to inject malicious code into every downstream build artifact. 100% of GitLab installations occupy this high-value role by definition. The chain documented here (auth → crafted regex → RCE as git user → secret exfiltration → supply-chain pivot) succeeds in every deployment role from dev sandbox to enterprise production. Verdict floor: CRITICAL.
- Sibling exploitation pressure: Four CVEs in the same Onigmo code path within one month, escalating from authenticated (this CVE) to unauthenticated (CVE-2026-110264, CVSS 10.0) with active in-the-wild exploitation. This family is under active attacker research and the authenticated requirement may be the only thing keeping *this specific* CVE from mass exploitation today.
Why not higher?
The vendor's 9.9 assumes trivial exploitation at low complexity. In practice, the authentication requirement meaningfully reduces the externally-reachable attack surface compared to the unauthenticated sibling CVE-2026-110264 (CVSS 10.0), and no standalone PoC exists for the double-free variant yet. The heap exploitation step, while demonstrated feasible for the sibling integer overflow, still requires adaptation work. These factors justify a modest 0.8-point reduction from the vendor score rather than a full match.
Why not lower?
GitLab is a canonical supply-chain component where code execution directly translates to fleet-scale impact — source code theft, secret exfiltration, pipeline poisoning, and downstream artifact compromise. The authentication barrier is thin (Developer access is widely granted), a working PoC for the sibling CVE in the same code path proves the exploitation chain is practical, and ~60K–86K self-managed instances are internet-facing. Dropping below CRITICAL would require evidence that the double-free primitive is fundamentally unexploitable or that the vast majority of GitLab instances have compensating controls that prevent CI/CD config parsing — neither is true.
Crowdsourced verification payload.
Run this script on each self-managed GitLab server (Omnibus or source install) as root or a user with read access to /opt/gitlab/ or the GitLab installation directory. Example: sudo bash check_cve_2026_89078.sh. No network access required.
#!/usr/bin/env bash
# check_cve_2026_89078.sh — Detect CVE-2026-89078 (GitLab Onigmo double-free)
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
set -euo pipefail
VERSION_FILE="/opt/gitlab/version-manifest.txt"
VERSION_FILE_ALT="/opt/gitlab/VERSION"
GL_VERSION=""
# Try Omnibus version file first
if [[ -f "$VERSION_FILE_ALT" ]]; then
GL_VERSION=$(cat "$VERSION_FILE_ALT" | head -1 | tr -d '[:space:]')
elif command -v gitlab-rake &>/dev/null; then
GL_VERSION=$(gitlab-rake gitlab:env:info 2>/dev/null | grep -i 'Version:' | head -1 | awk '{print $2}' | tr -d '[:space:]')
elif [[ -f "/home/git/gitlab/VERSION" ]]; then
GL_VERSION=$(cat /home/git/gitlab/VERSION | head -1 | tr -d '[:space:]')
fi
if [[ -z "$GL_VERSION" ]]; then
echo "UNKNOWN — Could not determine GitLab version. Is this a GitLab server?"
exit 2
fi
echo "Detected GitLab version: $GL_VERSION"
# Parse major.minor.patch
IFS='.' read -r MAJOR MINOR PATCH <<< "$(echo "$GL_VERSION" | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+')"
if [[ -z "$MAJOR" || -z "$MINOR" || -z "$PATCH" ]]; then
echo "UNKNOWN — Could not parse version string: $GL_VERSION"
exit 2
fi
# Affected: 19.2.0-19.2.6, 19.3.0-19.3.2, 19.4.0
# Fixed: 19.2.7+, 19.3.3+, 19.4.1+
if (( MAJOR < 19 )); then
echo "PATCHED — Version $GL_VERSION is below the affected range (19.2–19.4)."
exit 0
elif (( MAJOR > 19 )); then
echo "PATCHED — Version $GL_VERSION is above the affected range."
exit 0
elif (( MAJOR == 19 )); then
if (( MINOR < 2 )); then
echo "PATCHED — Version $GL_VERSION is below the affected range (19.2+)."
exit 0
elif (( MINOR == 2 )); then
if (( PATCH >= 7 )); then
echo "PATCHED — Version $GL_VERSION includes the fix (19.2.7+)."
exit 0
else
echo "VULNERABLE — Version $GL_VERSION is affected (19.2.0–19.2.6). Upgrade to 19.2.7+."
exit 1
fi
elif (( MINOR == 3 )); then
if (( PATCH >= 3 )); then
echo "PATCHED — Version $GL_VERSION includes the fix (19.3.3+)."
exit 0
else
echo "VULNERABLE — Version $GL_VERSION is affected (19.3.0–19.3.2). Upgrade to 19.3.3+."
exit 1
fi
elif (( MINOR == 4 )); then
if (( PATCH >= 1 )); then
echo "PATCHED — Version $GL_VERSION includes the fix (19.4.1+)."
exit 0
else
echo "VULNERABLE — Version $GL_VERSION is affected (19.4.0). Upgrade to 19.4.1+."
exit 1
fi
elif (( MINOR > 4 )); then
echo "PATCHED — Version $GL_VERSION is above the affected minor range."
exit 0
fi
fi
echo "UNKNOWN — Could not determine vulnerability status for $GL_VERSION."
exit 2