GitLab CVE-2026-97229: The Fix Lives in Ruby, and Custom Builds Stay Exposed
GitLab checks regular expressions from the CI/CD configuration before using them, but Ruby's regex engine may recompile a pattern later on. With CVE-2026-97229 that is exactly what happens. A pattern that looks harmless as compact ASCII grows so much when recompiled against a UTF-8 path that the size calculation in Onigmo overflows and the compiler produces invalid instructions. Because the bug is in Ruby and not in GitLab's code, the GitLab update only protects you where the bundled, patched Ruby is running. Alpine Linux experienced this on its own server in October, and since October 29 the vulnerability has been in CISA's KEV catalog.
TL;DR — 90 seconds
- What: An integer overflow in the size calculation of the Onigmo regex compiler in Ruby. GitLab reaches it through regular expressions in
.gitlab-ci.yml, and at the end an operating system command runs as thegituser. - Severity: CVSS 10.0 according to GitLab (
AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H). Since October 29 the vulnerability has been in the CISA KEV catalog, Rapid7 has published a PoC, and EPSS is around 27 percent. - Who is affected: Self-managed CE and EE instances in the 19.2 to 19.4 lines before the fix. If you run GitLab with a separately installed Ruby, you stay vulnerable even after the GitLab update until Ruby itself is updated.
- What to do: Update to the current patch version of your line, at least 19.4.2, 19.3.5 or 19.2.8. With your own Ruby, also move to Ruby 3.3.13, 3.4.12 or 4.0.9, because Ruby 3.2 no longer gets a fix.
- What would have protected you: no self-registration and no public projects for outsiders, Ruby as its own component in the patch process, and an egress block for the GitLab server. According to the Ruby team, a timeout for regex evaluation does not help.
A pattern that is recompiled after the check
GitLab evaluates regular expressions in CI/CD configurations, for example in rules that match branches or changed paths. These expressions come from anyone allowed to commit a .gitlab-ci.yml, which makes them outside input. GitLab therefore checks them before use. According to Rapid7, the crafted expression passes this check as long as it is a compact US-ASCII pattern.
This is exactly where the bug was. When the pattern meets a valid UTF-8 path in the repository, Onigmo, Ruby's regex engine, recompiles it for that encoding, after GitLab's checks. Onigmo translates a pattern into a program for its own small virtual machine. The compiler unrolls repeats with a fixed lower bound as long as the result stays below a size limit. According to Ruby PR #19020, with nested repeats the product of length and lower bound already overflowed, so the check against the limit wrongly passed. Concatenated repeats could also push the program past INT_MAX, and then the relative jump addresses in the program overflowed.
Why that becomes dangerous follows from the role of these jump addresses. If a jump in the regex program points to the wrong place, the machine executes bytes as instructions that were never meant to be instructions. Rapid7 describes how this allows attacker-controlled Onigmo operations and finally an operating system command as the git user. Johan Carlsson reported the finding through GitLab's HackerOne programme.
The vulnerability does not stand alone. According to the Ruby announcement of October 23, four GitLab CVEs go back to three bugs in Onigmo: a double free after an alternation (CVE-2026-89078), the program size overflow described here (CVE-2026-93577 and CVE-2026-97229) and an overflow of internal IDs with many repeats (CVE-2026-110264, which I describe in a separate post). According to the Ruby team, any application that compiles patterns from foreign input via Regexp.new, Regexp.union, Regexp.compile or interpolation into a literal is affected.
What the fix changes
The fix in Ruby PR #19020 works in two places. The first commit compares the size of an unrolled repeat with the limit using a division instead of a multiplication that can overflow. The second rejects a compiled program that is too large for the type of its jump addresses, and caps the size while the instructions are emitted rather than only in the preceding length calculation. A pattern that is too large thus ends in a RegexpError instead of a broken program.
That reveals a lot about the root cause. GitLab's check was not wrong, it simply saw a different program from the one that ran later. And Onigmo relied on an up-front calculation being correct instead of enforcing the limit where the program is actually built. The timeline is notable too: according to GitHub, the PR was merged into Ruby's main branch on September 25, and the Ruby release with all fixes appeared on October 23. GitLab had already patched its bundled Ruby itself. Anyone who got Ruby from another source had no official fix during that period.
Prerequisites, affected versions and exploit status
For the tested path, an attacker needs write access to a branch in a public project according to Rapid7, in order to place a crafted .gitlab-ci.yml and a matching UTF-8 path there. GitLab still rates the vulnerability as exploitable without authentication, and on many instances that is effectively true. Where anyone can register and create projects, such an account is a formality. According to the GitLab documentation, anyone visiting the domain can create an account by default. New instances require administrator approval by default, but older instances often lack this setting, and if approvals are waved through routinely, you effectively do not have it either.
| CVE | Affected | Fix | Prerequisite | CVSS |
|---|---|---|---|---|
| CVE-2026-97229 | CE/EE 19.2 before 19.2.8, 19.3 before 19.3.5, 19.4 before 19.4.2 | 19.2.8 / 19.3.5 / 19.4.2 with bundled Ruby; with your own Ruby also Ruby 3.3.13 / 3.4.12 / 4.0.9 | According to Rapid7: commit of .gitlab-ci.yml and a UTF-8 path to a public project | 10.0 (critical) |
| CVE-2026-92883 | CE/EE from 19.1 | 19.2.8 / 19.3.5 / 19.4.2 | Low-privilege account, victim interaction | 8.7 (high) |
| CVE-2026-16301 | CE/EE from 18.7 | 19.2.8 / 19.3.5 / 19.4.2 | Developer role | 8.5 (high) |
| CVE-2026-14814 | CE/EE, all versions before the fixes | 19.2.8 / 19.3.5 / 19.4.2 | Low-privilege account | 8.2 (high) |
| CVE-2026-81839 | EE from 18.1 | 19.2.8 / 19.3.5 / 19.4.2 | Low-privilege account, interaction | 7.7 (high) |
| CVE-2026-96893 | EE from 19.1 (Duo Agent Platform) | 19.2.8 / 19.3.5 / 19.4.2 | Low-privilege account, interaction | 7.3 (high) |
The remaining rows come from the same patch release of October 12. For CVE-2026-97229, the advisory only names the 19.2 to 19.4 lines, but does not say whether older lines are safe. The selection follows from GitLab's maintenance policy, under which security fixes are only backported to the current and the two previous monthly releases. Because the bug sits in Onigmo and older lines no longer receive a patched Ruby, I would treat instances before 19.2 as vulnerable. Whether the attack path works the same way there is not documented, however. GitLab.com runs patched, and GitLab Dedicated requires no action.
The exploit situation has become much more serious since mid-October. Rapid7 reproduced code execution on an official 19.4.1 CE image and published a PoC. CISA added the vulnerability to the KEV catalog on October 29 based on evidence of active exploitation, and US federal agencies are to patch by November 1. On October 30, EPSS estimated the probability of exploitation in the next 30 days at about 27 percent, which puts it in the top two percent of all scored CVEs.
The attack on Alpine Linux and what it shows
Alpine Linux has openly described what a real attack looks like. The instance had been running 19.3.4 since October 13, because the team had accidentally installed that version instead of 19.3.5. On October 20, a newly registered account began probing the instance. On October 21, the attacker sent crafted CI configurations that crashed GitLab workers eight times, and in four of those cases, according to Alpine, got partway into the chain before it failed at the next step. Users saw 502 errors and failed CI jobs. The attacker also downloaded Alpine's GitLab container image, which the team interprets as an attempt to tune the exploit to exactly this build.
The most instructive part came afterwards. On October 22, Alpine updated to 19.3.6 and was still vulnerable, as the team found on October 26. The instance used a normally installed Ruby rather than the Ruby from the official GitLab packages, and GitLab's fix is a patch to exactly that bundled Ruby. Upstream Ruby with the fixes only appeared on October 23. New attempts by the attacker on October 25 and 26 failed, and their account had been blocked since the 23rd.
Alpine found no evidence of success but cannot rule it out. According to Alpine, a successful exploit leaves neither a crash nor a trace in the web log, some logs from the day of the attack are missing, and outbound connections were not logged. The team therefore rebuilt GitLab from scratch, reset all passwords and replaced GitLab's secret keys. It checked releases, images and packages from the period in question against records outside GitLab and found them unchanged, and signing keys were never stored on the server.
What a successful attack costs is shown by Alpine's list of potentially affected data: users' names, email addresses and IPs, password hashes, 2FA secrets, CI/CD variables and runner tokens, credentials for webhooks and mirrors, hashes of personal access tokens, and private repositories and confidential issues. A GitLab instance is therefore direct access to the supply chain of every project built there.
Patch, Ruby and the trap of custom builds
Which fix works for you depends on where your Ruby comes from. GitLab's Linux packages and official container images bring their own Ruby, which GitLab has patched itself. There, updating to the current patch version of your line is enough, at least 19.4.2, 19.3.5 or 19.2.8. On October 21, 19.5.1, 19.4.3, 19.3.6 and 19.2.9 followed with further Ruby patches and the fix for CVE-2026-110264. In the 19.3 line, 19.3.5 also fixes a 19.3.4 bug with the Ona integration enabled, according to GitLab.
Things look different for source installations and for custom images that take Ruby from the distribution or a base image. There, the GitLab update changes nothing about Onigmo. You also need Ruby 4.0.9, 3.4.12 or 3.3.13, which according to the Ruby team contain all three fixes. According to the maintenance schedule, Ruby 3.2 has been end of life since April 2026 and does not appear in the announcement. Alpine shipped the fixes for releases 3.21 to 3.24 and edge via apk upgrade and asks users of its own image to pull alpinelinux/gitlab:19.3.6 again, because images pulled before October 27 are vulnerable.
The release includes database migrations. According to GitLab, single-node instances have downtime until the migrations have finished, while multi-node instances can use the zero-downtime procedure.
Until the patch is in, Rapid7 recommends restricting anonymous access to public projects and preventing untrusted users from creating or writing to public projects. That only blocks the demonstrated attack path and does not fix the compiler bug. According to the GitLab documentation, the relevant switches are under Admin > Settings > General:
- New user account restrictions: clear Allow new user accounts or enable Require admin approval for new user accounts.
- Visibility and access controls: under Restricted visibility levels, restrict the Public level. Then only administrators can create public projects.
- Visibility and access controls: raise Default minimum role required to create projects. According to the documentation, the setting only applies to new groups.
Hardening beyond the patch
First, some context. The attack needs two things: the right to bring a CI configuration into a project, and a regex engine that compiles a foreign pattern. Once it has succeeded, code runs with GitLab's own privileges and reaches everything GitLab knows. What works, therefore, is controlling who may inject patterns at all, making sure every component with the fix is really patched, and limiting how much a compromised server gives away. Classic container hardening helps little here, because the attacker lands in the process that already owns the data.
Who may bring in CI configurations
The demonstrated chain starts with a commit to a public project. An instance without self-registration, or with genuine approval of new accounts, and without public projects for outsiders takes this entry point away from strangers. At Alpine, the attack came exactly through a freshly registered account. For open source projects that have to accept outside contributions, this is hard to implement. There it pays to clearly separate forks and merge requests from outside from projects in which CI runs with access to secrets. This measure reduces the number of attackers, but it does not help against a compromised internal account.
Ruby as its own component in the patch process
Alpine's case shows that the fix can sit in a dependency, and then updating the application achieves nothing. If you run GitLab from source or on your own base image, you have to track Ruby with the same urgency as GitLab itself and read the release notes of every security release for component patches. Alpine now recommends this for itself too. A CVE scanner alone is not enough for this. According to Glitchwire, the Ruby team did not request its own CVEs for these bugs, the four numbers were assigned by GitLab. A scanner that checks the Ruby package may then find no matching entry, and one that only compares the GitLab version shows green after the update. I derive this scanning gap from how the numbers were assigned. The safest choice is to use the official packages or images unless there is a compelling reason for a custom build.
Fewer secrets on the GitLab server
How expensive a break-in becomes depends on what the server stores. CI/CD variables with long-lived cloud keys are lost after a compromise. Via the id_tokens keyword, GitLab can issue short-lived OIDC tokens to jobs, with which a job signs in to AWS, Vault or other providers without a permanent secret living in GitLab. Release signing keys do not belong on the server either. At Alpine they were never there, which is why the integrity of the releases could be checked against records outside GitLab. Signed release tags, which Alpine plans to introduce as a lesson, make exactly this check independent of GitLab.
Egress block and logging for the server
After code execution, an attacker usually wants to get data out or open a connection to the outside. An egress rule that only allows the GitLab server the destinations it really needs for mirrors, webhooks, mail and updates makes both harder. In Kubernetes this works through NetworkPolicies with policyTypes: [Egress], on a VM through firewall rules or security groups. At the very least, you should log outbound connections and keep logs for longer than 14 days. Exactly this gap prevented Alpine from ruling out the attacker's success with certainty. The rule takes maintenance, because every new integration needs an exception, and webhooks to arbitrary destinations fundamentally conflict with it.
What does not help here
A timeout for regex evaluation such as Regexp.timeout does not protect against these bugs according to the Ruby team, and a length limit for patterns does not help reliably either, because some bugs can be triggered with short patterns. A web application firewall does not see the pattern as an attack, because it arrives as part of a Git push in a YAML file rather than in an HTTP request to the web interface. I derive this from the attack path. runAsNonRoot, dropped capabilities, seccomp and a read-only file system remain sensible for GitLab pods, because they make an escape to the node harder. They do not protect the data the attacker already reaches as the git user, though.
You will find more on egress rules, secrets and update processes in the series TYPO3 on Kubernetes under IT-Grundschutz.
Detection and verification
You find out whether you are affected in two steps. According to the documentation, Help > Help in the left sidebar shows the running GitLab version. If it is in the 19.2 to 19.4 lines below the fix, the instance is vulnerable. Then comes the question Alpine overlooked: which Ruby is actually running? With the official packages and images it is GitLab's bundled Ruby. For source installations and custom images, check the Ruby version in the container or on the host and compare it with 3.3.13, 3.4.12 and 4.0.9. The attack surface also includes whether outsiders can register themselves and whether there are public projects they may write to.
You can only recognise an attempted attack by its side effects. GitLab and Rapid7 have not published any IoCs. At Alpine, failed attempts showed up as crashed GitLab workers, and users saw 502 errors and failed CI jobs. Before that came a newly registered account probing the instance and the download of the GitLab image. From this sequence I derive what to watch for: new accounts pushing .gitlab-ci.yml to public projects, clusters of worker crashes and paths with unusual UTF-8 characters in fresh commits. According to Alpine, however, a successful attack leaves no crash and no trace in the web log. Without logged outbound connections, little remains to recognise it by.
If you suspect a compromise, Alpine's approach is a good model. Block the suspicious account, take the instance offline and rebuild it from a patched state instead of cleaning up the running system. Then reset passwords, replace GitLab's secret keys and rotate runner tokens, CI/CD variables, access tokens and credentials for webhooks and mirrors. Finally, check releases and artefacts against records outside GitLab.
Recommendation for operators
Act now if your self-managed instance runs the 19.2 to 19.4 lines before the fix, or uses its own Ruby before 3.3.13, 3.4.12 or 4.0.9, and is reachable from the internet. Exploitation is confirmed, a PoC is public, and open registration or public projects make the instance an easy target. Patch outside the regular maintenance window, check the actual Ruby version afterwards and, for a single node, plan for the downtime of the migrations.
Monitoring is enough if you use GitLab.com or GitLab Dedicated. For self-managed, the regular window is only enough if the instance is not publicly reachable, does not allow self-registration and has no public projects. The update remains mandatory anyway because of the other high-severity vulnerabilities in the same release.
As with every vulnerability: patches and updates belong in automation. When Renovate or Dependabot propose new GitLab images, Helm charts and base images including Ruby as merge requests and the pipeline tests them against a staging instance, a critical security release is a matter of hours rather than weeks. Alpine's case shows the value of this discipline particularly clearly, because there a wrong patch version chosen by hand and a forgotten Ruby led to a week of exposure. A cooldown period for routine updates protects against compromised releases, and you exempt security updates from it. If migrations or integrations fail, that shows up in staging and not on the production instance. If you update by hand, you can easily end up with weeks without a patch during the holiday season, followed by a bulk update across several versions with correspondingly many migrations.
FAQ on CVE-2026-97229
Is the GitLab update alone enough?+
Only with the official packages or images, which ship the patched Ruby. With your own Ruby you also need Ruby 3.3.13, 3.4.12 or 4.0.9.
Which version is the target?+
The newest patch version of your line. At least 19.4.2, 19.3.5 or 19.2.8, ideally the releases of October 21. They also close CVE-2026-110264.
Is it enough to switch off public projects?+
No. That only blocks the attack path Rapid7 demonstrated, while the flaw in the regex compiler remains. It can only be fixed with the update and, with your own Ruby, a current Ruby.
Is CVE-2026-97229 already being exploited?+
Yes. CISA added the vulnerability to the KEV catalog on October 29. Alpine Linux has described an attack on its own instance, and Rapid7 has published a PoC.
Can the flaw really be exploited without logging in?+
GitLab rates it that way, even though Rapid7 needed an account with write access to a public project for the tested path. Where anyone can register, however, that account is a mere formality.
Conclusion
CVE-2026-97229 shows two things that reach far beyond GitLab. The first concerns input validation: GitLab checked the pattern but not the program Onigmo later built from it, and anyone who lets foreign patterns be compiled depends on the robustness of the engine. The second concerns the patch process. The fix was in Ruby, updating the application had no effect with a separate Ruby, and for Alpine exactly this difference meant a week of exposure and a complete rebuild. If you build an application yourself, you take on responsibility for every component the vendor would otherwise ship. It is worth going through your own builds to see where runtime, libraries or base images differ from what the vendor tests and patches.
Sources
- GitLab: Critical Patch Release 19.4.2, 19.3.5, 19.2.8 (October 12, 2026)
- GitLab: Patch Release 19.5.1, 19.4.3, 19.3.6, 19.2.9 (October 21, 2026)
- Ruby: Regexp fixes for GitLab CVEs (October 23, 2026)
- Ruby PR #19020: Avoid overflow in the regexp compiler's program size arithmetic
- Ruby: Maintenance status of branches
- Alpine Linux: GitLab security incident, attempted exploitation of CVE-2026-97229
- Rapid7: CVE-2026-97229, Critical Unauthenticated Remote Code Execution in GitLab
- The Hacker News: CISA Adds Actively Exploited GitLab RCE to KEV
- CISA: Known Exploited Vulnerabilities Catalog
- Glitchwire: Ruby patches Onigmo regex bugs
- FIRST EPSS: CVE-2026-97229
- GitLab Docs: Sign-up restrictions
- GitLab Docs: Visibility and access controls
- GitLab Docs: OpenID Connect (OIDC) Authentication Using ID Tokens
- GitLab Docs: Find the GitLab version
- GitLab Docs: Maintenance policy
I check your GitLab instance for unpatched components such as Ruby, lock down open registration and public projects and automate the security updates.
GitLab audits for self-managed instances, reviews of custom builds and base images, OIDC instead of long-lived CI secrets, egress rules and update automation with Renovate and tested pipelines.
Platform operations instead of consulting on paper: I continuously check, patch and harden your infrastructure.
