GitLab CVE-2026-110264: An Org File Overflows Ruby's Regex Compiler
GitLab does not show files in Org format as raw text. It renders them to HTML with the org-ruby gem, and it has done so since version 7.0 in 2014. While doing that, org-ruby builds a regular expression from custom TODO keywords defined in the file without escaping the keyword. If such a keyword contains enough repetitions, the Onigmo regex engine counts its internal repeat IDs past the limit of a signed 16-bit value and writes outside the intended memory during matching. CVE-2026-110264 turns this into code execution as the git user, triggered by visiting a public page. That is why the fix does not live in GitLab's code but in the Ruby that GitLab ships.
TL;DR — 90 seconds
- What: org-ruby inserts custom TODO keywords from an .org file into a regular expression without validation. A crafted keyword makes Onigmo overflow its 16-bit repeat IDs, and during matching the engine writes outside its memory.
- Severity: CVSS 10.0 (
AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H), classified as CWE-190. According to GitLab the bug leads to code execution as thegituser. No confirmed public exploit and no attacks in the wild are known so far. - Who is affected: Self-managed GitLab CE and EE from 7.0, exploitable without authentication as soon as a public project can hold such a file. If you upgraded to 19.4.2, 19.3.5 or 19.2.8 on 12 October, you are still vulnerable.
- What to do: Upgrade to 19.5.1, 19.4.3 or 19.3.6, or if necessary to the backports 19.2.9, 19.1.9, 19.0.10 or 18.11.13, which carry only this one of the release's twelve security fixes.
- What would have protected you: an instance that is not openly reachable from the internet, no public projects and no open sign-up. Container hardening, on the other hand, does little to stop the instance secrets from being stolen after a break-in.
From Org file to memory corruption
Org is the text format of Emacs's Org mode, and GitLab treats it much like Markdown or AsciiDoc. When someone opens a file ending in .org in the repository browser, Workhorse passes the request to a Puma worker running the Rails application. From there it travels through Gitlab::OtherMarkup and the gitlab-markup gem to org-ruby, which translates the file into HTML. According to the detection rules GitLab published on 20 October, GitLab uses org-ruby 0.9.12 and a time budget for rendering. The Gemfile of GitLab 7.0 from June 2014 lists org-ruby for the first time, and the vulnerability applies from exactly that version.
Org files may define their own status words for tasks. A line such as #+TODO:, #+SEQ_TODO: or #+TYP_TODO: replaces the usual words TODO and DONE with custom terms, and the parser then has to recognise them in every heading. org-ruby does this with regular expressions. When it builds the shared alternation it still escapes the keywords correctly, but in a second place it puts the raw word straight into Regexp.new. Whatever is in the file becomes the pattern, and the pattern becomes a program.
A look inside the regex engine shows why this is more than a cosmetic flaw. Ruby compiles every regular expression with Onigmo into bytecode, which an interpreter written in C then runs against the text. Every repetition with a range gets its own repeat ID, which the interpreter uses during matching to find its counter in memory. According to the Omnibus merge request for the fix, Onigmo stores these IDs as a signed 16-bit operand in the bytecode but counts them in a plain int while compiling. Once the limit of 32,767 is exceeded, the pattern compiles without error, but the operand is negative. During matching the interpreter then uses this negative index to access memory in front of its counter array and writes into it.
The bug therefore sits in two layers at once. org-ruby gives the content of a foreign file the power to shape a regular expression, and Onigmo relies on no pattern ever growing large enough to overflow its own counters. According to GitLab, the out-of-bounds write can be turned into a call to system() by preparing the heap through the request body. The rendering time budget does not change this, because it only limits how long rendering takes, not what happens in memory meanwhile.
The fix in the bundled Ruby
There is no public fix in GitLab's own code, and org-ruby stays pinned to ~> 0.9.12 in the current Gemfile. What gets repaired is the engine: Omnibus merge request !9921, merged on 14 October, backports Ruby pull request 16801, titled “Reject overflowed repeat/null-check IDs in huge regexps”. From then on the compiler checks whether repeat and null-check IDs still fit into their operands and rejects an oversized pattern with a RegexpError, for example with the message “too many range repeat”. A program with a negative index thus never reaches the interpreter. Upstream, according to the merge request, this fix currently exists only in the development branch that will become Ruby 4.1, not in the 3.3, 3.4 and 4.0 branches. GitLab therefore patches its Ruby builds itself, both in the Linux package and in the cloud-native images.
For the older lines the backports are larger. Backport merge request !9927 for 19.1 bundles three Ruby patches: this repeat ID fix, a fix for a double free in range repetitions and a program size limit. GitLab had already shipped the latter two on 12 October with the releases for the sister vulnerability CVE-2026-97229 in CI rules, and the release post lists the double-free patch for 19.0.10 and 18.11.13 as well. I read the fact that GitLab repairs the engine rather than the caller as a deliberate choice: GitLab secures the shared sink because not every path by which user input becomes a regular expression can be found reliably. The downside is that the unescaped call in org-ruby remains and protection depends solely on the check in the compiler.
From a public project to a shell as git
The CVSS vector describes an attack without authentication and without user interaction. That holds for triggering the bug, but it deserves a closer look. According to GitLab's detection rule, the attacker only needs one public project that can hold the crafted file. So the file first has to be placed somewhere, and after that an anonymous request to the page that renders it is enough. This is easiest on instances with open sign-up, because the attacker simply creates an account and their own public project.
A second path goes through the editor preview. It picks the renderer based on the file_path parameter sent by the client and can thus force the Org renderer even for a file that does not end in .org. Whether this path also works without signing in is not clear from GitLab's information; the sample log in the detection rule shows a signed-in user.
According to GitLab the attack itself is not a clean single shot but a noisy grind with many crashing Puma workers until the memory layout fits. If it succeeds, the code runs with the rights of the git user, under which Puma and Sidekiq also run in the Linux package. How much damage follows depends on what this user can read. The file /etc/gitlab/gitlab-secrets.json belongs to root with mode 0600, so git cannot read it directly. The secrets.yml generated from it under /var/opt/gitlab/gitlab-rails/etc/, however, is written with mode 0644 according to the Omnibus recipe and contains, among others, db_key_base, secret_key_base, otp_key_base and openid_connect_signing_key. git can read database.yml with the database credentials through its group. Encrypted columns and their key thus end up in the same hands. GitLab itself lists access to all CI/CD variables, runner tokens and access tokens as well as read and write access to every repository as consequences.
Every self-managed instance that is reachable from the internet and allows public projects is therefore acutely at risk, with open sign-up as the worst case. Instances without public projects and without self-registration are much harder for outsiders to reach. For anyone who already has an account with write access there, the vulnerability still means code execution on the server. Many who moved to 19.4.2, 19.3.5 or 19.2.8 on 12 October because of CVE-2026-97229 are likely to feel safe without being safe, since those versions do not contain the new fix. According to the release post GitLab.com is already patched, and GitLab Dedicated tenants on 19.3.5-internal0, 19.4.2-internal0 or later need to do nothing.
| CVE | Affected | Fix | Precondition | CVSS |
|---|---|---|---|---|
| CVE-2026-110264 | GitLab CE/EE from 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, backports 19.2.9 / 19.1.9 / 19.0.10 / 18.11.13 | Public project with crafted .org file, triggered without authentication | 10.0 (critical) |
| CVE-2026-97229, sister vulnerability | 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 | Regular expression in the rules of a CI configuration | 10.0 (critical) |
| CVE-2026-15723, same release | From 10.1 before 19.3.6, 19.4 before 19.4.3, 19.5 before 19.5.1 | 19.3.6 / 19.4.3 / 19.5.1, not in the backports | Authenticated, OmniAuth SAML and LDAP auto-linking enabled | 8.2 (high) |
The release post contains twelve security fixes in total, but the other eleven are only in 19.3.6, 19.4.3 and 19.5.1. No source describes a chain of the two, but I consider one plausible: on an instance without open sign-up, someone with any account could take over an LDAP account via CVE-2026-15723 and gain the write access they lack for planting the file. Anyone who only moves to one of the backports leaves this vulnerability open.
Neither GitLab nor the trade press names a confirmed public exploit, and GitLab reports no signs of exploitation. For the sister vulnerability CVE-2026-97229, Rapid7 says it built a working exploit that executes code as git. That this bug family can be exploited is therefore established. According to The Hacker News, CVE-2026-110264 is not in the CISA KEV catalogue (as of 22 October), and FIRST’s EPSS API still returned no score on 22 October. It was reported by joaxcar via HackerOne; according to The Hacker News it is his fourth RCE through a regex engine within a month.
GitLab 19.5.1 and the path through the required stops
Everything is fully fixed in 19.5.1, 19.4.3 and 19.3.6. For older lines GitLab built the backports 19.2.9, 19.1.9, 19.0.10 and 18.11.13 outside the normal support window, and according to the version ranges in the release post they close only CVE-2026-110264. The release post names no workarounds via gitlab.rb or admin settings, and there is no setting that disables only Org rendering. If you still run 18.10 or older, there is no patched release for your line, and you have to go through the required stops defined in the upgrade documentation, at least to 18.11.13. The stops are currently 18.2, 18.5, 18.8, 18.11, 19.2, 19.5, 19.8 and 19.11, and GitLab's Upgrade Path Tool calculates the route from your starting version.
The release post announces a few pitfalls. 19.3.6 and 19.4.3 include regular database migrations, and 19.4.3 adds a post-deploy migration. A single-node instance therefore has a short downtime, while multi-node setups can upgrade using the zero-downtime procedure. If Gitaly rejects an archive download (GetArchive) because it is overloaded, Workhorse answers with HTTP 429 from 19.3.6, 19.4.3 and 19.5.1 onwards, which monitoring and clients may notice. The release also updates gitlab-markup to 2.2.0 and rdoc to 8.1.0. For Helm installations, chart version 10.5.0 belongs to 19.5.1.
Source installations deserve special attention. Because the fix lives in the bundled Ruby and not in the Rails code, updating GitLab alone will likely not protect you if you use your own Ruby from the 3.3, 3.4 or 4.0 lines. The release post says nothing about this. My assessment rests on Omnibus merge request !9921, which places the upstream fix only in Ruby's development branch, and I would confirm it with the plausibility check below before rolling out. In the Linux package you can run a plausibility check after the update. From the error messages named in the Omnibus merge request I derive that the bundled libruby under /opt/gitlab/embedded should contain the new error message “too many range repeat”. This is a plausibility check, not an official verification procedure.
Hardening beyond the patch
Before looking at individual measures, it is worth asking what can work against this mechanism at all. The bug sits in a parser that GitLab runs for every visitor who looks at a file, and it is triggered in exactly the process that already knows all the keys of the instance. Three things are therefore effective: fewer people who can place content and request pages, fewer ways out of the process, and less to gain from a compromised process. Anything that merely hardens the container against breakouts does not protect what GitLab itself has to read.
No public projects
The unauthenticated path needs a publicly rendered file, and GitLab itself names reducing public visibility as a stopgap until patching. Under Admin > Settings > General > Visibility and access controls, “Restricted visibility levels” lets you specify that only administrators may create public groups, projects and snippets. The documentation names two limits: the restriction does not apply to projects in personal namespaces, and a level set as the default cannot be restricted. The documentation does not say that existing public projects are switched over, so you have to find and review them yourself. For a company instance, “public” is rarely needed anyway, and “internal” covers most of what teams share with each other.
Closed sign-up
Without an account of their own, an outsider cannot get a project they may write to. Under Admin > Settings > General > New user account restrictions you disable “Allow new user accounts” or at least turn on “Require admin approval for new user accounts”, optionally combined with an allowlist of email domains. According to the documentation anyone who can reach the instance may create an account by default, and only new instances require admin approval out of the box. Older installations whose settings nobody ever touched are therefore worth checking specifically. The measure does not help against existing accounts, but it limits the circle of possible attackers to people you know.
No open exposure to the internet
The most effective measure is one that keeps unknown attackers from reaching the instance at all. In its documentation on responding to security incidents, GitLab recommends restricting inbound and outbound network access to authorised users and servers. For an instance used only by your own team, that means a VPN, zero-trust access or at least an IP allowlist in front of the load balancer. Public open-source instances naturally cannot do this. For most company instances, however, it is the measure that also helps against the next vulnerability of this family, and after four findings in one month a next one is likely.
Egress restriction for the webservice pods
After code execution an attacker usually wants to open a connection to the outside to fetch tools or exfiltrate data. For the webservice, the Helm chart offers the values networkpolicy.enabled, networkpolicy.egress.enabled and networkpolicy.egress.rules, which are false or empty by default. Once egress is enabled, the policy blocks every outbound connection without a rule. The example in the chart documentation deserves a second look: besides Gitaly on port 8075, KAS, database, Redis and DNS it also allows 0.0.0.0/0 (excluding 10.0.0.0/8) on port 443, and exactly that rule opens the door for a reverse shell again. The documentation justifies it with external object storage. Replace it with the concrete destinations your instance really needs, such as the endpoints of your object storage.
The limit of the measure is clear. Database, Redis and Gitaly have to stay reachable, and that is exactly where the valuable data lives. The restriction does not prevent access, it makes exfiltration and downloading tools harder. In the Linux package you achieve the same with a host firewall for outbound connections of the git user, which I derive as a transfer of the principle rather than from GitLab's documentation.
Secrets out of Rails' reach
The hardest part of this case is the db_key_base key. GitLab uses it to encrypt sensitive database columns, including CI/CD variables and 2FA data, and according to the backup documentation the file holding this key should therefore never sit in the same place as the encrypted data. A compromised Rails process has both. On top of that, db_key_base cannot officially be rotated, and the corresponding issue 25332 is still open. Once this key has leaked, you can replace variables and tokens, but not the key that keeps old backups decryptable. That is why I think prevention pays off more than cleanup here. Truly valuable pipeline credentials belong in an external secrets manager from which jobs fetch them at runtime, instead of living as CI/CD variables in the GitLab database.
This too has a limit you should know. Jobs usually authenticate to an external secrets manager with an ID token signed by GitLab, and the openid_connect_signing_key used for that is also in secrets.yml. What I derive from this: an attacker with this key could issue their own ID tokens. Roles in the secrets manager should therefore be bound tightly to claims such as project, branch and protected refs and grant only short-lived, narrow permissions. That does not prevent abuse, but it makes it more expensive and visible in the secrets manager's audit log.
Rate limits for anonymous requests
GitLab describes the attack as a grind with many failed attempts. Under Admin > Settings > Network > User and IP rate limits you can enable limits for unauthenticated web and API requests, which according to the documentation are off by default and start at 3600 requests per 3600 seconds once enabled. A limit slows the grind down but does not stop it, and an attacker with many IP addresses easily gets around it. I see it as a supplement, not as protection.
Measures that do not help
The Helm chart already sets sensible values for the webservice: runAsNonRoot: true, allowPrivilegeEscalation: false, capabilities.drop: ["ALL"] and a seccomp profile RuntimeDefault. These settings make container breakouts and privilege escalation harder, but against this case they are largely ineffective. The code runs in the same process that legitimately needs the Rails secrets and database credentials for its work, and exactly that data is the target. The same applies to a read-only root filesystem, which the chart documentation does not mention for the webservice anyway.
A WAF rule that blocks paths containing .org is not a mitigation either. The editor preview picks the renderer from a parameter, not from the path, and other views render files as well. The “Filtering outbound requests” setting only applies to requests made by the application itself, such as webhooks, not to processes an attacker starts via system(). You can find more on egress rules, tenant separation and network segmentation in the series TYPO3 on Kubernetes under IT-Grundschutz.
Detection and verification
The first indicator of whether you are affected is the version, which you find in the admin area or under /help on your instance. Everything before the fixed versions listed is vulnerable, and 19.4.2, 19.3.5 and 19.2.8 explicitly are too. After that, look at the attack surface: are there public projects, is sign-up open, and is the instance reachable from the internet?
To find out whether someone has tried, you can use the detection rules GitLab published on 20 October. The most important one looks in exceptions_json.log for a burst of Timeout::Error from OtherMarkup while rendering .org files in Projects::BlobController#show and fires at five such events per client within 30 minutes. Anonymous attackers appear there as ip/<address>. A second rule looks in production_json.log for calls to Projects::BlobController#preview whose file_path ends in .org although the URL names no Org file, and every hit is suspicious. Crashing workers leave an entry with [BUG] and frames from org-ruby or Onigmo in puma_stderr.log, and Workhorse logs a 502 for it while the request is missing from the Rails logs. Correlate by host and time rather than by correlation ID. GitLab points out that puma_stderr.log and the Workhorse log are often not collected centrally at all, and you should change that.
In the repositories themselves you can search for Org files with oversized #+TODO:, #+SEQ_TODO: or #+TYP_TODO: lines. GitLab gives the expression ^\#\+(TODO|SEQ_TODO|TYP_TODO):.{500,} for this. For the time after a possible success, GitLab recommends looking for new files of the git user in /tmp, unexpected child processes of Puma, outbound connections and changed crontabs or SSH keys of this user. GitLab has not named any concrete IoCs such as IP addresses, hashes or domains so far.
If you suspect a compromise, take the instance off the network and secure the logs so they can no longer be changed. GitLab's security incident documentation recommends rebuilding the host from a known-good backup or from scratch. After that, rotate all CI/CD variables, runner tokens and access tokens and review the admin accounts and, if your edition offers it, the credentials inventory. According to GitLab's detection the keys from gitlab-secrets.json should be replaced as well, but there is no official way to do that for db_key_base.
Recommendation for operators
Act now if your self-managed instance is reachable from the internet and allows public projects, especially with open sign-up. Upgrade immediately to 19.5.1, 19.4.3 or 19.3.6 and restrict visibility and sign-up until then. Afterwards, check the logs of the past weeks with GitLab's detection rules. If you upgraded on 12 October, the same applies, because that patch does not cover this vulnerability.
Monitoring is enough almost never quite holds here; the closest case is an instance reachable only via VPN, with no public projects and only known accounts. Even then the update belongs in the next few days, because the vulnerability remains code execution for every signed-in user and you only get the other eleven fixes of the release with 19.3.6 or later.
Regardless of the individual case, the same thing I say about every vulnerability applies: patches and updates belong in automation. For GitLab this means that Renovate or Dependabot propose new versions of the Linux package, the Docker image or the Helm chart as a merge request, and the pipeline tests the upgrade against a staging instance with realistic data. Then a critical patch release like this one is a matter of hours rather than weeks, even if it is the third within a month. A cooldown for routine updates protects against compromised releases, and you exempt security updates from it. The migrations in 19.3.6 and 19.4.3 or side effects of the rdoc update then show up in testing and not in production. If you update by hand, you can easily miss a few releases during the holiday season and then face an upgrade across several required stops for which nobody has a maintenance window. That is exactly how instances on 18.10 and older come about, for which there is now no patch.
Frequently asked questions about CVE-2026-110264
I upgraded on 12 October. Am I protected?+
My instance has no public projects. Do I still need to patch?+
Yes. Outsiders have a harder time reaching the vulnerability, but every signed-in user with write access can still exploit it. Patch soon, even if the pressure is lower.
Is it enough to block .org files at the reverse proxy?+
No. The editor preview picks the renderer via a parameter, not via the path. A path-based block can therefore be bypassed and does not replace the update.
Which secrets do I need to replace after a suspected compromise?+
All CI/CD variables, runner tokens and access tokens as well as the keys from gitlab-secrets.json. There is no official rotation path for db_key_base. Plan for rebuilding the instance as well.
Conclusion
CVE-2026-110264 is not the first serious bug in Ruby's regex engine that GitLab had to patch this autumn, and the pattern is the same every time. Somewhere user input becomes a regular expression, and for a C engine a regular expression is a program it executes. In this case the source had been sitting since 2014 in a gem for a niche format that hardly anyone had on their radar. The transferable lesson has two parts. Every place where input turns into patterns, templates or queries is an attack surface for code execution, even if it looks like a formatting feature. And because such places can never all be found, what counts in the end is what a compromised process can reach. A key like db_key_base that cannot be replaced turns a break-in into lasting damage. That is why it pays to keep valuable secrets away from places the web server can read in the first place.
Sources
- GitLab Critical Patch Release: 19.5.1, 19.4.3, 19.3.6, 19.2.9, 19.1.9, 19.0.10, 18.11.13
- NVD: CVE-2026-110264
- omnibus-gitlab MR !9921: Ruby regexp repeat ID overflow patch
- Ruby PR 16801: Reject overflowed repeat/null-check IDs in huge regexps
- omnibus-gitlab MR !9927: Ruby backports for 19.1
- GitLab TLDR: detections for Org rendering and Onigmo crashes
- GitLab Patch Release 19.4.2 (CVE-2026-97229)
- Rapid7: technical analysis of CVE-2026-97229
- The Hacker News: Critical GitLab Flaw (CVSS 10.0)
- GitLab Docs: Visibility and access controls
- GitLab Docs: Sign-up restrictions
- GitLab Docs: User and IP rate limits
- GitLab Helm Chart: Webservice
- GitLab Docs: Responding to security incidents
- GitLab Docs: Log system
- GitLab Docs: Back up GitLab (secrets)
- GitLab issue 25332: rotating db_key_base
- GitLab Docs: Upgrade paths
- GitLab Docs: Architecture
I review your self-managed GitLab instance for exposure, visibility and secret storage, implement egress rules and automate the rollout of GitLab patches.
GitLab audits for the Linux package and Helm, upgrade paths through the required stops, NetworkPolicies for webservice pods and incident response with secret rotation.
Platform operations instead of consulting on paper: I continuously review, patch and harden your infrastructure.
