SecurityDevOps

GitLab CVE-2026-97229: Exploited, in CISA KEV, and the Fix Lives in Ruby

November 3, 2026
14 min read
A red ruby resting on a steel pipe beside a valve junction lit by an orange warning lamp in a dim industrial room
Share:

CVE-2026-97229, the CVSS 10 flaw in GitLab’s CI/CD regular-expression compiler that GitLab patched on 12 October, entered CISA’s Known Exploited Vulnerabilities catalog on 29 October with a federal due date of 1 November, two weeks after Rapid7 published a working proof of concept. Shadowserver still counted more than 1,000 vulnerable servers online over the weekend, and the fix has a catch that bit Alpine Linux: it is a patch to the Ruby that GitLab bundles, so a GitLab that runs on its own Ruby stays exploitable after the upgrade.

In my 21 October security-day roundup this bug got one line, and its successor CVE-2026-110264 one card. Since then it has picked up a public exploit, a documented attack on Alpine Linux’s GitLab and a three-day federal deadline. Below: what changed, what it means, and this week’s work: read-only checks, upgrades per install type and what to do if you can’t patch today. Versions and commands come from GitLab’s advisories and docs, Ruby’s announcement, CISA’s KEV feed and my own reading of the omnibus-gitlab release tags on 3 November.

1. Context: From Patch Note to KEV Entry in 17 Days

GitLab’s 12 October advisory describes the flaw in one sentence: an unauthenticated user could run arbitrary code on the server ‘via a specially crafted regular expression in a CI/CD configuration, due to insufficient regular expression validation.’ It affects CE and EE 19.2 before 19.2.8, 19.3 before 19.3.5 and 19.4 before 19.4.2, scores CVSS 3.1 10.0 and was reported by joaxcar through HackerOne. It is also a bypass: GitLab’s detection notes say the 23 September fix for CVE-2026-93577 was incomplete. Underneath, both are the same integer overflow in Onigmo, the regex engine built into Ruby, which Ruby fixed upstream on 23 October in 3.3.13, 3.4.12 and 4.0.9.

Since then the threat picture has changed. On 13 October Rapid7 published a proof of concept that runs commands as the git user on the official 19.4.1 CE image. On 20 and 21 October a newly registered account sent crafted CI configurations to Alpine Linux’s GitLab, crashing its workers eight times; Alpine found no proof the code ran, but rebuilt the server. On 29 October CISA added the CVE to its KEV catalog with a 1 November due date for US federal agencies, the three-day window BOD 26-04 gives a bug that is automatable, grants total control and sits on an internet-facing asset.

Timeline (dates as published by each source)

10–11 SepGitLab fixes CVE-2026-85706, an unauthenticated file read (CVSS 10); CISA lists it in KEV the next day.
23 Sep19.4.1, 19.3.3 and 19.2.7 fix CVE-2026-93577 (CVSS 9.9). The fix turns out to be incomplete.
12 Oct19.4.2, 19.3.5 and 19.2.8 fix CVE-2026-97229, the bypass, with a patched Ruby inside.
13 OctRapid7 publishes a proof of concept. Alpine upgrades to 19.3.4 instead of 19.3.5 by mistake.
20–21 OctAn attacker hits Alpine’s GitLab. GitLab fixes CVE-2026-110264 and warns that installs with their own Ruby must upgrade it too.
22–23 OctAlpine moves to 19.3.6, still vulnerable on its own Ruby. Ruby 3.3.13, 3.4.12 and 4.0.9 ship.
27–28 OctAlpine rebuilds its GitLab, republishes alpinelinux/gitlab with Ruby 3.3.13 and discloses the incident.
29 OctCISA adds CVE-2026-97229 to KEV, due 1 November. GitLab ships 19.5.3, 19.4.5, 19.3.7 and backports (no new regex fixes).
1 NovShadowserver: more than 1,000 instances vulnerable to CVE-2026-97229, more than 11,300 to CVE-2026-110264.

How each source rates it

