The 10.0 Lives in a Gem From 2014: GitLab’s Org-Mode RCE and the Fix Its Gemfile Forbids

On 21 October 2026 GitLab shipped a critical patch release — 19.5.1, 19.4.3, 19.3.6, 19.2.9, 19.1.9, 19.0.10 and 18.11.13 — carrying twelve CVEs. The headline is CVE-2026-110264, rated CVSS 10.0 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H), reported by joaxcar through GitLab’s HackerOne programme. The affected range is the widest we have seen from this vendor: “all versions from 7.0 before 18.11.13” and every later line up to 19.5. GitLab backported the fix to four lines beyond its standard patch set, which it does rarely and only under pressure.

The advisory itself is one sentence: the flaw “could have allowed an unauthenticated user to execute arbitrary code on the GitLab server due to integer overflow when compiling a specially crafted regular expression.” Read on its own, that sounds like a repeat of the CI/CD regex compiler bugs this site covered in September and again on 13 October. It is not. GitLab published, in the same release, two open-source detection rules that describe a different bug in a different component through a different mechanism — and those documents are far more candid than the advisory they accompany.

What GitLab’s own detections say

The two rules live in GitLab’s public gitlab-security-oss/tldr detection repository, and we read both first-hand. They separate the October CVEs into two distinct families. The CI path — CVE-2026-93577 and its bypass CVE-2026-97229 — is a compile-time overflow: a crafted rules:*:regexp pattern compiles to a program larger than INT_MAX (2 GiB), wrapping a signed 32-bit jump offset negative (CWE-190). The Org-mode path — CVE-2026-110264 — is something else entirely:

“the org-ruby gem interpolates a raw attacker-supplied #+TODO: keyword into Regexp.new, and a keyword with more than 32,767 generic repeats makes Onigmo assign a repeat ID past the signed 16-bit limit, causing an out-of-bounds write at match time.”

That is CWE-94 plus CWE-787, not CWE-190, and it fires at match time rather than compile time. GitLab’s detection text states the preconditions plainly: the attack needs “no authentication — only one public project that can hold the .org payload,” and with heap grooming driven by the request body it “becomes a system() call as the git user.” The business-impact section names what follows: theft of gitlab-secrets.json, every CI/CD variable, runner tokens and access tokens, and read-write access to every repository on the instance.

One line in the crash-detection rule deserves to be read by anyone planning to rely on telemetry instead of patching: “a successful CVE-2026-110264 exploit is quiet and does not reliably crash.” GitLab says explicitly that the absence of a detection hit “never clears an attempt or a success.” The rules are corroborating evidence for a noisy grinding campaign, not an assurance of safety.

The sink is a dependency nobody has touched since 2014

We traced the component GitLab names. org-ruby is an MIT-licensed gem by Waldemar Quevedo that renders Emacs Org-mode files; GitLab reaches it through Gitlab::OtherMarkup and the gitlab-markup gem whenever an instance displays a .org blob. The vulnerable code is in lib/org-ruby/headline.rb, and we read it first-hand at the version-0.9.12 tag. Line 100:

@headline_text.sub!(Regexp.new("^#{@keyword}\s*"), "")

@keyword is taken from a #+TODO: (or #+SEQ_TODO: / #+TYP_TODO:) declaration in the file being rendered. The gem escapes those keywords when it assembles its matching alternation, then interpolates the raw matched keyword into a second expression without escaping it. An attacker who can get GitLab to render a .org file therefore controls a regular-expression source string.

Version 0.9.12 was published on 23 December 2014 and has been downloaded 37,185,895 times. Between that release and this month, the gem shipped nothing at all. GitLab’s affected range beginning at version 7.0 is not a coincidence of versioning; it is the span of time this one line has been sitting in the dependency tree.

Upstream fixed it twelve days early, and told nobody

On 9 October 2026 — twelve days before GitLab’s advisory — the gem woke up. Three commits modernised it for current Ruby and adopted GitHub Actions, 0.10.0 was tagged at 18:16Z, and then pull request #112, authored by a contributor using the handle mockra and titled “Escape custom TODO keywords,” merged at 19:54:22Z. 0.10.1 followed on RubyGems at 20:02:38Z. The fix is one line:

- @headline_text.sub!(Regexp.new("^#{@keyword}\s*"), "")
+ @headline_text.sub!(Regexp.new("^#{Regexp.escape(@keyword)}\s*"), "")

The same commit adds two specs. One is named “should treat regexp metacharacters in a matched keyword as literal text.” The other is named “should not compile repeat expressions from a matched keyword” and drives the parser with the keyword REPEAT{1,1000000000} — a generic repeat count a thousand times past the signed 16-bit boundary GitLab’s detection document describes. The test that proves the maximum-severity bug is in the repository, written in the language of a parsing nicety.

Nothing was labelled security. There is no GitHub security advisory on the repository — the advisories endpoint returns an empty list. There is no CVE for the gem; CVE-2026-110264 is assigned to GitLab CE/EE, not to org-ruby. And an OSV query for org-ruby in the RubyGems ecosystem returns {} — zero records. Every other Ruby application that renders Org-mode files through this gem is, as of today, running the vulnerable line with no CVE, no advisory, no OSV entry and therefore no scanner alert of any kind. Uptake of the fixed release reflects that: 0.10.1 has been downloaded 1,048 times against 0.9.12’s 37.1 million.

GitLab 19.5.1 still pins the vulnerable gem

This is the part that matters most for anyone auditing the patch. We read the Gemfile and Gemfile.lock at tag v19.5.1-ee — the release that fixes the 10.0. The declaration is:

gem 'org-ruby', '~> 0.9.12', feature_category: :markdown

and the lockfile resolves org-ruby (0.9.12). The pessimistic constraint ~> 0.9.12 permits 0.9.x and nothing above it, so the upstream fix in 0.10.1 is not merely unapplied — it is structurally unreachable without editing the Gemfile. The backport release v18.11.13-ee ships the same pinned 0.9.12 with the stock RubyGems checksum, so no privately patched build was substituted either.

The release does bump gitlab-markup from 2.1.0 to 2.2.0, which looks like it could be the vehicle. It is not. We downloaded both gems and diffed them: the only changes are an RDoc 8 keyword-argument compatibility fix in rdoc.rb and the version constant. There is no security content in that bump.

We then compared the public tags v19.5.0-ee and v19.5.1-ee — 87 commits across 172 files. Eleven of the twelve CVEs are plainly visible as merge branches named for them: security-oauth_discovery_config_host, security-ldap-email-filter-escape, security-saml-ldap-membership-lock, security-597022-workhorse-senddata-header-leak, security-604064-milestones-idor, security-631564-amazon-q-auto-review-admin-lock and the rest. There is no branch for the Org-mode RCE, and no change to org-ruby anywhere in the diff. The one maximum-severity fix in the release is the one that is not legible in the public tree.

We do not claim GitLab shipped the release unfixed, and we have no evidence of that. GitLab’s stated policy, quoted in this very advisory, is that “the issues detailing each vulnerability are made public on our issue tracker 90 days after the release in which they were patched” — and withholding the patch commit for an unauthenticated 10.0 with published detections is a defensible call while the world upgrades. What we do state, because it is directly observable, is this: the vulnerable gem version is still what GitLab installs, the constraint in the Gemfile forbids the fixed version, and so whatever mitigation 19.5.1 applies sits in front of the sink rather than removing it. Defenders auditing by SBOM or dependency scan will see org-ruby 0.9.12 on a patched instance and should expect that, not treat it as a failed upgrade.

One further detail from the same diff is worth recording. A security branch named security-603568-push-rule-bypass-git-notes-19-5, carrying a commit titled “Validate push checks on all ref types,” was merged into the 19.5 stable branch and then reverted before release. No push-rule CVE appears in the advisory, which is consistent with a fix that was pulled rather than shipped. Instances relying on push rules to constrain what reaches a repository should not assume that gap closed in this release.

