CVE-2026-97229 is a CVSS 10.0 remote code execution vulnerability in GitLab Community and Enterprise Edition, and it is no longer theoretical. On 20 and 21 October 2026 an attacker registered an account on Alpine Linux’s GitLab, sent crafted CI configurations, crashed the server’s workers eight times and got partway through the exploit on four of them. Alpine found no evidence that code ran, could not rule it out, and rebuilt the instance from scratch. Its packages and releases were verified against records outside GitLab, and its signing keys never lived on that server. CISA added the CVE to its Known Exploited Vulnerabilities catalog on 29 October with a 1 November deadline and a forensic triage requirement.
The detail that makes this one different is where the fix lives. The bug is in Onigmo, the regular expression engine inside Ruby, and GitLab fixed it by patching the Ruby it ships in its own packages. Alpine upgraded GitLab twice and stayed vulnerable both times, because their image ran standard Ruby. This post is three things. A plain-English account of the bug and the companion CVE-2026-110264, taken from GitLab’s, Ruby’s and Alpine’s own write-ups. A passive Nuclei template that identifies vulnerable GitLab releases from a single anonymous static file, tested against a live 19.4.1 instance and seven official images. And the part that takes longer: finding every self-managed GitLab on your attack surface, including the ones running Ruby you did not build. Credit for the discovery goes to Johan Carlsson (joaxcar), who reported it through GitLab’s HackerOne program.
CVE-2026-97229 at a glance
- CVE-2026-97229, “Integer Overflow issue in CI/CD Regular Expression Compiler”. CWE-190.
- Impact: code execution on the GitLab server as the
gituser, which can read every repository, CI/CD variable, runner token and integration secret on the instance. - CVSS 3.1: 10.0 Critical,
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H(GitLab as CNA; NVD shows the same vector). - Affected: GitLab CE/EE 19.2 before 19.2.8, 19.3 before 19.3.5, 19.4 before 19.4.2.
- Fixed: 19.2.8, 19.3.5, 19.4.2, released 12 October 2026. Note 19.3.5, not 19.3.4.
- Underlying Ruby fix: Ruby 4.0.8, 3.4.12 or 3.3.13 for this bug. Ruby 4.0.9, 3.4.12 or 3.3.13 covers all four related GitLab CVEs.
- Exploitation status: CISA KEV since 29 October 2026 (due 1 November, forensic triage required). Attempted exploitation against Alpine Linux on 20-21 October. A public proof of concept from Rapid7 has been available since mid-October.
- Scope: self-managed GitLab only. GitLab.com and GitLab Dedicated were patched before the release was published.
- Companion: CVE-2026-110264, also CVSS 10.0, same engine, same reporter, affects every release from 7.0 up to the 21 October fixes. Not on KEV.
- Fix now: move to the latest patch release for your branch (19.5.3, 19.4.5, 19.3.7, 19.2.10, 19.1.10, 19.0.11 or 18.11.14 as of 29 October). If you run GitLab on your own Ruby, upgrade Ruby too.
What is CVE-2026-97229?
GitLab lets users put regular expressions in CI/CD configuration, for example to decide whether a job runs based on which files exist or changed. Those patterns are written by whoever can push a .gitlab-ci.yml, and GitLab compiles them with Ruby’s Regexp.new, which hands them to Onigmo.
Onigmo turns a pattern into a small program for its matching virtual machine. When a pattern contains a counted repeat, say “this group, 50,000 times”, the compiler decides whether to expand the repeat inline by multiplying the size of the repeated body by the repeat count and comparing the result with a limit. That multiplication is done in a signed 32-bit integer. Pick a body size and a count whose product lands just past INT_MAX and the result wraps negative, a negative number is always “small enough to expand”, and the compiler cheerfully writes out a program of more than 2 GiB. From there, as Rapid7’s technical analysis shows, attacker-shaped bytes end up being interpreted as Onigmo instructions. That is the integer overflow in CWE-190, and the code execution that follows.
GitLab knew this bug class was live. Two earlier CVEs from the same researcher, CVE-2026-89078 (a double free in the regex parser) and CVE-2026-93577 (an integer overflow in the regex compiler), were fixed in 19.4.1 on 23 September, and that release added guards in GitLab’s own code that reject oversized repeats and oversized compiled programs. CVE-2026-97229 walks around those guards. GitLab’s own detection guidance describes 19.4.1 as carrying “an incomplete fix bypassed by CVE-2026-97229”. Two tricks do it. An inline regex comment splits the digits of a repeat count, so GitLab’s guard reads a small number while Onigmo reads a large one. And the pattern is compiled small as ASCII, passes the size check, and is then silently recompiled for UTF-8 when it is matched against a repository path containing a non-ASCII character. That second compile happens after the check has already returned.
Does it need authentication? GitLab and NVD rate it PR:N, unauthenticated. Rapid7’s public exploit path needs one thing first: write access to a public project, to plant the CI file and the oddly named path. Only the final trigger, a CI lint request, is sent without credentials. Those two statements stop conflicting the moment an instance allows open self-registration, because then anyone on the internet can get write access to a project they create. That is exactly what happened at Alpine. Defend for the vendor’s rating.
The companion: CVE-2026-110264
GitLab’s 21 October critical patch release fixed CVE-2026-110264, a second CVSS 10.0 unauthenticated code execution in the same engine. This time Onigmo’s internal IDs overflow in a pattern with many repeats. GitLab’s published detections tie it to the Org-mode (.org) file renderer rather than CI. The affected range is enormous: every release from 7.0 before 18.11.13, plus 19.0 through 19.5 before 19.0.10, 19.1.9, 19.2.9, 19.3.6, 19.4.3 and 19.5.1. It is not on KEV, and the CISA assessment attached to its NVD record lists no exploitation yet. Ruby’s 23 October releases fix the underlying bugs for all four CVEs (89078, 93577, 97229 and 110264), and Ruby’s advisory is blunt about two common assumptions: Regexp.timeout does not protect against these bugs, and limiting pattern length does not reliably protect either.
Affected and fixed versions
| Branch | CVE-2026-97229 | CVE-2026-110264 | Latest patch (29 Oct) |
|---|---|---|---|
| 19.5 | not affected | fixed in 19.5.1 | 19.5.3 |
| 19.4 | fixed in 19.4.2 | fixed in 19.4.3 | 19.4.5 |
| 19.3 | fixed in 19.3.5 (19.3.4 listed as affected) | fixed in 19.3.6 | 19.3.7 |
| 19.2 | fixed in 19.2.8 | fixed in 19.2.9 | 19.2.10 |
| 19.1 | not affected | fixed in 19.1.9 | 19.1.10 |
| 19.0 | not affected | fixed in 19.0.10 | 19.0.11 |
| 18.11 | not affected | fixed in 18.11.13 | 18.11.14 |
| 7.0 to 18.10 | not affected | no fix, branch out of support | upgrade the branch |
| Ruby series | Fixes CVE-2026-97229 (#19020) | Fixes all four GitLab Onigmo CVEs |
|---|---|---|
| 4.0 | 4.0.8 | 4.0.9 |
| 3.4 | 3.4.12 | 3.4.12 |
| 3.3 | 3.3.13 | 3.3.13 |
Two traps sit underneath those tables, and Alpine walked into both.
The 19.3 fix is 19.3.5. GitLab’s release post notes that 19.3.4 had a bug on merge request pages and was superseded by 19.3.5, and some third-party trackers listed 19.3.4 as the fix. Alpine upgraded to 19.3.4 on 13 October by mistake. Follow the vendor: 19.3.4 is listed as affected.
A fixed GitLab version on your own Ruby is not a fixed GitLab. GitLab’s official Omnibus packages and Cloud Native images ship a patched Ruby. A source install, or any container image that installs GitLab on a distribution’s Ruby, gets the new GitLab code and none of the Ruby patches. Alpine’s 22 October upgrade to 19.3.6 left them exactly as vulnerable as before. Upstream Ruby did not ship the fix until 23 October, eleven days after GitLab’s release, so for that window there was no way to be safe on stock Ruby at all.
We went one step further and pulled the official gitlab/gitlab-ee images either side of the 19.3 boundary. The omnibus-gitlab 19.3.4+ee.0 tag, built on 7 October, already contains both Ruby regexp patches that 19.3.5 carries. In the images, libruby is byte-identical between 19.3.4 and 19.3.5, and the only Rails code that differs is two merge request view templates and one migration. So the official 19.3.4 EE package appears to carry the same fix for this CVE as 19.3.5, and GitLab’s “affected” listing for it is conservative. That does not change what you should do: GitLab’s advisory is the authority, and 19.3.4 is vulnerable to CVE-2026-110264 regardless. What it does change is the lesson from Alpine. Their wrong-version mistake was not what mattered. Their Ruby was.
How the exploit chain works
The shape of the attack, with every payload position redacted:
# Stage 1 - needs write access to any project (self-registration is enough)
push branch "x":
.gitlab-ci.yml -> a rules: clause carrying a regexp
[REDACTED: repeated character classes wrapped in a counted
repeat whose bound is split by an inline comment]
[REDACTED path] -> one ~255-byte filename ending in a non-ASCII character
# Stage 2 - unauthenticated when the project is public
GET /api/v4/projects/<id>/ci/lint?content_ref=x HTTP/1.1
Host: gitlab.example.com
The mechanism in prose. The lint endpoint evaluates the pushed configuration, which compiles the attacker’s pattern. GitLab’s guard sees a small repeat bound and a small ASCII program and lets it through. Matching that pattern against the non-ASCII filename forces Ruby to recompile it for UTF-8, where character classes expand into large Unicode range tables, and the size calculation for the repeat overflows. The worker then executes a program the attacker largely wrote. A failed attempt crashes the Puma worker and Workhorse returns a 502. A successful one, per Alpine, leaves no crash and no trace in the web logs.
We are deliberately not publishing a working exploit here. Rapid7 has published a full technical analysis and a proof of concept, vulhub ships a reproduction environment, and repeating either would add nothing a defender can use. The same line applies across this series, from php2shell to the JFrog Artifactory auth bypass, where the detection we shipped was passive while active exploits circulated.
Am I affected? Quick manual checks
The authoritative check is local, and it depends on how GitLab was installed.
On an Omnibus package or the official Docker image, the GitLab version is what matters:
sudo gitlab-rake gitlab:env:info | grep -E 'GitLab information|^Version'
cat /opt/gitlab/embedded/service/gitlab-rails/VERSION; echo
docker exec <container> cat /opt/gitlab/embedded/service/gitlab-rails/VERSION; echo
Do not use ruby -v here. GitLab patched its bundled Ruby without changing the version string. We checked the official EE images for 19.3.4, 19.3.5, 19.4.1 and 19.4.2: vulnerable and fixed alike print ruby 3.3.12.
On a source install or a self-built image, check both GitLab and the Ruby that Puma actually runs:
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production # GitLab + Ruby versions
readlink -f /proc/$(pgrep -f 'puma.*gitlab' | head -1)/exe # which ruby binary is live
docker run --rm --entrypoint ruby <your-image> -v # custom images
You need Ruby 3.3.13, 3.4.12 or 4.0.9 or later. If you use Alpine’s own alpinelinux/gitlab image, pull alpinelinux/gitlab:19.3.6 again: Alpine says the copy republished on 27 October contains Ruby 3.3.13 and every earlier pull is vulnerable. If your distribution backports Ruby fixes without moving the version number, check its security tracker rather than the upstream number.
The remote check is a static file. GitLab hides its version from anonymous visitors: the API returns 401, and the analytics context on the sign-in page has its instance_version field stripped. What it cannot hide is its compiled frontend. /assets/webpack/manifest.json is served to anyone, and the first few kilobytes carry a webpack build hash that is unique to each release:
curl -s -r 0-131071 -H 'Accept-Encoding: identity' \
https://gitlab.example.com/assets/webpack/manifest.json | grep -m1 -o '"hash": "[0-9a-f]*"'
Look the hash up in the public dictionary maintained by the gitlab-version-nse project, which hashes every official CE and EE image daily. We confirmed its entries against seven images we pulled ourselves, and against our live 19.4.1 lab. What the remote signal cannot tell you is which Ruby the server runs, so a “fixed” result on a self-built instance is not evidence of anything.
Nuclei detection template for CVE-2026-97229
There is no official Nuclei template for either CVE. The one community submission, a CVE-2026-110264 check, was closed on 30 October because ProjectDiscovery currently accepts version-detection templates only for KEV-listed CVEs. That submission also relied on the analytics instance_version field, which current GitLab releases strip for anonymous visitors, so it could not have seen any affected 19.x host. CVE-2026-97229 is now on KEV.
This template is passive. It sends one GET for a static file, reads at most 128 KB of it, and compares the build hash against every affected release. It sends no regular expression, no CI configuration and no lint request.
id: CVE-2026-97229
info:
name: GitLab CE/EE - CI/CD Regex Compiler Integer Overflow RCE (CVE-2026-97229)
author: xer0dayz
severity: critical
description: |
GitLab CE/EE 19.2 before 19.2.8, 19.3 before 19.3.5 and 19.4 before 19.4.2 compile
user-supplied CI/CD regular expressions with Ruby's Onigmo engine; an integer overflow in
the compiled-pattern size leads to code execution as the git user. CISA KEV 2026-10-29.
NON-DESTRUCTIVE: reads the anonymous static file /assets/webpack/manifest.json (first
128 KB), matches its webpack build hash against the official images of every affected
release, and names the release bucket. Sends no regex, CI config or lint request.
19.3.4 and 19.3.5 share one frontend and are reported as "ambiguous". A fixed GitLab
version on self-built Ruby is still vulnerable (needs Ruby 3.3.13 / 3.4.12 / 4.0.8+).
remediation: |
Upgrade to 19.2.8, 19.3.5 or 19.4.2 or later (19.3.4 is NOT fixed); prefer the latest
patch release. If GitLab runs on your own Ruby, upgrade Ruby to 3.3.13, 3.4.12 or 4.0.9+.
reference:
- https://docs.gitlab.com/releases/patches/patch-release-gitlab-19-4-2-released/
- https://nvd.nist.gov/vuln/detail/CVE-2026-97229
- https://www.ruby-lang.org/en/news/2026/10/23/regexp-fixes-for-gitlab-cves/
- https://alpinelinux.org/posts/GitLab-security-incident-CVE-2026-97229.html
classification:
cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
cvss-score: 10.0
cve-id: CVE-2026-97229
cwe-id: CWE-190
metadata:
verified: true
max-request: 1
vendor: gitlab
product: gitlab
shodan-query: http.title:"GitLab"
tags: cve,cve2026,gitlab,onigmo,ruby,rce,kev
http:
- method: GET
path:
- "{{BaseURL}}/assets/webpack/manifest.json"
max-size: 131072
matchers:
- type: word
name: gitlab-19.2.0-to-19.2.7
words:
- '"hash": "cd51060b5ebfc7fee399"' # ce 19.2.0, 19.2.1
- '"hash": "fed674581912bd0eee21"' # ee 19.2.0
- '"hash": "91126999cb25dd0eb2b9"' # ee 19.2.1
- '"hash": "37409ea97682789657a2"' # ce 19.2.2, 19.2.4, 19.2.5
- '"hash": "c2ed53b7a882a3d184f6"' # ee 19.2.2, 19.2.4
- '"hash": "584afc5604ada4ee17fe"' # ee 19.2.5
- '"hash": "2dcd267e8b92170166b3"' # ce 19.2.6
- '"hash": "a3b7a182238fc2a3eb83"' # ee 19.2.6
- '"hash": "7c44f93e873ad13682ea"' # ce 19.2.7
- '"hash": "941872c6e34f9c0b82c6"' # ee 19.2.7
- type: word
name: gitlab-19.3.0-to-19.3.3
words:
- '"hash": "cff2e319562e869b0325"' # ce 19.3.0, 19.3.1
- '"hash": "996f4e60a85fb0fa20b8"' # ee 19.3.0
- '"hash": "3353589a679bbde5febe"' # ee 19.3.1
- '"hash": "92a58542f35eda3a34fb"' # ce 19.3.2
- '"hash": "48e29423e5db4e10b53e"' # ee 19.3.2
- '"hash": "c1b0c33653568430d449"' # ce 19.3.3
- '"hash": "5675501e687da6dcc68d"' # ee 19.3.3
- type: word
name: gitlab-19.4.0-to-19.4.1
words:
- '"hash": "4a0604ce2f0fef861a8e"' # ce 19.4.0
- '"hash": "d54e81b4cb9a7e430a23"' # ee 19.4.0
- '"hash": "dc9422e12d538bec6265"' # ce 19.4.1
- '"hash": "137bd8a1bd825506f6aa"' # ee 19.4.1
# 19.3.4 (vulnerable) and 19.3.5 (fixed) ship the same webpack build.
- type: word
name: gitlab-19.3.4-or-19.3.5-ambiguous
words:
- '"hash": "7537b41ac85fb1ee2b2f"' # ce 19.3.4, 19.3.5
- '"hash": "7e0dd7dfa59b3f44401c"' # ee 19.3.4, 19.3.5
extractors:
- type: regex
name: webpack_hash
group: 1
regex:
- '"hash": "([0-9a-f]{20})"'
The downloadable file carries a longer description and the full reference list. A companion CVE-2026-110264.yaml uses the same technique across 18.7 to 19.5 and adds a second request that compares instance_version on older releases that still expose it.
Run it:
nuclei -t CVE-2026-97229.yaml -validate
nuclei -t CVE-2026-97229.yaml -t CVE-2026-110264.yaml -u https://gitlab.example.com
nuclei -t CVE-2026-97229.yaml -t CVE-2026-110264.yaml -l gitlab-hosts.txt -jsonl -o gitlab.jsonl
This is the actual output against our GitLab 19.4.1 EE lab:
[CVE-2026-97229:gitlab-19.4.0-to-19.4.1] [http] [critical] http://localhost:8929/assets/webpack/manifest.json ["137bd8a1bd825506f6aa"]
[CVE-2026-110264:gitlab-19.4.0-to-19.4.2] [http] [critical] http://localhost:8929/assets/webpack/manifest.json ["137bd8a1bd825506f6aa"]
Beyond the live instance, both templates and the bundled Python checker were run against a mock serving every 18.7-and-later fingerprint in the dictionary plus legacy version banners, 142 cases per CVE, and matched the vendor’s version ranges with zero disagreements.
Why match the build hash rather than a version string
Because there is no version string left to match. GitLab has spent several releases removing every anonymous version leak, and that is the right decision for GitLab. But a compiled frontend has to be served to anonymous users or the sign-in page does not render, and webpack stamps every build with a hash. It is the one fingerprint GitLab cannot remove without changing how its frontend is delivered. Matching exact hash strings with word matchers also avoids a trap we have hit before, where a regex extractor that matches twice binds a list and compare_versions silently returns false.
Three blind spots, stated plainly
Self-built Ruby makes a clean result meaningless. The template identifies the GitLab release. It cannot see Ruby. A source install or a custom image on 19.4.5 reads as fixed while running exactly the Ruby Alpine was running. If you know a host is not on GitLab’s official packages, the remote check only tells you which GitLab to expect when you log in and run ruby -v.
Some boundaries cannot be split remotely. Releases that changed nothing in the frontend share a hash. For CVE-2026-97229 that is only 19.3.4 and 19.3.5, reported as gitlab-19.3.4-or-19.3.5-ambiguous. A host in that bucket is still a definite CVE-2026-110264 hit. For CVE-2026-110264 there are four ambiguous buckets: 18.11.12-14, 19.0.9-11, 19.1.8-10 and 19.2.8-9.
Silence is “unknown”, not “clean”. A release newer than the dictionary, a custom-built frontend, or a reverse proxy that does not pass /assets/ all produce no match. The checker reports those hosts as UNKNOWN rather than passing them.
Detecting CVE-2026-97229 across your attack surface with Sn1per
The template is the easy half. The hard half is the host list. Self-managed GitLab accumulates: the instance a team stood up for a client project, the old one kept read-only after a migration, the staging copy a contractor built from source on a VM, the git. subdomain from an acquisition. Those are the instances that stay on 19.3.4, or that got upgraded on stock Ruby by someone following the release post to the letter. None of them are in your CMDB.
So treat it as a discovery problem first.
# Discover first: subdomains, certificate transparency, ports, services
sniper -t example.com -m recon -w gitlab-onigmo
# Web pass, which fires both templates on every HTTP and HTTPS port found
sniper -t gitlab.example.com -m web -w gitlab-onigmo
# Sweep a known fleet
sniper -f gitlab-hosts.txt -m webscan -w gitlab-onigmo
Drop both templates into /sniper/templates/nuclei/server/ and they run with no configuration change. Findings land in the workspace as critical, with the release bucket in the matcher name. Then run the bundled checker over the same hosts. It names the release and adds the two exposure flags that made Alpine reachable, open self-registration and anonymously listed public projects:
./cve-2026-97229-detect.py -f gitlab-hosts.txt
Pull findings out of a Professional or Enterprise install over the API:
curl -sk -H "X-API-Key: $SN1PER_API_KEY" \
"https://sn1per.local/pro/api.php?action=vulnerabilities&workspace=gitlab-onigmo" \
| jq -r '.[] | select(.name | test("97229|110264")) | [.host, .severity, .details] | @tsv'
One note on where this runs. The output is a list of your source code servers, the release each one runs, and whether a stranger can register on it. That is a map of your software supply chain with a public exploit attached, and it is not something to generate inside someone else’s multi-tenant cloud. Sn1per runs on your hardware and the findings stay there. The longer version of that argument is in our guide to external attack surface management with Sn1per.
Which Sn1per edition fits
| Capability | Community | Professional 2026 | Enterprise |
|---|---|---|---|
| Run the GitLab templates | Yes | Yes | Yes |
| Curated Nuclei set maintained for you | Partial | Yes | Yes |
| Workspaces and scan history | Basic | Yes | Yes |
| Web UI with asset risk ranking | No | Yes | Yes |
| JSON API for finding export | No | Yes | Yes |
| Scheduled recurring surface sweeps | No | Limited | Yes |
| Multi-user, delegated workspaces | No | No | Yes |
For a one-off answer, Community plus the templates is enough. The recurring question matters more here, because this is the fourth Onigmo CVE in GitLab in under a month, and the same engine sits underneath every Ruby application you run. That is the case for continuous attack surface testing rather than a single sweep.
Remediation and mitigation
- Upgrade GitLab to the latest patch release for your branch. As of 29 October that is 19.5.3, 19.4.5, 19.3.7, 19.2.10, 19.1.10, 19.0.11 or 18.11.14. The minimum for CVE-2026-97229 alone is 19.2.8, 19.3.5 or 19.4.2, but stopping there leaves CVE-2026-110264 open. Releases before 18.11 have no fix and need a branch upgrade.
- If GitLab runs on your own Ruby, upgrade Ruby as well. 3.3.13, 3.4.12 or 4.0.9, or later. Then restart Puma and Sidekiq so no worker keeps the old
librubymapped. If you build your own images, add the Ruby version to your release checklist. Alpine now checks every GitLab security release for Ruby or component patches its own build has to apply. Copy that. - Close open self-registration. Under Admin > Settings > General > Sign-up restrictions, turn sign-up off or require administrator approval. Every observed attack path so far starts with write access to a project, and open sign-up hands that to anyone.
- Restrict public projects if you do not need them. Rapid7 notes that restricting anonymous access to public projects, and stopping untrusted users creating or writing to them, reduces exposure to the demonstrated path. Restricted visibility levels in the admin settings do this instance-wide.
- Do the forensic triage CISA asks for, before you rebuild. Preserve Workhorse, Puma and Rails JSON logs first. Alpine lost some of theirs to rotation and container replacement, and could not prove the negative as a result.
- If you cannot rule out exploitation, rotate everything the
gituser could read. Alpine’s list of what could have been exposed is the right checklist: password hashes, 2FA secrets, CI/CD variables, runner registration tokens, webhook, integration and push-mirror credentials, deploy keys, and GitLab’s own secrets file. Patching does not revoke a token that has already been read. Alpine replaced GitLab’s secret keys and reset every password. - Check your other Ruby applications. Ruby’s advisory applies to anything that builds a regular expression from untrusted input: search features, filter rules, user-defined validation. Upgrade Ruby across the estate, not only on GitLab.
If you were exposed: hunting for exploitation
GitLab has published seven detection rules for this family: six listed against CVE-2026-97229 and two against CVE-2026-110264, with the native-crash rule shared. The useful thing about them is that they say which log actually sees what, because most of the obvious places do not.
The detonation is visible in Workhorse, not Rails. A worker that crashes mid-compile never writes its Rails log line. Look for a 502 with badgateway: failed to receive response: EOF on the CI lint or GraphQL endpoints, typically after a request lasting several seconds:
grep -E 'ci/lint|/api/graphql' /var/log/gitlab/gitlab-workhorse/current \
| grep -E '"status":502|badgateway: failed to receive response: EOF'
Crashed workers leave a Ruby [BUG] report. Look in the Puma stderr log for a crash whose backtrace includes Onigmo frames (onig_compile, compile_tree, compile_length_tree, match_at) together with regexp_matcher.rb for the CI path or org-ruby for the Org-mode path:
grep -B2 -A40 '\[BUG\]' /var/log/gitlab/puma/puma_stderr.log | grep -E 'onig_|compile_tree|regexp_matcher|org-ruby'
The only place the hostile pattern is logged verbatim is application_json.log, when GitLab’s guard rejects one (regexp rejected at compile time, with the pattern in extra.regexp), and graphql_json.log, which records ciLint variables in full. A counted repeat with a (?# comment inside its braces has no benign use.
Two cheap correlators. A pushed path with a final segment around 255 bytes containing a non-ASCII byte, and a burst of ci/lint 5xx responses from python-requests or curl user agents. Benign CI tooling does not produce either.
Two caveats, both from Alpine’s experience. A successful exploit leaves no crash and nothing in the web logs, so these rules catch failed attempts, which are useful precisely because attackers usually fail a few times first. And without outbound connection logging on the GitLab host you will not see the one thing that proves success. Rapid7 adds two host-side signs worth alerting on regardless: a shell spawned by a Puma worker, and a sudden jump of around 2 GiB in a worker’s memory.
Frequently asked questions
What is CVE-2026-97229?
It is a CVSS 10.0 remote code execution vulnerability in GitLab CE/EE 19.2 to 19.4, rated unauthenticated by GitLab. A crafted regular expression in CI/CD configuration triggers an integer overflow in Onigmo, Ruby’s regex engine, when GitLab compiles it, leading to code execution as the git user. It bypasses the incomplete fix for CVE-2026-93577 shipped in GitLab 19.4.1. It was reported by Johan Carlsson through GitLab’s bug bounty program.
Which GitLab versions are affected by CVE-2026-97229?
19.2 before 19.2.8, 19.3 before 19.3.5, and 19.4 before 19.4.2, both CE and EE. 19.3.4 is listed as affected; the 19.3 fix is 19.3.5. The companion CVE-2026-110264 affects far more, every release from 7.0 up to 18.11.13, 19.0.10, 19.1.9, 19.2.9, 19.3.6, 19.4.3 and 19.5.1, so upgrade to the latest patch release for your branch rather than the minimum.
Is CVE-2026-97229 being exploited in the wild?
Yes. CISA added it to the KEV catalog on 29 October 2026 with a 1 November due date and a forensic triage requirement. Alpine Linux disclosed an attempted exploitation of its GitLab on 20-21 October, by an attacker using a self-registered account, that crashed workers eight times and came close to working. Alpine found no evidence of code execution, could not rule it out, and rebuilt the instance. A public proof of concept is available.
I upgraded GitLab. Why could I still be vulnerable to CVE-2026-97229?
Because the fix is in Ruby, not in GitLab’s code. GitLab patches the Ruby it ships in its official Omnibus packages and Cloud Native images. If you installed from source or built your own container image on a distribution’s Ruby, upgrading GitLab changes nothing, and you also need Ruby 3.3.13, 3.4.12 or 4.0.9 or later. This is exactly what happened to Alpine, whose 19.3.6 upgrade left them vulnerable. On official packages, do not use ruby -v to check: it reports 3.3.12 before and after the fix.
Is the CVE-2026-97229 Nuclei template safe to run in production?
Yes. It sends one anonymous GET for a static JavaScript build manifest and reads at most 128 KB of it. It sends no regular expression, no CI configuration and no lint request, so it cannot crash a worker the way an exploitation attempt does. Treat a match as a strong lead and confirm on the host, and treat silence as unknown, particularly on self-built instances where the template cannot see Ruby.
Closing
The lesson from CVE-2026-97229 is not about regular expressions. It is that “patched” depends on how something was built. GitLab shipped a correct fix on 12 October, and it reached only the people running GitLab’s own packages. Everyone else got a version bump and a vulnerable runtime, and the upstream runtime fix took another eleven days. Alpine is a security-focused distribution with competent infrastructure people, and they upgraded twice before they found out.
Every self-built platform on your estate has this shape: an application version you track, sitting on a runtime you assume. The inventory worth building is the one that records both, for every instance, including the ones nobody remembers standing up.
Grab the templates, run them across every HTTP and HTTPS port you own rather than only the GitLab you know about, close open sign-up while you are there, and for anything you did not install from GitLab’s own packages, log in and check the Ruby.