SourceRatingWhat to read into it
GitLab (CNA, CVE record)CVSS 3.1 10.0, CriticalPR:N, UI:N: no privileges or user interaction
Rapid7RCE reproduced, PoC publicIts route needs write access to a public project, which ‘conflicts with GitLab’s PR:N rating’
CISA KEVAdded 29 Oct, due 1 NovCWE-190, forensic triage required, ransomware use ‘Unknown’
CISA SSVCActive, automatable, total impactThe mix that earns a three-day KEV deadline on internet-facing assets
Ruby coreNo CVE requestedTrack the fix under GitLab’s four CVE IDs
Debianruby3.3, ruby3.1 vulnerableNo fixed package when I checked on 3 November

The fix is a Ruby patch, not GitLab code

GitLab’s 21 October advisory says it in so many words about CVE-2026-110264: ‘The fix … is a patch to Ruby, not to GitLab code, and it is only included in official GitLab Linux packages and container images.’ CVE-2026-97229 works the same way: the 12 October notes list ‘Add Ruby regexp program size patch for 3.3 and 3.4’, and GitLab’s self-compiled guide now says upgrading GitLab alone does not apply the fix.

Where things stand (checked 3 November 2026)

🟢

Fixed versions

GitLab’s advisories, the CVE records and the omnibus-gitlab release tags agree on 19.4.2, 19.3.5 and 19.2.8, and on the 21 October releases for CVE-2026-110264. The newest patch on every line is from 29 October.

🟢

KEV listing

From CISA’s KEV JSON feed: added 29 October, due 1 November, forensic triage ‘Yes’, and a note that ‘other products can potentially be affected by the same underlying Ruby/Onigmo Integer Overflow issue’.

🔴

Exploitation in the wild

CISA doesn’t publish its evidence. The public account is Alpine’s: eight worker crashes, four that ‘got partway through the exploit and failed at the next step’, and no proof of success. I found no confirmed compromise, which is absence of evidence, not evidence of absence.

🟡

Exposure

BleepingComputer, citing Shadowserver on 1 November: more than 1,000 instances vulnerable to CVE-2026-97229 and more than 11,300 to CVE-2026-110264, out of about 39,000 GitLab instances seen daily.

⚪

Still open

Rapid7 doesn’t know how the private report avoids the write-access step; GitLab’s issues go public 90 days after each fix. CVE-2026-110264 isn’t in KEV, and Debian’s Ruby has no fix yet.

Golden rule

Judge a GitLab server by who built its Ruby. On official packages, images and charts, the GitLab version is the fix; on anything with its own Ruby, the fix is the GitLab version plus Ruby 3.3.13, 3.4.12 or 4.0.9 or later in the same series.

2. Five Implications for Teams That Run GitLab

Rapid7’s technical analysis and GitLab’s published detection rules are the two best maps of how the bug is reached and what an attempt leaves behind. Read alongside Alpine’s write-up, they change how I would prioritise it.

🚨

The KEV deadline has already passed

CISA KEV · BOD 26-04

The federal due date was Sunday 1 November, three days after the listing. For everyone else that reads as ‘today’: an internet-facing GitLab on 19.2 to 19.4 below the fixed releases faces a public exploit, and may have been probed since mid-October.

🔐

‘Unauthenticated’ in practice means ‘anyone who can sign up’

Rapid7 · Alpine · GitLab detections

GitLab rates it PR:N, but Rapid7’s working route needs a CI file committed to a public project the attacker can write to; only the final CI lint request skips credentials. Alpine’s attacker used a newly registered account. With open sign-up and public projects, assume that prerequisite is met.

🧬

The fix lives in Ruby, so the GitLab version can mislead

GitLab advisory · Ruby · Alpine

Official packages, images and charts ship a Ruby GitLab patched itself; self-compiled installs and community images bring their own, which a GitLab upgrade doesn’t touch. Alpine made both mistakes: it installed 19.3.4 instead of 19.3.5, then moved to 19.3.6 on its own Ruby and found the server still vulnerable on 26 October.

🔁

12 October is not enough, and 110264 is the wider exposure

GitLab · Shadowserver

CVE-2026-110264, fixed on 21 October, is a second CVSS 10 Onigmo bug, reached by rendering Org-mode files, and it affects every version from 7.0. It isn’t in KEV, but Shadowserver counts about eleven times more servers vulnerable to it. A server patched on 12 October is still exposed.

🧪

Exploits are build-specific, and attackers fetch your build

Rapid7 · Alpine

Rapid7 validated its chain on one image, official 19.4.1 CE on amd64, and says other builds need adjusted offsets. No comfort for custom builds: Alpine says its attacker ‘downloaded our GitLab container image, which suggests they were tuning the exploit to our exact build.’ Failed attempts still crash Puma or Sidekiq workers, your best early signal.

Which release fixes what, per GitLab line

LineCVE-2026-97229 (12 Oct)CVE-2026-110264 (21 Oct)Latest patch (29 Oct)Bundled Ruby, as ruby -v reports it
19.5Not affected19.5.119.5.33.3.12, patched by GitLab
19.419.4.219.4.319.4.53.3.12, patched by GitLab
19.319.3.5 (not 19.3.4)19.3.619.3.73.3.12, patched by GitLab
19.219.2.819.2.919.2.103.3.11, patched by GitLab
19.1 · 19.0Not affected19.1.9 · 19.0.1019.1.10 · 19.0.113.3.11, patched by GitLab
18.11Not affected18.11.1318.11.143.3.10, patched by GitLab
18.10 and olderNot affectedNo backportUpgrade path to 18.11.14 or laterUnpatched

Where the fix lives, per install type

Install typeWhere its Ruby comes fromWhat fixes CVE-2026-97229
Linux package (deb/rpm)Bundled in /opt/gitlab/embedded, patched by GitLabUpgrading GitLab to a fixed release
Official Docker image (gitlab/gitlab-ce, gitlab/gitlab-ee)The Linux package inside the imagePulling a fixed tag and recreating the container
Helm chart (cloud-native images)CNG images, patched by GitLabUpgrading to a chart version mapped to a fixed release
Self-compiled (source)The system Ruby you installedUpgrading GitLab and Ruby to 3.3.13, 3.4.12 or 4.0.9 or later
Community or in-house images, alpinelinux/gitlab includedWhatever the Dockerfile installsUpgrading GitLab and rebuilding on a fixed Ruby
GitLab.com · GitLab DedicatedManaged by GitLabNothing to do: already patched

Some outlets list 19.3.4 as fixed; GitLab’s advisory and the CVE record say 19.3.5. Beyond GitLab, Ruby says any app that compiles regexps from attacker-controlled strings (Regexp.new, Regexp.union, Regexp.compile, /#{input}/) is exposed and that Regexp.timeout doesn’t protect; apps that only use their own regexps are not affected.

3. What to Do This Week

Same order as any server bug in KEV: find out what you run, save the evidence, patch the way your install type demands, and close the doors the exploit uses until you can. Everything in step 1 only reads.

Step 1: Find out what you run, and whose Ruby it uses

GitLab version per install type

The rake task, API endpoint and chart mapping come from GitLab’s docs; the in-app Help > Help page also shows the version to signed-in users. On a Linux package the version answers the question, because the patched Ruby ships inside it. Swap in your own container name.

# Linux package: read 'GitLab information > Version'
sudo gitlab-rake gitlab:env:info

# Official Docker image: the same task inside the container
docker exec gitlab gitlab-rake gitlab:env:info

# Any install type, from your laptop (needs an access token)
curl --request GET \
  --header "PRIVATE-TOKEN: <your_access_token>" \
  --url "https://gitlab.example.com/api/v4/version"

# Helm chart: chart versions are not GitLab versions,
# map them on the chart version mappings page
helm list -n <namespace>

# Self-compiled: from /home/git/gitlab, as the git user
bundle exec rake gitlab:env:info RAILS_ENV=production

# fixed for CVE-2026-97229:  19.4.2, 19.3.5, 19.2.8
# fixed for both regex CVEs: 19.5.1, 19.4.3, 19.3.6, 19.2.9, 19.1.9, 19.0.10, 18.11.13

Whose Ruby is it? Containers, custom images and source installs

/opt/gitlab/embedded/bin/ruby is in the Linux package’s file list, and version-manifest.txt is where GitLab’s docs say the bundled versions are recorded. On a GitLab package, expect 3.3.10 to 3.3.12 even when fixed. Anywhere else, the Ruby number is the test.

# Linux package or official image: bundled Ruby, patched by GitLab
/opt/gitlab/embedded/bin/ruby -v        # 3.3.12 on 19.3 to 19.5, even when fixed
grep -iw ruby /opt/gitlab/version-manifest.txt

# Each running GitLab container: Linux-package image, or its own Ruby?
for c in $(docker ps --format '{{.Names}} {{.Image}}' | awk 'tolower($2) ~ /gitlab/ {print $1}'); do
  if docker exec "$c" test -x /opt/gitlab/embedded/bin/ruby; then
    echo "$c: Linux-package image, judge it by the GitLab version"
  else
    echo "$c: own Ruby: $(docker exec "$c" ruby -v 2>/dev/null || echo 'ruby not on PATH')"
  fi
done

# Self-compiled or custom image: this number must reach the fix in its series
ruby -v      # fixed: 3.3.13, 3.4.12, 4.0.9 · NOT fixed: 3.4.10, 3.4.11 · 3.2: no fix
apk info -v 2>/dev/null | grep '^ruby-[0-9]'     # Alpine: 3.3.13-r0 (3.21), 3.4.12-r0 (3.22+)
dpkg -l 'ruby3*' | awk '/^ii/ {print $2, $3}'    # Debian: ruby3.3 and ruby3.1 unfixed on 3 Nov

Don’t judge a GitLab package by ruby -v

I read the omnibus-gitlab release tags: 19.4.2 and 19.3.5 patch Ruby 3.3.12 with regexp-program-size, 19.4.3 adds regexp-repeat-id-overflow, and the version string never changes. A fully patched 19.4.5 still bundles Ruby 3.3.12, below the 3.3.13 Ruby calls fixed, so a scanner that flags it is reading the number, not the patch. The reverse trap is worse: a custom image on GitLab 19.3.6 shows a fixed GitLab over a vulnerable Ruby, exactly what Alpine ran.

Step 2: Patch per install type

Save the logs first (the yellow box below explains why), then upgrade. The 12, 21 and 29 October releases all include database migrations, so single-node instances go down while they run; multi-node instances can use GitLab’s zero-downtime procedure. From 18.11 to 19.5 you don’t need a new minor: the latest patch on your line fixes both regex CVEs.

🟢

Linux package (deb/rpm)

docs.gitlab.com/update/package · 29 Oct releases

Install the latest patch for your line, pinning the exact version as GitLab’s docs show. The package backs up the database first unless /etc/gitlab/skip-auto-backup exists; keep your own backup anyway, including /etc/gitlab/gitlab-secrets.json. The 29 October release also asks for GitLab Runner 19.5.1 (eMudhra CA change).

# Debian/Ubuntu (CE: gitlab-ce=<version>-ce.0)
sudo apt update && sudo apt install gitlab-ee=19.4.5-ee.0

# RHEL, AlmaLinux, Oracle Linux 9 (.el8 on 8)
sudo dnf install gitlab-ee-19.4.5-ee.0.el9

sudo gitlab-rake gitlab:env:info          # Version: 19.4.5-ee
🐳

Official Docker image

docs.gitlab.com/update/docker · recreate, don’t restart

Back up the database and the secrets file, pull the fixed tag and recreate the container; GitLab reconfigures and migrates on first start. The 19.5.3, 19.4.5, 19.3.7 and 19.2.10 tags (CE and EE) hit Docker Hub on 29 October. Pin an exact tag, not latest.

# Docker Engine: keep your own hostname, ports and volumes
sudo docker stop gitlab
sudo docker rm gitlab
sudo docker pull gitlab/gitlab-ee:19.4.5-ee.0
# ...then the same docker run as before, with the new tag

# Docker Compose: set image: gitlab/gitlab-ee:19.4.5-ee.0, then
docker compose pull
docker compose up -d
☸️

Helm chart (cloud-native images)

docs.gitlab.com/charts · version mappings

The CNG images carry GitLab’s Ruby patches too, so upgrading the chart is the fix. Chart versions differ from GitLab’s: 19.5.3 is 10.5.3 or 10.5.4, 19.4.5 is 10.4.5 or 10.4.6, 19.3.7 is 10.3.8 or 10.3.9, 19.2.10 is 10.2.10. Export your values, skip --reuse-values, and use the documented pause-and-resume sequence for zero downtime.

helm repo update
helm search repo -l gitlab/gitlab | head
helm get values gitlab > gitlab.yaml
helm upgrade gitlab gitlab/gitlab \
  --version 10.4.6 \
  -f gitlab.yaml \
  --set gitlab.migrations.enabled=true
🛠️

Self-compiled installations

Self-compiled install guide · Ruby, 23 Oct

Two upgrades, not one: GitLab along the documented path, and the system Ruby to 3.3.13, 3.4.12 or 4.0.9 or later in the same series, which GitLab’s guide now requires (built from the official source, not through RVM, rbenv or chruby). Then restart every Puma and Sidekiq process, because a running worker keeps the Ruby it started with.

ruby -v       # before: ruby 3.3.12 on a source install means vulnerable
# build and install Ruby 3.3.13 / 3.4.12 / 4.0.9 from ruby-lang.org, then
ruby -v       # 3.3.13 or later in your series
bundle exec rake gitlab:env:info RAILS_ENV=production    # check 'Ruby Version'
🏔️

Community and in-house images, Alpine’s included

alpinelinux.org · Docker Hub

Rebuild on a fixed Ruby, not just a newer GitLab. On Alpine bases, apk upgrade brings ruby 3.3.13-r0 (3.21) or 3.4.12-r0 (3.22 and later); official ruby images exist as 3.3.13, 3.4.12 and 4.0.9. For alpinelinux/gitlab, Alpine says ‘any 19.3.6 image you pulled before October 27, and all older tags, are vulnerable’. Check every tag with ruby -v.

# In your Dockerfile, pin a fixed Ruby base, for example
#   FROM ruby:3.4.12-alpine        (or 3.3.13 / 4.0.9)
docker build --pull --no-cache -t my-gitlab:19.4.5 .
docker run --rm --entrypoint ruby my-gitlab:19.4.5 -v
🪦

18.10 and older

Upgrade path tool · required stops

No backport of CVE-2026-110264 exists below 18.11, so you have to move. Required stops are 18.2, 18.5, 18.8 and 18.11, then 19.2, 19.5, 19.8 and 19.11 (17.3, 17.5, 17.8 and 17.11 before that). Land on the latest patch of each stop, plan with GitLab’s Upgrade Path tool, and pin the package version at every step.

Step 3: If you can’t patch today

GitLab’s advisories offer no workaround, and Ruby’s (stop compiling regexps from untrusted input) isn’t something you can do inside GitLab. What you can do is make the known route harder. Rapid7 suggests restricting credential-free access to attacker-writable public projects or blocking the CI lint route; GitLab’s detection notes add: limit who can create accounts and tokens, and reduce public visibility where you can.

Close self-registration

Both options are in GitLab’s sign-up restrictions docs, including the Rails console line. Alpine’s attacker came in through a newly registered account.

# UI: Admin > Settings > General > New user account restrictions
#     clear 'Allow new user accounts' (or tick 'Require admin approval')

# Rails console on a Linux package install
sudo gitlab-rails console
::Gitlab::CurrentSettings.update!(signup_enabled: false)

Hunt with GitLab’s own log queries

These lines come from the detection rules GitLab linked in both advisories (gitlab-security-oss/tldr), with Linux package paths; on Helm, the same logs live on the Webservice and Sidekiq pods. GitLab calls a Workhorse 502 whose correlation ID never reached api_json.log ‘the single highest-confidence signal in the set’.

WH=/var/log/gitlab/gitlab-workhorse/current
APP=/var/log/gitlab/gitlab-rails/application_json.log
PUMA=/var/log/gitlab/puma/puma_stderr.log
SIDEKIQ=/var/log/gitlab/sidekiq/current

# 1. Workers that died on the lint or GraphQL routes (the detonation)
grep -E '"uri":"[^"]*(ci/lint|/api/graphql)' "$WH"* | grep -E '"status":502|badgateway'

# 2. Patterns the CI regexp guard caught (the probe, logged verbatim)
grep 'Gitlab::Ci::Build::Rules::Rule::Clause::RegexpMatcher' "$APP"* \
  | grep -E 'rejected at compile time|match timed out|time budget exceeded'

# 3. Native Ruby crashes through Onigmo (CI path and Org-mode path)
grep -B1 -A20 '\[BUG\]' "$PUMA"* \
  | grep -E 'org-ruby|headline\.rb|regexp_matcher\.rb|match_at|onig_search_gpos|onig_compile|compile_tree|compile_length_tree|rb_str_sub_bang'
grep 'A worker terminated, shutting down the cluster' "$SIDEKIQ"*

# 4. The trigger commit: paths near 255 bytes with a non-ASCII byte
#    (run it in a clone of each suspect project)
git ls-tree -r --name-only HEAD | awk 'length($0) >= 240' | grep -P '[^\x00-\x7F]'

What these mitigations cover, and what they don’t

  • Sign-up off: stops new accounts, not existing ones. Review accounts created since 12 October and any that pushed CI files to public projects.
  • Public projects: making them private or internal shrinks Rapid7’s route and the Org-mode path of CVE-2026-110264. GitLab documents ‘Restricted visibility levels’ as limiting who can create public projects, so change existing ones by hand.
  • Blocking ci/lint: covers Rapid7’s route only; GitLab’s detections also watch the GraphQL ciLint mutation and pipeline creation, and the undisclosed route may differ.
  • Regexp.timeout and length limits: don’t help; Ruby says neither protects, and some of these bugs trigger with short patterns.
  • If code already ran: rotate what GitLab’s response notes list: the secrets in gitlab-secrets.json, every CI/CD variable, every runner token and every access token.

Save the evidence before you patch

Upgrading restarts every worker, and the crash traces go with them. CISA’s BOD 26-04 guidance puts evidence collection before patching for KEV entries that need forensic triage, as this one does, and GitLab’s notes say to preserve the full crash report before any restart. Copy /var/log/gitlab off the box, then patch. A quiet success leaves no 502, so also look for files owned by git in /tmp, unexpected child processes of Puma or Sidekiq, outbound connections, and changed crontabs or SSH keys for the git user.

The order I’d work in

  1. 1. Today: internet-facing GitLab on 19.2 to 19.4 below the fixed releases. Save the logs, then move to the latest patch on your line; if you can’t, close sign-ups and review public projects now.
  2. 2. Also today: every install with its own Ruby: self-compiled servers and community or in-house images. Check ruby -v, upgrade Ruby, rebuild, restart.
  3. 3. By Friday 6 November: every other line below the 21 October releases. Anything on 18.7 or later that also missed September’s releases is open to CVE-2026-85706, in KEV since 11 September.
  4. 4. This week: run GitLab’s log queries back to 12 October; treat a hit as an incident and rotate secrets and tokens.
  5. 5. Ongoing: your own Ruby services that compile user-supplied regexps, Debian’s Ruby fix, and GitLab’s issues for these CVEs, public 90 days after each fix.

Conclusion

Three weeks ago CVE-2026-97229 was a line in a patch release. Now it is a KEV entry with a public proof of concept, a documented attack on a well-run open-source project and more than a thousand servers still exposed. The twist is where the fix lives: in a Ruby GitLab patches inside its own packages, which is how Alpine ended up on a ‘fixed’ GitLab that was still exploitable.

On GitLab’s Linux package, Docker image or Helm chart, this is a normal patch: save the logs, move to the latest release on your line, and both regex CVEs are covered. If you bring your own Ruby, it’s two upgrades, and ruby -v is the number that tells the truth. Either way, closing open sign-up is the cheapest control you can turn on today.

This week in one screen

Check

  • • gitlab:env:info or /api/v4/version
  • • Official package, or your own Ruby?
  • • ruby -v wherever the Ruby is yours

Patch

  • • 19.5.3 / 19.4.5 / 19.3.7 / 19.2.10
  • • 19.1.10 / 19.0.11 / 18.11.14
  • • Ruby 3.3.13 / 3.4.12 / 4.0.9 if you build it

Watch

  • • Workhorse 502s on ci/lint and /api/graphql
  • • [BUG] crashes in puma_stderr.log
  • • New sign-ups and public projects

Sources

Diego Rodriguez

Diego Rodriguez

Senior Full-Stack & AI Engineer

Diego has 10+ years of experience building production-grade AI-powered applications, from LLM orchestration and RAG pipelines to ML-driven risk detection and algorithmic trading systems.

Learn more about Diego →