The AI-adjacent fixes in the same batch

Two of the remaining eleven sit in this site’s usual territory. CVE-2026-103515 (3.1) is an access-control failure in group Duo admin-lock enforcement for Amazon Q settings: where an instance administrator had locked a group’s Duo availability, a group Owner could “re-enable AI-backed data egress to a third-party service for that group and its descendant projects.” The admin lock is the control that is supposed to stop repository content leaving for a model provider, and it was enforceable at the wrong granularity. CVE-2026-112378 (4.3) lets an authenticated user reach paid AI features without a valid subscription by way of an AI Gateway feature-enablement header trusted from the client. Both are low-scored and neither is the story of this release, but together they continue the pattern we traced through the AI Gateway sandbox escape: GitLab’s AI governance surfaces keep taking security decisions from values the caller supplies.

What to do

  • Upgrade now, and upgrade even very old instances. The 10.0 is unauthenticated, scope-changed, and reaches back to 7.0. GitLab backported to 18.11.13, 19.0.10, 19.1.9 and 19.2.9 precisely so that long-lived installations have a path; there is no version old enough to be out of scope.
  • If you cannot patch immediately, consider whether your instance needs to render .org files at all. The attack requires only one project whose contents GitLab will render through the Org pipeline. Org-mode rendering is a feature most instances never knowingly use.
  • Deploy GitLab’s detections, but do not treat them as clearance. The render-timeout rule keys on five or more Timeout::Error exceptions from the OtherMarkup renderer on a .org file from a single client within 30 minutes, in exceptions_json.log. Confirm your pipeline actually ships exceptions_json.log and puma_stderr.log — native crash reports are frequently not collected by default. GitLab states a successful exploit may be silent.
  • Watch the editor-preview variant. GitLab’s secondary rule covers a #preview request whose caller-supplied file_path ends in .org while the URL path is a different file — the renderer is chosen from the parameter, not the real file. GitLab describes this shape as having a zero baseline, so any hit is notable.
  • If you ship Ruby that renders Org-mode content outside GitLab, you are on your own. Move to org-ruby 0.10.1 by hand. No CVE, no GHSA and no OSV record exists for the gem, so nothing in your scanning stack will raise this for you.
  • Expect org-ruby 0.9.12 in your SBOM after patching. It is pinned by GitLab’s own Gemfile constraint and its presence is not evidence that the upgrade failed.

Verification note. The release date, the seven patched versions, the twelve-CVE table, CVE-2026-110264’s CVSS 10.0 score and vector, its affected-version ranges, the backport statement, the joaxcar credit, the 90-day issue-tracker policy and all quoted advisory text were read first-hand from the GitLab Docs patch-release page. The mechanism description, CWE references, the system()-as-git outcome, the “no authentication — only one public project” precondition, the “quiet and does not reliably crash” caveat, the log fields and the editor-preview variant were read first-hand from GitLab’s public gitlab-security-oss/tldr detection repository. The org-ruby release dates and download counts come from the RubyGems API; the vulnerable line at headline.rb, the Regexp.escape fix commit, its pull-request title and author handle, the two added spec names and the REPEAT{1,1000000000} test input were read first-hand from the upstream repository at the relevant tags. The empty GitHub security-advisory list and the empty OSV response were retrieved directly from those APIs. The Gemfile declaration, both lockfiles, the gitlab-markup gem-to-gem diff, and the 87-commit tag comparison including the absent Org-mode security branch and the reverted push-rule branch were retrieved first-hand from the GitLab API and RubyGems. CVE-2026-110264 had no NVD record at the time of writing; CVE-2026-97229 (published 13 October, Analyzed) and CVE-2026-93577 (9.9, published 24 September, Analyzed) were read from the NVD 2.0 API. No GitLab instance was run, no exploit was written or attempted, and no request was sent to any third-party deployment. The reading that the fix sits in front of the sink rather than in the gem is our analysis of the observable artefacts, not a vendor statement. We have no evidence of exploitation in the wild and imply none.

Sources: