October 23, 2026
The Patch That Wasn’t: A Perfect-10 Pre-Auth RCE in GitLab That Hides in a Regex Engine
A senior-level field briefing on CVE-2026–110264: how an unauthenticated attacker runs code on your GitLab server by uploading a text file…

By Peter Dunn
12 min read
A senior-level field briefing on CVE-2026–110264: how an unauthenticated attacker runs code on your GitLab server by uploading a text file, and why the patch you shipped nine days ago doesn't close it.
TL;DR for the on-call engineer
A single crafted Org-mode (.org) file in one public project lets an unauthenticated attacker execute code on a self-managed GitLab server as the git user. The flaw is CVSS 10.0 (CVE-2026-110264) and it reaches back to GitLab 7.0, roughly twelve years of releases. No public proof-of-concept has surfaced yet and GitLab reports no confirmed exploitation, but the entire attack is one unauthenticated file, so patch on that assumption. Upgrade self-managed instances to 19.5.1 / 19.4.3 / 19.3.6 / 19.2.9 / 19.1.9 / 19.0.10 / 18.11.13 and verify the running version. If you applied GitLab's October 12 emergency patch, you are affected again and must upgrade a second time. GitLab.com and Dedicated are already patched.
Most GitLab vulnerabilities we triage live in a feature: a broken access-control check in GraphQL, a path traversal in an export, an auth-linking edge case. CVE-2026–110264 is different, and that difference is the whole story. The bug is not in a GitLab feature. It is two layers below it, in a markup gem that turns your text into a regular expression, feeding a regex engine that miscounts. The endpoint an attacker hits can be a read-only file view in a public repo, and it still wins, because the vulnerable code runs when GitLab renders the file, before any feature logic you would think to audit.
That property, pre-auth and pre-feature, reachable by anyone who can push a file to a public project, is what moved GitLab to ship an out-of-band critical release and what should move you to patch today. GitLab holds your source, your CI/CD secrets, and your deploy keys. An unauthenticated RCE here is not a server compromise. It is a supply-chain compromise.
Executive summary: for whoever owns supply-chain risk
Here it is in one paragraph. A defect in GitLab itself, not in any pipeline or project you wrote, lets an attacker on the internet run code on your GitLab server with a single uploaded file, before any login. It affects every self-managed GitLab going back to 2014, and upgrading to the fixed release is the only complete fix. Two things make it worse than a normal critical. First, the earlier emergency patch did not hold: organizations that responded correctly to GitLab's October 12 advisory are vulnerable again and have to upgrade a second time inside two weeks. Second, the blast radius is your software supply chain, because code execution on a GitLab server hands an attacker the CI/CD variables, runner registration tokens, deploy keys, and container-registry credentials your pipelines use to ship software.
The one metric to ask your teams for is not "did we upgrade GitLab." It is "how many self-managed instances, including the forgotten mirror, the acquired team's server, and the CI box someone stood up in 2019, are confirmed running a fixed version." Those are different numbers, and the gap between them is where real compromises happen.
What actually happened, and when
The timeline matters operationally, because it is a story about a patch that had to be shipped twice.
- Oct 12, 2026: GitLab shipped a critical release (19.4.2 / 19.3.5 / 19.2.8) for CVE-2026–97229, an integer overflow in the CI/CD regular-expression compiler, the same engine reached through pipeline configuration. Security teams patched.
- Oct 21, 2026: GitLab shipped a second out-of-band critical release (19.5.1 and backports) for CVE-2026–110264, CVSS 10.0, reached through a different door: Org-mode file rendering. Crucially, the October 12 fix does not cover it. An instance patched on the 12th is exposed on the 21st.
- As of this writing: no public proof-of-concept has appeared, GitLab reports no confirmed in-the-wild exploitation, and the CVE is not yet in CISA's Known Exploited Vulnerabilities catalog. That is the calm before the storm, not the all-clear. Public analyses already describe the trigger, the attack needs no authentication, and self-managed servers are often internet-facing, so a working exploit can appear fast.
Credit for the discovery goes to a researcher using the handle joaxcar, reported through GitLab's HackerOne program. The 19.5.1 release also fixed a batch of lesser issues, among them an OmniAuth-to-LDAP account-takeover, OAuth discovery cache poisoning, and several authorization bypasses, but CVE-2026–110264 is the one that sets the clock.
Am I affected? The fix matrix
CVE-2026–110264 affects every self-managed GitLab CE and EE release from 7.0 onward, below the fixed point for each branch. Both Community and Enterprise Edition are in scope; GitLab.com and GitLab Dedicated are already patched. Three traps hide inside the version table.
The re-break. The October 12 releases (19.4.2 and siblings) fixed the other regex flaw, not this one. If your change ticket from two weeks ago says "patched," it is stale. Re-check the running version.
Bundled Ruby versus system Ruby. The real fix is a backport of a Ruby change that makes regex compilation fail cleanly when a repeat counter overflows. GitLab ships that fix inside its bundled Ruby. Omnibus and container installs get it from the upgrade. A source install that runs against the system Ruby may upgrade GitLab and still be exposed, because the vulnerable regex engine lives in a Ruby you did not touch. Check the Ruby that actually serves requests.
GitLab.com is not your instance. It is already patched and needs no action. This is entirely a self-managed problem, which means it lands on exactly the servers least likely to be in your asset list.
The bug below the bug: how a text file becomes code execution
Here is the chain, because understanding it is what lets you reason about your exposure. None of this is a working exploit; it is the shape of one.
GitLab renders .org files, Emacs Org-mode markup, to HTML using a Ruby gem called org-ruby. Org-mode lets a document redefine its own headline keywords with a #+TODO: line. When org-ruby processes headlines, it builds a regular expression from those keywords. In one place it escapes the keyword properly. In another it does not: it passes the keyword straight into Regexp.new. So text the file's author controls becomes, verbatim, a regular expression that GitLab compiles. The attacker now controls a regex running inside your server.
That regex is compiled by Onigmo, the engine Ruby uses. Onigmo assigns every counted repetition (something like {0,100}) an internal repeat ID, and stores that ID in a signed 16-bit field. Push the number of counted repeats past 32,767 and the ID wraps negative. Later, when the pattern runs, Onigmo uses that negative ID to pick a position in its match stack and writes out of bounds. From there an attacker shapes memory until that out-of-bounds write redirects execution into bytes they control, and runs commands as the account GitLab runs under, git.
The elegant, dangerous part is the reachability. The attacker does not need an account, a token, or a pipeline. They need GitLab to render the file. A single public project containing the malicious .org file is enough: GitLab renders it to HTML on view, and the chain fires. The file looks like documentation. The payload is a header line.
The attack chain, end to end
Walking the stages: an attacker crafts an .org file whose #+TODO: line defines a keyword built from a pathological counted repetition (stage 1, control the regex). They place that file in a public project on a target self-managed instance, a surface that needs no authentication (stage 2, reach the renderer). When GitLab renders the file, org-ruby passes the keyword to Regexp.new and Onigmo compiles the attacker's pattern; the repeat-ID overflow produces an out-of-bounds write at match time (stage 3, corrupt the engine). Shaped carefully, that write redirects control flow and runs commands as git (stage 4, execute). On a self-managed box this succeeds with no login and no feature interaction, which is why it carries a perfect 10.0: the scope changes from the web request to the whole host and everything the git account can read.
Blast radius: what GitLab RCE actually costs you
Code execution as git on a GitLab server is not the end of the incident. It is the beginning of a supply-chain incident, because of what lives on that host and in its database.
An attacker with code execution can read the CI/CD variables every project uses: the cloud keys, registry logins, and API tokens your pipelines depend on. They can mint or steal runner registration tokens and run code on your build fleet. They can read deploy keys and personal access tokens, clone or quietly alter source across every project, poison the container and package registries your teams pull from, and reach the secrets that let pipelines deploy to production. Each of those is a pivot off the GitLab host into the systems your software is built and shipped with. That is why "a server was compromised" undersells it: the server is the one that signs and ships everything else.
The practical consequence for incident scope is blunt. If a self-managed instance shows any sign of exploitation, treat every secret that lived in it (CI/CD variables, runner tokens, deploy keys, registry credentials, PATs) as exposed, and rotate on the assumption that the attacker read them.
What the traffic looks like (and why there is no PoC here)
This is a briefing, not a weapon, so there is no working exploit below. With a max-severity bug whose trigger is already described in public, that restraint matters. What defenders need is the shape the attack leaves behind.
The tell is Org-mode rendering that misbehaves. A legitimate .org file renders fast and quietly. An exploitation attempt drives the regex engine into pathological work, so the honest signals are rendering timeouts and native worker crashes tied to Org files, ideally several from one client in a short window against a project that has no business being rendered that often. GitLab named its own detections after exactly this: an "Org mode render error burst" and an "Onigmo native worker crash." Alert on those, and remember the hard caveat below.
Detection: hunt the render, not the payload
Because the compromise needs no file on disk and a clean hit may leave almost nothing, signature detection is insufficient. Hunt behaviour, with the two signals GitLab ships as Sigma rules, plus their Omnibus log equivalents:
- Org render timeouts. In
exceptions_json.log, look forTimeout::Errorraised inOtherMarkupon a.orgpath. GitLab's rule fires when one client triggers five or more inside thirty minutes; pivot onmeta.client_idandmeta.projectto find the source and the planted file. - Worker crashes. In
puma_stderr.log, look for a Ruby[BUG]report namingorg-ruby,headline.rb, or Onigmo internals such asmatch_atoronig_compile. That is the engine dying mid-compile.
Two caveats decide whether these rules help you. First, a successful exploit can leave no error at all: the crashes and timeouts are the attacker's failed attempts, not their win, so absence of these signals is not absence of compromise. Second, puma_stderr.log is frequently not shipped to a SIEM by default; if you are not collecting it, you cannot alert on it. Fix that before you need it. Pair both signals with the host-level rule that catches the follow-on: a GitLab worker process (Puma or Sidekiq) becoming the parent of a shell, id, whoami, or a network tool. A web worker should never spawn a shell.
Mitigation: one priority, and a workaround that does not exist
The hierarchy here is unusually clean, because GitLab offers no half-measure. Upgrading is the only fix. There is no setting that disables Org rendering on its own, and no documented configuration workaround: the vulnerable path is reached by rendering a file type GitLab supports by default. Everything else is detection and containment around an upgrade you still have to do.
Three operational notes for that upgrade. The 19.4.3 and 19.3.6 patches run database migrations that will take a single-node instance offline during the upgrade, so schedule accordingly rather than being surprised at 2am. Confirm the fix landed in the Ruby that serves requests, not just the GitLab package, because source installs on a system Ruby are the trap. And restart the workers: a patched package is not a patched process until Puma and Sidekiq are running the fixed code.
The "patched twice" playbook
This incident rewards a specific discipline, because the obvious signal, a closed change ticket, lies to you here.
- Inventory every self-managed GitLab, including mirrors, acquired-company servers, lab and CI boxes, and appliances that bundle GitLab. Prioritize anything internet-facing.
- Check the running version against the fix matrix, not the package manifest and not last month's ticket. An instance patched on October 12 is still vulnerable.
- Upgrade to the fixed release for the branch, accept the migration downtime where it applies, and restart workers.
- Verify at the process: confirm the live GitLab reports a fixed version, and on source installs confirm the Ruby regex engine actually carries the fix.
- Hunt and contain: run the Org-render and worker-crash detections across the fleet, preserve logs before remediating any hit, and rotate CI/CD secrets, runner tokens, deploy keys, and registry credentials on any instance you have reason to believe was reached.
Patching at fleet scale
On one box this is an upgrade and a worker restart. Across an organization the failure mode is different and more embarrassing: GitLab is "patched" on paper while a forgotten instance keeps serving. Self-managed GitLab sprawls. A team spun one up years ago, an acquisition brought three more, a CI mirror runs an ancient release no one owns. Those are exactly the servers missing from the asset list the patch team works from, and exactly the ones an attacker finds by scanning.
So the discipline is the single-host one, scaled: enumerate the runtimes the way an attacker will, scanning your own ranges for GitLab and querying your fleet for the GitLab package and its bundled Ruby version, then sequence by exposure. Internet-facing first, then internal, with the understanding that an internal instance is one SSRF or one phished credential away from reachable. Report in the only currency that means anything here: confirmed-fixed instances, not upgrade tickets closed.
Notes from the field
Two things stand out. First, the exposure is almost never where the inventory says it is. The vulnerable GitLab turns up on a marketing team's self-hosted server or a lab box far more often than on the production instance everyone patches first, which is exactly why enumeration beats assumption, and why "we patched GitLab" is a sentence that needs a number after it.
Second, the "patch that wasn't" is the lesson to internalize past this one CVE. Two critical regex-engine bugs in the same component in nine days, the second re-opening what the first appeared to close, with the real fix riding inside a bundled language runtime: that is a reminder that patched is not a state you reach once. It is a claim about a specific running process, on a specific instance, carrying a specific library, that you have to re-verify every time the ground shifts.
CVE-2026–110264 is a reminder that the trust boundary isn't only your code, or even your platform. Sometimes it's the gem your platform calls to turn a text file into HTML.
Field FAQ
We're on GitLab.com. Are we affected? No. GitLab.com and GitLab Dedicated are already patched. This is a self-managed problem.
We patched on October 12. Are we done? No. That release fixed a different regex flaw (CVE-2026–97229). CVE-2026–110264 was fixed on October 21. Upgrade again and verify the version.
Can we just disable Org-mode rendering as a stopgap? There is no supported setting to disable Org rendering alone, and no documented workaround. Upgrading is the only fix.
We run a source install. Anything special? Yes. The fix ships in GitLab's bundled Ruby. If your install runs against the system Ruby, a GitLab upgrade may not deliver the fix, so confirm the regex engine in the Ruby that serves requests.
Is there a public exploit yet? Not as of this writing, and no confirmed in-the-wild exploitation. Given a CVSS 10.0 bug whose trigger is a single unauthenticated file, treat that as a head start, not a reason to wait.
Do we need to rotate secrets? If an instance shows any sign of exploitation, assume everything that lived in it (CI/CD variables, runner tokens, deploy keys, registry credentials, PATs) is exposed, and rotate it. Absent evidence, prioritize patching first, then rotate on instances you have reason to believe were reached.
References
- GitLab Critical Patch Release 19.5.1: official advisory, affected and fixed versions, and the Org-render / Onigmo worker-crash detections
- Rapid7: technical analysis of the companion regex integer overflow (CVE-2026–97229)
- BleepingComputer: GitLab warns of max-severity RCE in the regex compiler engine
- Technical write-up: Org-mode rendering to the Onigmo overflow (org-ruby
#+TODO:toRegexp.new)
Written from a defender's chair. Nothing here is a working exploit; it's what you need to find and close the door before someone else walks through it.