On 23 October 2026 Ruby released 4.0.9, 3.4.12 and 3.3.13 to fix memory corruption bugs in Onigmo, its built in regular expression engine. GitLab turned these bugs into four critical remote code execution CVEs. Any Ruby application that compiles a regex from attacker controlled text is exposed. Upgrade Ruby, then audit every Regexp.new call.
GitLab has already patched its own product by shipping a patched copy of Ruby, and we covered the GitLab side in our post on CVE-2026-110264. This post is about everything else. The bugs live in the Ruby interpreter, not in GitLab code, so a Rails application, a Sinatra service, a background worker or a command line tool built on Ruby can carry the same flaw if it lets users supply patterns.
There is a second problem that makes this harder to manage than an ordinary runtime patch. Ruby has not requested its own CVE identifiers for these fixes, "at least for now", in its own words. The four CVE records that do exist name GitLab as the only affected product. A scanner that matches CVE records against the Ruby versions in your images has nothing to match, so it will report your Ruby 3.4.11 containers as clean. This post explains the bugs in plain terms, gives the exact fixed versions and release hashes, and walks through finding every Ruby runtime and every risky regex call in your estate.
What happened
Ruby's advisory, posted by k0kubun at 03:11:22 UTC on 23 October, explains the chain. GitLab compiles regular expressions supplied by its users, such as patterns in CI configuration, with Regexp.new. GitLab found that specially crafted patterns could corrupt memory while Ruby compiled them, and that this could be turned into remote code execution on GitLab servers. GitLab fixed each issue in its own releases by patching the Ruby it ships, and assigned four CVEs between 23 September and 21 October. Ruby then shipped the same fixes for everyone else.
Two of the three underlying fixes had been sitting in Ruby's main branch for months before the CVEs appeared. The double free fix was merged in May 2025 and first shipped in Ruby 4.0.0. The repeat ID fix was merged in April 2026 but had not been backported to any stable release until 23 October. Only the program size fix was written after GitLab's first advisory, in September 2026, and Ruby credits that bug to a finding made through GitLab's bug bounty. That gap between "fixed in the main branch" and "fixed in the release you run" is the most useful lesson in this story, and we come back to it below.
Timeline
All times UTC. Where a source gives only a date, the time column says so.
| Date and time (UTC) | Event | Source |
|---|---|---|
| 12 Apr 2022 (date only) | Ruby publishes CVE-2022-28738, a double free in Regexp compilation, and says it treats the issue as a vulnerability despite untrusted patterns being generally unsafe | ruby-lang.org |
| 23 Apr 2024 (date only) | Ruby publishes CVE-2024-27282, an arbitrary heap read when attacker supplied data reaches the regex compiler | ruby-lang.org |
| 14 May 2025, 05:44:53 | Pull request #13332, "Prevent double free for too big repetition quantifiers", is opened by makenowjust | GitHub ruby/ruby |
| 16 May 2025, 01:14:26 | #13332 is merged to master as commit 35000ac2ed3a1829f8d193ffb23da0e44cc6fe5f | GitHub ruby/ruby |
| 25 Dec 2025 (date only) | Ruby 4.0 is released, and its first release, 4.0.0, already contains #13332 | ruby-lang.org branches page, Ruby advisory |
| 1 Apr 2026 (date only) | Ruby 3.2 reaches end of life; Ruby 3.3 leaves normal maintenance and enters security maintenance | ruby-lang.org branches page |
| 28 Apr 2026, 08:36:27 | Pull request #16801, "Reject overflowed repeat/null-check IDs in huge regexps", is opened by makenowjust | GitHub ruby/ruby |
| 29 Apr 2026, 02:09:25 | #16801 is merged to master as commit 493d26dcdea8b2f0ac5d28e057b401bc809e7fae | GitHub ruby/ruby |
| 23 Sep 2026 (date only) | GitLab releases 19.4.1, 19.3.3 and 19.2.7, fixing CVE-2026-89078 and CVE-2026-93577 (both CVSS 9.9, authenticated) | GitLab release notes |
| 23 Sep 2026, 23:04:45 and 23:05:20 | CVE records for CVE-2026-93577 and CVE-2026-89078 are published by GitLab as CNA | CVE.org |
| 25 Sep 2026, 03:18:49 | Pull request #19020, "Avoid overflow in the regexp compiler's program size arithmetic", is opened by hsbt | GitHub ruby/ruby |
| 25 Sep 2026, 04:29:17 | #19020 is merged to master as commit cf35145fc46bc278e0ab0003478fc4afe6564819 | GitHub ruby/ruby |
| 12 Oct 2026 (date only) | GitLab releases 19.4.2, 19.3.5 and 19.2.8, fixing CVE-2026-97229 (CVSS 10.0, unauthenticated); bug fix list includes "Add Ruby regexp program size patch for 3.3 and 3.4" | GitLab release notes |
| 13 Oct 2026, 01:05:46 | CVE-2026-97229 record is published | CVE.org |
| 20 Oct 2026 (date only) | GitLab's "Onigmo native worker crash" Sigma rule is dated | gitlab-security-oss detections |
| 21 Oct 2026 (date only) | GitLab releases 19.5.1, 19.4.3, 19.3.6, 19.2.9, 19.1.9, 19.0.10 and 18.11.13, fixing CVE-2026-110264 (CVSS 10.0, unauthenticated, all versions from 7.0); bug fix list includes "Add Ruby regexp repeat ID overflow patch for 3.3, 3.4 and 4.0" | GitLab release notes |
| 22 Oct 2026, 00:33:48 | CVE-2026-110264 record is published | CVE.org |
| 22 Oct 2026 (date only) | Ruby 4.0.8 ships as "a routine update that includes bugfixes"; Ruby's later table shows it is the first 4.0 release with #19020 | ruby-lang.org |
| 23 Oct 2026, 03:11:22 | Ruby publishes "Regexp compilation fixes related to GitLab's CVE-2026-89078, CVE-2026-93577, CVE-2026-97229 and CVE-2026-110264" and releases 4.0.9, 3.4.12 and 3.3.13 | ruby-lang.org |
| 23 Oct 2026, 04:04:17 | GitHub release for v4.0.9 is published, listing #16801 as its change | GitHub ruby/ruby releases |
| 23 Oct 2026, 17:00 to 17:19 | Docker Hub shows the official ruby image tags 4.0.9, 3.4.12 and 3.3.13 (and variants such as 4.0.9-slim and 3.4.12-alpine) as last updated in this window | Docker Hub tag API |
| 23 Oct 2026, 17:00:09 | CISA's KEV catalog version 2026.10.23 is released; none of the four CVEs is listed | CISA KEV JSON feed |
Who is affected
Ruby's advisory draws the line clearly. The bugs are reached only when an application compiles a regular expression from a string that an attacker controls. Applications that use only regular expressions written by their own developers are not affected.
| Code pattern | Exposed? | Why |
|---|---|---|
Regexp.new(params[:pattern]) | Yes | The user writes the whole pattern |
Regexp.compile(user_input) | Yes | Alias for Regexp.new |
Regexp.union(list_from_request) with strings | Depends | Regexp.union escapes plain strings, but Ruby's advisory lists it as a sink, so treat any attacker reachable call as in scope until you have checked what reaches it |
/#{input}/ or %r{#{input}} | Yes | Interpolation builds a new pattern from the input at runtime |
/#{Regexp.escape(input)}/ | Lower risk | The input is escaped into a literal match, so it cannot add quantifiers or groups; still verify the call really escapes everything that reaches it |
str.match?(/\A[a-z]+\z/) with a user supplied str | No | The pattern is fixed; only the subject string comes from the user |
| A gem that builds a regex from file contents, configuration or markup it parses for you | Yes, if an attacker controls that content | This is how GitLab's unauthenticated path worked, as explained below |
Typical places where user supplied patterns show up in Ruby applications:
- Search features with a "use regular expression" toggle, log viewers and admin filters.
- Routing rules, allowlists and denylists that tenants configure in a multi tenant SaaS.
- CI, automation and workflow products that accept rules from configuration files, as GitLab does.
- Markup and document renderers that turn keywords from a document into patterns.
- Chat bots, alerting rules and "if the message matches" automations.
- Internal tools that started as trusted and later became reachable from a shared network.
Which Ruby versions are affected:
| Ruby series | Status (ruby-lang.org) | Fully fixed release | Notes |
|---|---|---|---|
| 4.0 | Normal maintenance | 4.0.9 | 4.0.0 already had the double free fix; 4.0.8 added the program size fix; 4.0.9 added the repeat ID fix |
| 3.4 | Normal maintenance | 3.4.12 | First 3.4 release with any of the three fixes |
| 3.3 | Security maintenance, EOL expected 31 Mar 2027 | 3.3.13 | Included "as an exception" because GitLab assigned CVEs; also updates the resolv default gem to 0.3.2 for CVE-2026-80212 and CVE-2026-80213 |
| 3.2 and older | End of life since 1 Apr 2026 | None | No fix will ship; move to 3.4 or 4.0 |
Outside your own code, think about Ruby you did not install yourself. Vendor products often ship an embedded interpreter under their own directory. GitLab Omnibus is the best known example, with its Ruby under /opt/gitlab/embedded, and GitLab has patched that copy. Configuration management agents, log shippers and appliances built on Ruby may carry their own interpreter too, and they are only fixed when that vendor ships an update.
How it works, in plain terms
A regex is a small program
When Ruby sees Regexp.new("a+b"), it does not keep the text around and interpret it character by character. Onigmo, the engine built into Ruby, parses the pattern into a tree, then compiles the tree into a compact program of opcodes for a small virtual machine. Matching a string means running that program. Like any compiler, Onigmo has to calculate sizes, offsets and internal counters as it builds the program, and like any C code it has to get every one of those calculations right.
Most of the time nobody worries about this because the patterns are written by the application's own developers. Ruby's documentation discusses untrusted patterns only as a denial of service concern: a hostile pattern might run slowly, so set a timeout. The GitLab findings show that, for these bugs, a hostile pattern can do much more than run slowly. It can make the compiler produce a program that reads or writes outside its own memory.
The three bugs
Ruby's advisory maps each GitLab CVE to one fix. Described without any attack detail:
- A double free on a parse error (#13332, CVE-2026-89078). When parsing a pattern failed in a particular way after an alternation, Onigmo freed the same piece of memory twice, once during parsing and once during compilation. Freeing memory twice can corrupt the allocator's bookkeeping, which is a classic route to code execution. The fix makes sure the node is freed once.
- An integer overflow in program size (#19020, CVE-2026-93577 and CVE-2026-97229). Large bounded repeats make the compiler multiply sizes together. The pull request explains that the product could overflow a C
int, so a size check passed for a repeat far larger than the limit, and that concatenated repeats could push the compiled program pastINT_MAXand wrap the jump offsets written into it. GitLab's detection write-up describes the result as a program larger than 2 GiB with a signed 32 bit jump offset wrapped negative. The fix compares sizes by division so the check cannot overflow, and caps the real program size as it is emitted. - An overflow of internal IDs (#16801, CVE-2026-110264). Onigmo gives each repeat in a pattern an internal ID. GitLab's detection write-up says a pattern with more than 32,767 generic repeats pushes that ID past the signed 16 bit limit, which causes an out of bounds write at match time. The fix adds explicit errors when repeat or null check IDs would overflow.
In each case the bug turns a regex into a way to make the regex virtual machine work outside its buffer. GitLab's write-up says that, with preparation, this was turned into a system() call running as the git user on GitLab servers.
How untrusted text reached the compiler in GitLab
GitLab's detection rule describes two paths. The CI path takes a pattern from a rules: entry with a regexp clause in a CI configuration and compiles it in regexp_matcher.rb. The Org mode path is more instructive for everyone else. GitLab renders .org files with the org-ruby gem, and according to GitLab's write-up the gem interpolates a raw, attacker supplied #+TODO: keyword from the document into Regexp.new. That is why CVE-2026-110264 needed no account: anyone who could get a crafted .org file rendered could reach Onigmo.
The point for other Ruby teams is that the risky call does not have to be in your code. A third party gem that builds a pattern from the content it parses can create the same sink on your behalf. RubyGems lists org-ruby 0.10.0 and 0.10.1, published on 9 October 2026, as the first releases since 0.9.12 in December 2014. We have not confirmed whether those releases change how keywords reach Regexp.new, so treat the Ruby upgrade as the fix regardless.
Why timeouts and length limits do not help
Two mitigations that Ruby developers reach for first do not work here, and Ruby's advisory says so directly:
Regexp.timeoutdoes not protect against these bugs. The timeout limits how long a match runs. The memory corruption happens while the pattern is compiled, or, for the repeat ID bug, in a way a timeout does not catch.- Limiting pattern length does not reliably protect either. Ruby says some of the bugs can be triggered by short patterns. A single short repeat can expand into a very large compiled program.
The only workaround Ruby offers if you cannot upgrade is to stop compiling regular expressions from untrusted input.
Technical details
Every value below comes from a source we fetched on 23 October 2026. Ruby has not published CVSS scores of its own, because it has not assigned CVEs; the scores are GitLab's.
Identifiers and scores
| CVE | GitLab title | Auth needed | CVSS 3.1 (GitLab) | CWE | CVE record published (UTC) |
|---|---|---|---|---|---|
| CVE-2026-89078 | Double Free issue in Regular Expression Parser | Yes | 9.9, CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:L | CWE-415 | 23 Sep 2026, 23:05:20 |
| CVE-2026-93577 | Integer Overflow issue in Regular Expression Compiler | Yes | 9.9, CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H | CWE-190 | 23 Sep 2026, 23:04:45 |
| CVE-2026-97229 | Integer Overflow issue in CI/CD Regular Expression Compiler | No | 10.0, CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H | CWE-190 | 13 Oct 2026, 01:05:46 |
| CVE-2026-110264 | Integer Overflow issue in Regular Expression Compiler | No | 10.0, CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H | CWE-190 | 22 Oct 2026, 00:33:48 |
The CISA ADP enrichment on all four records rates exploitation as "none" and technical impact as "total". It rates CVE-2026-89078 and CVE-2026-93577 as not automatable and CVE-2026-97229 and CVE-2026-110264 as automatable. None of the four appeared in CISA's KEV catalog version 2026.10.23. GitLab credits joaxcar (Johan Carlsson) for all four reports through its HackerOne program, and Ruby credits him for the integer overflow fixed by #19020. GitLab's references list HackerOne reports 4019059, 3995696, 4055749, 4058589 and 4093475 and GitLab work items 628577, 629758, 630686 and 633633.
What the CVE records actually say is affected
This is the root of the scanning gap. Each CVE.org record lists exactly one affected product, vendor GitLab, product GitLab. NVD's analysed configurations for CVE-2026-89078, CVE-2026-93577 and CVE-2026-97229 contain only cpe:2.3:a:gitlab:gitlab entries for the community and enterprise editions. CVE-2026-110264 was still "Undergoing Analysis" at NVD with no configurations when we checked. No record names Ruby, Onigmo or org-ruby.
Ruby fix mapping
| GitLab CVE | Bug in Ruby (Ruby's wording) | Ruby change and merge commit | First fixed 4.0 | First fixed 3.4 | First fixed 3.3 |
|---|---|---|---|---|---|
| CVE-2026-89078 | Double free when parsing a pattern fails after an alternation | #13332, 35000ac2ed3a1829f8d193ffb23da0e44cc6fe5f | 4.0.0 | 3.4.12 | 3.3.13 |
| CVE-2026-93577, CVE-2026-97229 | Integer overflow in the size of a compiled pattern | #19020, cf35145fc46bc278e0ab0003478fc4afe6564819 | 4.0.8 | 3.4.12 | 3.3.13 |
| CVE-2026-110264 | Overflow of internal IDs in a pattern with many repeats | #16801, 493d26dcdea8b2f0ac5d28e057b401bc809e7fae | 4.0.9 | 3.4.12 | 3.3.13 |
The pull request sizes are small: #13332 changed 2 files (16 additions, 7 deletions), #19020 changed 5 files (34 additions, 6 deletions) and #16801 changed 4 files (20 additions, no deletions). Small diffs are what make a backport or a private patch realistic for teams that cannot change Ruby versions quickly, which is exactly what GitLab did.
GitLab fixed versions
| CVE | Affected GitLab CE/EE (per GitLab) | Fixed in |
|---|---|---|
| CVE-2026-89078, CVE-2026-93577 | 19.2 before 19.2.7, 19.3 before 19.3.3, 19.4 before 19.4.1 | 19.4.1, 19.3.3, 19.2.7 (23 Sep) |
| CVE-2026-97229 | 19.2 before 19.2.8, 19.3 before 19.3.5, 19.4 before 19.4.2 | 19.4.2, 19.3.5, 19.2.8 (12 Oct) |
| CVE-2026-110264 | 7.0 before 18.11.13, 19.0 before 19.0.10, 19.1 before 19.1.9, 19.2 before 19.2.9, 19.3 before 19.3.6, 19.4 before 19.4.3, 19.5 before 19.5.1 | 19.5.1, 19.4.3, 19.3.6, 19.2.9, 19.1.9, 19.0.10, 18.11.13 (21 Oct) |
GitLab says GitLab.com and GitLab Dedicated are already patched. Self managed instances need the release for their branch; see our GitLab post for upgrade paths and what to rotate.
Release tarballs and checksums
Ruby publishes size and hashes for each source package. If you build Ruby from source, or you mirror tarballs internally, verify against these SHA256 values from ruby-lang.org:
| File | Size (bytes) | SHA256 |
|---|---|---|
ruby-4.0.9.tar.gz | 23946524 | c35350682ff050b79702d57ade4516e7336063f510155d19fb2fb5b3f3909511 |
ruby-4.0.9.tar.xz | 17996144 | a8057adb39f51ad6dc3106237504c93dd792bb515837b4fe7817ae21ed7e51f4 |
ruby-4.0.9.zip | 29263242 | 8242fbb2354cb71811cb2ff4d5e149b7a0ecb700e6c61a897710204ac36f866b |
ruby-3.4.12.tar.gz | 22498017 | 5e5d85ad93b0adcccbad47733680f4892bd0078e3342437f7babce01c1872479 |
ruby-3.4.12.tar.xz | 16732860 | db6c147940d7ad0b706823d42c8e6713c36dec70a1f83f635f1d3a7f0599ec90 |
ruby-3.4.12.zip | 27719665 | b1bec4f3a2b0c47f349a102b013f1c8f345b9991388e75368d1178ac4f9cfc0e |
ruby-3.3.13.tar.gz | 21497008 | 4cae72e971bd1cdb20ca87d299111182417c5276c162de8ead0ab0531426cd61 |
ruby-3.3.13.tar.xz | 15878972 | 6bd764359992373a96556b45fb7dfe6b6170da34a0a2fd0b7e1be7932fea982b |
ruby-3.3.13.zip | 26961541 | 8cf1c468af6dbcaa2c48270cc4c951325dabe5fd84b609641d4bd7b89c9d5766 |
Ruby also lists SHA1 and SHA512 values on each release page. For reference, ruby-4.0.8.tar.gz (22 October) has SHA256 0968e2ac4cbd6f1fd85d3092aed66cd3c2d8ba4a5e2493d6e6c6cb0c431897d2; it contains two of the three fixes but not #16801.
Crash signatures from GitLab's detection rule
GitLab published a Sigma rule, "GitLab org-ruby or Onigmo native worker crash", ID 58461d1f-2ad8-41a8-861e-57c8b697657f, status experimental, level critical. It fires when a native Ruby [BUG] crash report also contains an org-ruby file or an Onigmo frame. The strings it matches are useful for any Ruby service, not only GitLab:
| Crash shape | Ruby frame | C frames named by GitLab |
|---|---|---|
| Compile time (oversized program) | GitLab's regexp_matcher.rb in the CI path | onig_compile, compile_tree, compile_length_tree |
| Match time (out of bounds write) | org-ruby headline.rb (parse_keywords, sub!) | match_at, onig_search_gpos, rb_reg_search_set_match, rb_str_sub_bang |
GitLab is explicit that this is a corroborating signal: a successful exploit "is quiet and does not reliably crash", so the absence of a crash never clears an instance. It also notes that architecture, signal name and addresses vary (its lab saw a Bus Error on arm64-darwin, production typically shows a Segmentation fault on x86_64-linux), so match on the Onigmo frame names rather than on those.
GitLab has not published network indicators such as IP addresses or domains for these CVEs, and we found no file hashes for malicious artifacts.
Installers and images
The official ruby images on Docker Hub had 4.0.9, 3.4.12 and 3.3.13 tags by about 17:00 to 17:19 UTC on 23 October, including slim and Alpine variants we checked. The rbenv ruby-build project had definitions for all three versions on its main branch when we checked, which also serves asdf and other tools built on it. Distribution packages follow their own schedules and often backport fixes without changing the upstream version number, so check your distribution's changelog rather than the version string alone. Because there is no Ruby CVE, distribution security trackers may not have an entry to search for.
Why your scanner will not flag this
Software composition analysis, container scanners and SBOM tools mostly work the same way. They identify components and versions, then look those up in vulnerability records that say which product and which versions are affected. That chain breaks here in three places:
- No CVE names Ruby. The only CVEs are GitLab's, and their affected product is GitLab. A lookup for
ruby 3.4.11returns nothing new. - Ruby told users to borrow GitLab's IDs. The advisory says to "refer to GitLab's CVE identifiers" when tracking these issues, for example in a security scanner. That is a sensible stopgap, but it only works if someone adds those IDs to the Ruby component by hand.
- Many images build Ruby from source. The official Docker images, and many internal base images, compile Ruby into
/usr/localrather than installing an operating system package. Whether a scanner records that interpreter as a component at all depends on the tool and how the image was built.
None of this is anyone's fault. Ruby has a long standing position that compiling untrusted patterns is unsafe, while its own documentation describes only the denial of service angle. Ruby says it will discuss how to document this, whether to treat this class of bug as a vulnerability, and whether to request CVE identifiers, and will update its advisory if it does. Until then the honest status is "a security relevant runtime update with no CVE of its own", and the only reliable way to track it is to do so deliberately.
What to do now
The order matters. Upgrading the runtime closes all three bugs at once for every code path, including ones hidden in gems. The code audit tells you how urgent the upgrade is for each service and what to fix if a service cannot move yet.
Step 1. Find every Ruby runtime
Start with what is actually running, not with what your repositories say.
On a host or inside a running container:
ruby -e 'puts RUBY_DESCRIPTION'
which -a ruby
# Embedded interpreters shipped by vendors
find / -xdev -type f -name ruby -perm -u+x 2>/dev/null
For container images, check each image you deploy:
docker run --rm --entrypoint ruby your-registry/app:tag -e 'puts RUBY_VERSION'
For a Kubernetes cluster, list the images in use and feed them to the command above:
kubectl get pods -A -o jsonpath='{range .items[*]}{range .spec.containers[*]}{.image}{"\n"}{end}{end}' | sort -u
In repositories, look at every place a Ruby version is pinned:
grep -rnE '^FROM .*ruby' --include='Dockerfile*' .
cat .ruby-version .tool-versions 2>/dev/null
grep -n '^ruby' Gemfile
grep -n -A1 'RUBY VERSION' Gemfile.lock
grep -rnE 'ruby-version|ruby_version' .github .gitlab-ci.yml 2>/dev/null
On package based systems, check the installed package and its changelog:
dpkg -l | grep -E '^ii +ruby' # Debian, Ubuntu
rpm -qa | grep -E '^ruby' # RHEL, Fedora, SUSE
apk info -v 2>/dev/null | grep -E '^ruby' # Alpine
Step 2. Classify each runtime
A small Ruby check you can run anywhere Ruby is installed tells you whether that interpreter carries all three fixes by version number:
fixed = { "4.0" => "4.0.9", "3.4" => "3.4.12", "3.3" => "3.3.13" }
series = RUBY_VERSION.split(".")[0, 2].join(".")
current = Gem::Version.new(RUBY_VERSION)
if fixed.key?(series)
ok = current >= Gem::Version.new(fixed[series])
puts "#{RUBY_VERSION}: #{ok ? 'has the Onigmo fixes' : 'upgrade to ' + fixed[series]}"
else
puts "#{RUBY_VERSION}: series has no fix, move to 3.4 or 4.0"
end
Save it as onigmo_check.rb and run ruby onigmo_check.rb. A distribution package with a backported patch will report an older version number, so for those, confirm with the distribution changelog instead.
Step 3. Upgrade
Pick the method that matches how each runtime was installed.
Docker images. Bump the base image and rebuild, rather than patching running containers:
FROM ruby:3.4.12-slim
Pin by digest once you have pulled the new tag, so a later rebuild cannot silently fall back.
rbenv or asdf. Update the ruby-build definitions first, then install:
git -C "$(rbenv root)/plugins/ruby-build" pull # if installed as a plugin
rbenv install 3.4.12
asdf install ruby 3.4.12
Building from source. Download the tarball and check it against the SHA256 table above before building:
curl -fsSLO https://cache.ruby-lang.org/pub/ruby/3.4/ruby-3.4.12.tar.xz
echo "db6c147940d7ad0b706823d42c8e6713c36dec70a1f83f635f1d3a7f0599ec90 ruby-3.4.12.tar.xz" | shasum -a 256 -c
Bundler projects. Update the pins together so CI, local development and production agree:
echo "3.4.12" > .ruby-version
# Gemfile: ruby "3.4.12" (or "~> 3.4.12" if you allow patch updates)
bundle update --ruby
bundle exec rake test
Ruby 3.2 or older. There is no fixed release. Plan the move to 3.4 or 4.0, and until then apply the code level controls in Step 6 to every service on that runtime.
Vendor products with an embedded Ruby. Upgrade the product. For GitLab that means the release for your branch listed above, which ships a patched interpreter. For anything else, ask the vendor whether its embedded Ruby carries the fixes from #13332, #19020 and #16801.
Step 4. Verify what is running
After the rollout, re-run Step 1 against the live environment, not against the repository. Containers that were not restarted, worker pools pinned to an old image and cron jobs that pull a stale tag are the usual stragglers. For long running processes, the interpreter version that matters is the one loaded when the process started.
Step 5. Find the risky regex calls in your code
Search for every place a pattern is built at runtime. These searches are deliberately broad; expect to read each hit.
# Explicit constructors
rg -n --type ruby 'Regexp\.(new|compile|union)\s*\('
# Interpolation into regex literals
rg -n --type ruby '/[^/\n]*#\{[^}]*\}[^/\n]*/[imxo]*'
rg -n --type ruby '%r\{[^\n]*#\{'
# Common request and configuration sources near regex construction
rg -n --type ruby -e 'Regexp\.new\(\s*(params|request|args|options|config|yaml|json)'
For each hit, answer three questions. Can an attacker influence the string, directly or through stored data such as a saved filter or a tenant setting? Is it escaped with Regexp.escape before it reaches the pattern? Does the feature genuinely need regex syntax, or would a plain substring or prefix match do?
Step 6. Check your gems too
GitLab's unauthenticated path ran through a gem, so search installed dependencies as well:
bundle list --paths | xargs -I{} rg -l --type ruby 'Regexp\.(new|compile|union)\s*\(' {} 2>/dev/null
This will list many gems, most of which build patterns from their own constants. Narrow the list to gems that parse content an attacker can supply: markup and document renderers, template engines, search and query parsers, routing helpers and anything that reads configuration from users. For those, read the call sites and decide whether the input is trusted.
If you cannot upgrade a runtime yet, these code level controls reduce exposure:
- Escape input that is meant to be matched literally:
Regexp.new(Regexp.escape(input)). - Replace user supplied regex features with simpler matching where the product allows it, such as substring, prefix or glob matching.
- Where users must write patterns, consider a separate regex library with its own engine, such as a binding to a linear time engine. That changes syntax and needs testing, but it keeps untrusted patterns away from Onigmo entirely.
- Remove regex input from unauthenticated surfaces first. GitLab's two CVSS 10.0 issues were the unauthenticated ones.
Do not rely on Regexp.timeout or on length limits as the fix; Ruby says neither protects against these bugs.
Step 7. Look for crashes
Any Ruby service can produce the crash signature GitLab describes. Search the error output of your application servers and job workers for native crash reports that mention Onigmo frames:
grep -B1 -A30 '\[BUG\]' /path/to/app/stderr.log* \
| grep -E 'onig_compile|compile_tree|compile_length_tree|match_at|onig_search_gpos|rb_reg_search_set_match'
On GitLab Omnibus the Puma log is /var/log/gitlab/puma/puma_stderr.log and Sidekiq is /var/log/gitlab/sidekiq/current, as GitLab's rule documents. Make sure your log pipeline actually ships standard error from application workers; GitLab notes native crash reports are often not collected by default. A hit is worth an investigation. No hit proves nothing.
Step 8. Record it so it does not get lost
Because no Ruby CVE exists, write the link down where your tooling and your auditors will find it:
- Add a ticket per service with the Ruby version found, the version deployed and the date.
- If you publish SBOMs or VEX documents, record the GitLab CVE IDs against your Ruby component with a note, as Ruby suggests. For example, in a CycloneDX VEX entry:
{
"id": "CVE-2026-110264",
"source": { "name": "GitLab", "url": "https://www.cve.org/CVERecord?id=CVE-2026-110264" },
"analysis": {
"state": "resolved",
"detail": "Underlying Onigmo bug fixed in Ruby 3.4.12 (ruby/ruby #16801). Ruby has not assigned its own CVE; tracked per ruby-lang.org advisory of 23 Oct 2026."
},
"affects": [ { "ref": "ruby-runtime" } ]
}
- Add a CI check that fails if a Dockerfile,
.ruby-versionorGemfilepins a Ruby below the fixed version for its series, so an old tag cannot come back in a later merge.
What changed since the first reports
The story has moved in stages, and early coverage told only part of it:
- 23 September. GitLab's first two CVEs described a double free and an integer overflow "when parsing a specially crafted regular expression in a CI/CD configuration". Both needed an account. Nothing in the advisory pointed outside GitLab.
- 12 October. CVE-2026-97229 removed the authentication requirement on the CI path and was scored 10.0. GitLab's bug fix list named "Ruby regexp program size" and "repeat-range double-free" patches for Ruby 3.3 and 3.4, the first sign in GitLab's release notes that the fix lived in Ruby.
- 21 October. CVE-2026-110264 reached every GitLab version from 7.0 onward and needed no account. The fix list named a "repeat ID overflow" patch for Ruby 3.3, 3.4 and 4.0. GitLab's detection write-up described the
org-rubypath. - 23 October. Ruby published its own releases and the CVE mapping, confirmed that any application compiling attacker controlled patterns is affected, and said it has not requested CVEs of its own for now.
What has not changed: there is no public report of exploitation of any of the four CVEs, the CISA ADP enrichment says "none" for exploitation, and none are in KEV as of 23 October.
Open questions
- Will Ruby assign CVEs? Ruby says it will discuss it and update the advisory if it does. If it does, scanners will start to flag Ruby runtimes directly, and you may see new findings on images you already fixed.
- Will Ruby's documentation change? The Regexp page currently frames untrusted patterns only as a denial of service concern. A change would clarify whether compiling untrusted patterns is supported at all.
- What about older embedded Rubies? GitLab's backport notes for 19.1, 19.0 and 18.11 list the "repeat-range double-free patch for 3.3 and 3.4"; we have not confirmed exactly which Ruby patches each older GitLab line carries beyond what the release notes say.
- Did
org-rubychange? The gem published 0.10.0 and 0.10.1 on 9 October 2026 after almost twelve years. We could not confirm from the sources we read whether those releases address the keyword path. - How do distributions handle a fix with no CVE? Distribution trackers key on CVE IDs. Watch your distribution's Ruby package changelog for references to the three pull requests.
- Are other Ruby applications being probed? We found no public reporting of attempts against non GitLab Ruby applications.
What would have helped
This incident is less about one product and more about two habits: knowing which language runtimes you actually run, and knowing where your code turns user input into something the runtime compiles.
Practices any team can use
- Treat the language runtime as a dependency. Pin the Ruby version in
.ruby-version, theGemfileand the Dockerfile, and update all three together. A runtime that only appears in a base image tag is easy to forget. - Track releases, not only CVEs. Ruby's 4.0.8 release on 22 October was described as routine and still carried one of these fixes. Subscribing to ruby-lang.org news and the GitHub releases feed would have flagged 4.0.9, 3.4.12 and 3.3.13 the morning they shipped, CVE or not.
- Rebuild images on a schedule. Teams that rebuild from the latest patch tag weekly pick up fixes like these within days, even when nothing in their own code changed.
- Ban runtime regex construction from untrusted input in code review. A lint rule that flags
Regexp.newwith a non literal argument, and/#{...}/with request data, turns a design risk into a review comment. RuboCop custom cops or a simple CI grep both work. - Review gems that parse untrusted content. Renderers and parsers are the most likely to build patterns from what they read. Keep a short list of them and check their call sites when a runtime advisory like this one appears.
- Ship worker stderr to your log platform. Native crash reports are rarely collected. Without them, GitLab's corroborating signal does not exist for you.
- Move off end of life runtimes on time. Ruby 3.2 reached end of life on 1 April 2026 and will not get these fixes. A runtime calendar with owners prevents a forced migration under pressure.
Where Safeguard fits
Safeguard would not have prevented these bugs, and it would not have flagged a Ruby runtime for them automatically, because no CVE record names Ruby. Here is what it does help with.
- Inventory. Safeguard scans repositories, container images and SBOM files (
safeguard scan --dir .,safeguard scan --image <name>,safeguard scan --sbom <file>, with JSON results under./output), with dependency analysis that covers Bundler projects and container layers. The platform shows which projects contain a given component and version, which turns "which services still pin Ruby 3.4.11" into a query. Whether a source built interpreter in/usr/localis recorded depends on how the image was built, so confirm with the commands in Step 1. - Matching depends on public records. Vulnerability matching draws on NVD, GHSA, OSV, CISA KEV and vendor bulletins. Since those records name only GitLab, expect GitLab findings, not Ruby findings, until Ruby assigns CVEs. Use the version check in Step 2 in the meantime.
- Policies and gates. A policy evaluated in the platform can hold back builds that still pin a Ruby version below the fixed release for its series, so a stale base image cannot slip back in.
- Making the change with approval. The Safeguard CLI's assistant can search a repository for
Regexp.newcalls with request data, bump theFROMline,.ruby-versionandGemfiletogether, and runbundle exec rake test, asking before each command under its default permission mode. You can send that work from the desktop or phone app to a computer running the CLI and approve each step there. Autonomous remediation can open fix pull requests for dependency upgrades and ships switched off by default.
Where Safeguard does not help: it does not patch interpreters embedded in vendor products such as GitLab Omnibus, it cannot see Ruby inside appliances or hosted services, reachability analysis cannot decide whether a given Regexp.new call receives attacker input in your application, and it does not detect attacks or exploitation attempts.
Zero-Day Discovery and Gold
Safeguard's Zero-Day Discovery looks for previously unknown flaws before a CVE exists, in an agentic mode and a deterministic scanner mode, and issues findings as SGZ advisories before they reach public CVE databases. It covers seven package ecosystems, including RubyGems. The Ruby interpreter and its built in Onigmo engine are a language runtime written in C, not a gem, so they sit outside what Zero-Day Discovery covers. Safeguard had no part in finding these bugs; they were reported by joaxcar through GitLab's bug bounty program.
Gold Open Source at gold.safeguard.sh is free and needs no login. It shows CVSS, EPSS, KEV status, affected and fixed versions and a recommended safe version for a CVE, through the site, a free read API and a CI gate GitHub Action. We have not confirmed how it presents these four GitLab CVEs, and because the records name only GitLab, do not expect a Ruby safe version from any CVE based source. Gold Registry at registry.safeguard.sh provides hardened, signed components and container images held to zero exploitable critical or high CVEs; we make no claim that it carries a Ruby image with these fixes. The habit worth taking from both is the one this incident rewards: pull patched, signed components from a curated source, rebuild fast, and do not wait for a CVE to tell you a runtime release matters.
Common mistakes
- Upgrading GitLab and stopping there. GitLab patched its own embedded Ruby. Your other Ruby services did not get that patch.
- Trusting the scanner's silence. A clean report on a Ruby 3.4.11 image means no CVE record matched, not that the image is fixed.
- Checking the CLI Ruby and assuming the app matches.
ruby -vin a shell can differ from the interpreter a Puma or Sidekiq process started with, especially with version managers and multi stage images. - Forgetting Ruby 3.2. It is end of life and will not get a fix. It needs a migration plan, not a patch.
- Only searching your own code. GitLab's unauthenticated path went through a gem.
- Reading a distribution's version number literally. A backported package can be fixed with an older version string, and an unpatched one can look recent. Check the changelog.
- Pasting crash reports into public issues. A full native crash report can include memory addresses and paths. Share it with your security team first.
FAQ
What did Ruby release on 23 October 2026?
Ruby 4.0.9, 3.4.12 and 3.3.13. Together they fix three bugs in Onigmo, Ruby's regular expression engine, that GitLab used to show remote code execution: a double free on a parse error, an integer overflow in compiled program size, and an overflow of internal repeat IDs.
Is my Rails application affected?
Only if it compiles a regular expression from text an attacker controls, through Regexp.new, Regexp.compile, Regexp.union or interpolation like /#{input}/, or uses a gem that does so with attacker supplied content. Applications that only use regexes written by their developers are not affected, according to Ruby. Upgrade anyway; it is the only complete fix and it costs a patch release.
Why does my vulnerability scanner not show anything?
Ruby has not requested CVE identifiers for these fixes. The four CVEs that exist are GitLab's, and their records list GitLab as the only affected product. A scanner matching Ruby versions against CVE records has nothing to match. Ruby suggests tracking the issue under GitLab's CVE IDs; you will need to add that mapping yourself.
Does Ruby 4.0.8 fix it?
Partly. 4.0.8 contains the double free fix (already present since 4.0.0) and the program size fix, but not the repeat ID fix behind CVE-2026-110264. Use 4.0.9.
I am on Ruby 3.2. What now?
Ruby 3.2 reached end of life on 1 April 2026, and no fix will ship for it. Move to 3.4.12 or 4.0.9. Until you do, make sure no service on 3.2 compiles regexes from untrusted input, and prioritise any that does.
Does Regexp.timeout or a pattern length limit protect me?
No. Ruby's advisory says Regexp.timeout does not protect against these bugs and that length limits are not reliable because some bugs trigger with short patterns.
Has anyone exploited this outside GitLab?
We found no public report of exploitation, against GitLab or anything else. The CISA ADP enrichment on all four CVE records rates exploitation as "none", and none are in the KEV catalog as of 23 October.
Does upgrading GitLab fix Ruby on the rest of my servers?
No. GitLab ships its own embedded Ruby and patched that copy. System Ruby, other applications and container images are separate and need their own upgrade.
How do I prove to an auditor that we handled it?
Keep the inventory from Step 1, the before and after versions from Step 2, the merged pull requests that bumped Ruby, and a note or VEX entry tying the GitLab CVE IDs to your Ruby component with a link to Ruby's advisory. That shows you tracked a security relevant fix even though no Ruby CVE exists.
Fix it without waiting for upstream
This incident shows both gaps in one week: fixes that sat in Ruby's main branch for months before reaching a stable release, and a runtime fix with no CVE that most tooling cannot see. The habits below close the gap for the dependencies and images you build yourself.
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would have helped
These are real product sessions on other examples; none of them shows the Ruby Onigmo fixes or GitLab.
The CLI demo shows the assistant fixing a lodash CVE: it reads the finding and advisory, asks before each command, edits package.json to lodash 4.17.21, runs npm install and npm test with approval, and pushes nothing. For this incident the same flow applies to a Ruby repository: ask it to find every Regexp.new and interpolated regex that takes request data, then to bump the Dockerfile FROM line, .ruby-version and Gemfile to 3.4.12 together, and approve the Bundler update and your test run one at a time.
Click through it at your own pace, or open it full screen.
The desktop app demo shows a CLI session in the conversation list, a request for work on the computer, a folder choice, and Approve or Deny on each command and file edit, ending with an upgrade done and tests passing. Here you would pick the folder that holds a service's Dockerfile, ask for the Ruby version bump and the onigmo_check.rb version check from this post, and approve the image build and the check before anything is committed.
Click through it at your own pace, or open it full screen.
The mobile demo shows a real Android session that sends work to a computer running the CLI and approves a command on the phone with Allow once, Allow for turn or Deny. A Ruby runtime upgrade often lands outside working hours when an advisory arrives overnight; from the phone you could ask the computer to rebuild a service image on Ruby 3.4.12 and approve the build and test commands as they come up.
Click through it at your own pace, or open it full screen.
To try it on your own projects, download Safeguard and read the CLI documentation. Related reading: GitLab CVE-2026-110264 explained, finding affected products fast with SBOMs, container image supply chain incidents in 2026 and our October 2026 roundup.
Quick check
Which Ruby releases contain all three Onigmo fixes?
Ruby 4.0.9, 3.4.12 and 3.3.13, all released on 23 October 2026. Ruby 4.0.8 has only two of the three, and Ruby 3.2 and older get no fix.
Why will a CVE based scanner not flag an unpatched Ruby runtime for these bugs?
Ruby has not assigned CVEs of its own, and the four GitLab CVE records list only GitLab as the affected product, so there is no record that matches a Ruby version.
Sources
- Ruby: Regexp compilation fixes related to GitLab's CVE-2026-89078, CVE-2026-93577, CVE-2026-97229 and CVE-2026-110264
- Ruby 4.0.9 Released
- Ruby 3.4.12 Released
- Ruby 3.3.13 Released
- Ruby 4.0.8 Released
- Ruby maintenance branches
- Ruby: Double free in Regexp compilation (CVE-2022-28738)
- Ruby: Arbitrary memory address read vulnerability with Regex search (CVE-2024-27282)
- Ruby documentation: Regexp timeouts
- ruby/ruby pull request #13332
- ruby/ruby pull request #19020
- ruby/ruby pull request #16801
- ruby/ruby GitHub release v4.0.9
- CVE.org: CVE-2026-89078
- CVE.org: CVE-2026-93577
- CVE.org: CVE-2026-97229
- CVE.org: CVE-2026-110264
- NVD API: CVE-2026-89078
- NVD API: CVE-2026-93577
- NVD API: CVE-2026-97229
- NVD API: CVE-2026-110264
- GitLab Patch Release 19.4.1, 19.3.3, 19.2.7
- GitLab Patch Release 19.4.2, 19.3.5, 19.2.8
- GitLab Patch Release 19.5.1, 19.4.3, 19.3.6
- GitLab Security detection: Onigmo native worker crash
- CISA Known Exploited Vulnerabilities catalog (JSON feed)
- RubyGems: org-ruby versions
- Docker Hub: official ruby image tags
- rbenv ruby-build definitions
- The Hacker News: Ruby fixes three regex engine bugs responsible for four critical GitLab RCE flaws
Last reviewed: 24 October 2026. This is a developing story; check the linked advisories for the latest status.