GitLab's October 21, 2026 security release closes a CVSS 10.0 flaw that let an unauthenticated visitor run code on a self-managed instance by abusing the regular-expression compiler reachable through Org-mode rendering. The fix lands in 19.5.1, 19.4.3, 19.3.6, 19.2.9, 19.1.9, 19.0.10 and 18.11.13 for Community and Enterprise Edition, alongside nine other patched vulnerabilities ranging from CVSS 8.2 to 3.1. GitLab also published two detection rules to help self-managed operators spot exploitation attempts, because a successful compromise of the RCE can leave no obvious trace. GitLab.com already runs the patched code.
Release snapshot
The patch set covers seven version lines for both CE and EE: 19.5.1, 19.4.3, 19.3.6, 19.2.9, 19.1.9, 19.0.10 and 18.11.13. GitLab states that every self-managed deployment should move to one of these builds immediately.
GitLab Dedicated tenants on 19.3.5-internal0 or 19.4.2-internal0 and later already contain the fixes and need no action. Vulnerability issues become public in the issue tracker 90 days after the release that patched them. Routine patch releases follow a twice-monthly cadence on the second and fourth Wednesdays, but critical fixes for severe bugs ship out of band, as this one did — see the releases handbook and the security FAQ.
CVE-2026-110264: anatomy of the Org-mode regex flaw
This is the headline bug. GitLab rates it CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H — 10.0 — with scope change and full impact on confidentiality, integrity and availability. An attacker needs nothing more than network reachability: no account, no user interaction, no special position. The affected range stretches all the way back to 7.0, making this one of the broadest exposures GitLab has patched in recent memory:
- all versions 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
GitLab credits HackerOne reporter joaxcar. The fix was backported to the 18.11, 19.0, 19.1 and 19.2 lines in addition to the current releases.
Where the attacker's input lands
GitLab renders .org files through the org-ruby gem (0.9.12), reached via the markup pipeline (Gitlab::OtherMarkup / the gitlab-markup gem) under a render time budget. The gem handles custom TODO keywords in two steps. First it escapes the keywords while constructing an alternation. Then it takes the raw matched keyword and feeds it straight into a second regex:
Regexp.new("^#{keyword}\\s*")
The keyword itself comes from a #+TODO:, #+SEQ_TODO: or #+TYP_TODO: line inside the .org file. Anyone who can get GitLab to render such a file therefore controls a regexp source string — the definition of CWE-94, improper control of code generation. Because the Org renderer is reachable without authentication, a single public project holding the payload is enough to start.
What the overflow does
A keyword containing more than 32,767 generic repeats pushes Onigmo past its signed 16-bit repeat-ID ceiling. The identifier wraps to a negative value, and when the repeat opcode executes it writes outside its intended buffer (CWE-787). The result is a match-time memory corruption in the regex engine rather than in Ruby-level code.
That distinction matters operationally. Most attempts fail noisily: the render exceeds its time budget, the worker segfaults, or the write lands in unused heap and the request completes silently. With heap grooming driven by the request body, GitLab's detection documentation notes the corruption can be turned into a system() call running as the git user.
Why the blast radius is the whole instance
Code execution as git is effectively ownership of the appliance. It exposes gitlab-secrets.json, every CI/CD variable, runner registration tokens and access tokens, plus read and write access to every repository hosted on the server. GitLab's detections describe the consequence plainly: a successful CVE-2026-110264 exploit is quiet and does not reliably crash, so the absence of a crash signal never proves the instance is clean.
Detection: what GitLab's rules look for
GitLab Security released two detections for self-managed deployments, both published in the gitlab-security-oss/tldr repository. They observe different layers of the same attack and are meant to run together.
Render-timeout bursts
The Org-mode render error burst rule watches exceptions_json.log for repeated Timeout::Error entries raised by the OtherMarkup renderer on .org files. It fires when one client produces five or more of them inside 30 minutes. The relevant fields are exception.class, extra.class_name, extra.file_name, meta.caller_id, and the actor in meta.client_id / meta.remote_ip.
A single sanitized event from GitLab's documentation shows the shape:
{
"severity": "ERROR",
"time": "2026-10-16T03:02:36.429Z",
"correlation_id": "01K8QODHELP0000SYNTHETIC50",
"meta.caller_id": "Projects::BlobController#show",
"meta.feature_category": "source_code_management",
"meta.project": "example-group/pub",
"meta.client_id": "ip/203.0.113.50",
"meta.remote_ip": "203.0.113.50",
"exception.class": "Timeout::Error",
"exception.message": "execution expired",
"exception.cause_class": "Timeout::ExitException",
"user.username": null,
"extra.project_id": 42,
"extra.class_name": "OtherMarkup",
"extra.file_name": "p0.org"
}
(The event is one line in the raw log; fields are split above for readability.)
The same document includes a secondary rule for a telling evasion shape: an editor preview where the caller-supplied file_path ends in .org while the URL points at a non-Org file. GitLab picks the renderer from File.basename(file_path), so the parameter overrides the real file type. Ordinary editing rarely produces that mismatch.
Native worker crashes
The Onigmo crash rule reads puma_stderr.log and requires a [BUG] marker together with an org-ruby frame (headline.rb, regexp_helper.rb) or an Onigmo/matcher frame. The durable signature is the frame set, not the architecture, signal or addresses — GitLab notes its lab reproduction crashed on arm64-darwin with a Bus Error while production hosts on x86_64-linux typically report a Segmentation fault:
org-ruby-0.9.12/lib/org-ruby/headline.rb:100: [BUG] Segmentation fault at 0x0000000000000008
ruby 3.3.12 (2026-07-16 revision 0581089df9) [x86_64-linux]
-- Ruby level backtrace information ----------------------------------------
org-ruby/parser.rb:127:in `initialize'
org-ruby/headline.rb:100:in `parse_keywords'
org-ruby/headline.rb:100:in `sub!`
-- C level backtrace information -------------------------------------------
ruby(rb_bug_for_fatal_signal ...)
ruby(match_at+0x26f0 ...)
ruby(onig_search_gpos+0x630 ...)
ruby(rb_reg_search_set_match ...)
ruby(rb_str_sub_bang+0x140 ...)
One forensic caveat appears in both documents: the request that kills a worker never writes a Rails log line, so it carries no correlation_id. Investigators must join the crash to surrounding activity by host and time window.
Raw log queries
For operators without a SIEM, GitLab supplies direct searches. On an Omnibus install:
EXC=/var/log/gitlab/gitlab-rails/exceptions_json.log
WH=/var/log/gitlab/gitlab-workhorse/current
# Org-mode render timeouts, counted per client (burst = rendering campaign)
grep '"exception.class":"Timeout::Error"' "$EXC"* \
| grep '"extra.class_name":"OtherMarkup"' | grep -E '"extra.file_name":"[^"]*\.org"' \
| grep -o '"meta.client_id":"[^"]*"' | sort | uniq -c | sort -rn
# Preview forcing the Org renderer onto a non-Org file (any hit is notable)
PROD=/var/log/gitlab/gitlab-rails/production_json.log
grep 'Projects::BlobController#preview' "$PROD"* | grep '"file_path"' | grep -E '\.org"'
# Corroboration: Workhorse 502 on the .org blob route (the detonation, if collected)
grep -E '"uri":"[^"]*\.org' "$WH"* 2>/dev/null | grep -E '502|badgateway'
# Git side, in each suspect repository checkout: oversized #+TODO: keyword lines in .org files
rg -l --glob '**/*.org' '^\#\+(TODO|SEQ_TODO|TYP_TODO):.{500,}'
The crash-side companion query:
PUMA=/var/log/gitlab/puma/puma_stderr.log
SIDEKIQ=/var/log/gitlab/sidekiq/current
# Native [BUG] crashes whose backtrace references org-ruby or the Onigmo engine
grep -B1 -A20 '\[BUG\]' "$PUMA"* \
| grep -E \
-e 'org-ruby|headline\.rb' \
-e 'regexp_helper\.rb|regexp_matcher\.rb' \
-e 'match_at|onig_search_gpos' \
-e 'onig_compile|compile_tree' \
-e 'compile_length_tree|rb_str_sub_bang'
# Any [BUG] / segfault in the worker error log (broader, lower fidelity)
grep -E '\[BUG\]|Segmentation fault|Bus Error' "$PUMA"*
# Pipeline vector: Sidekiq cluster kill after a hostile .gitlab-ci.yml push
grep 'A worker terminated, shutting down the cluster' "$SIDEKIQ"*
The same detection material references companion CI-path flaws, CVE-2026-93577 and CVE-2026-97229, where a crafted rules:*:regexp pattern produces a program larger than INT_MAX and wraps a signed 32-bit jump offset (CWE-190). Those were fixed in 19.4.2, 19.3.5 and 19.2.8 according to that documentation.
If a rule fires, treat it as attempted RCE. Pull the named .org files, identify who committed them, and hunt for execution artifacts: unexpected child processes under Puma or Sidekiq, new files owned by git in /tmp, outbound connections, and modified crontabs or SSH keys for the git user. On confirmed compromise, rotate the contents of gitlab-secrets.json, every CI/CD variable, all runner tokens and all access tokens.
The rest of the patch set
Beyond the 10.0 RCE, the advisory covers nine more issues. Unless noted, each applies to CE and EE, and all of them are fixed in 19.3.6, 19.4.3 and 19.5.1.
Account takeover through OmniAuth/LDAP linking (8.2)
CVE-2026-15723, CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:N, affects versions from 10.1. With OmniAuth SAML and LDAP auto-linking both enabled, a logged-in attacker could seize an existing LDAP-backed account without the victim doing anything, exploiting weak sanitization in the LDAP email lookup used during identity linking. Reported by stealthy.
OAuth/OIDC discovery cache poisoning (7.5)
CVE-2026-112212, CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:L/A:L, reaches back to 18.3. An unauthenticated party could poison a shared cache's discovery document so that selected flows pointed at a server they controlled, because cached endpoint URLs trusted user-supplied values. Found internally by GitLab's Peter Arts.
Token scope escape through GraphQL (6.8)
CVE-2026-12664, CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N, affects versions from 18.2. A token limited to read_api could read a project's incoming email address and create work items — operations the scope should forbid — thanks to a GraphQL field that did not enforce scope. Reported by go7f0.
Gaps in the SAML/LDAP membership lock (5.9)
CVE-2026-16255, CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:L/A:N, applies to EE from 12.0. With the instance-wide SAML or LDAP membership lock switched on, requests created before the lock took effect were not fully constrained, letting an authenticated user grant or accept project and group membership outside the identity provider's authority. Reported by 3nvz.
Lower-severity fixes
| CVE | Issue | CVSS | Affected from | Reporter |
|---|---|---|---|---|
| CVE-2026-105777 | Internal credentials leaked via response headers (AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N) | 4.3 | 17.6.5 | altair00 |
| CVE-2026-13319 | Group milestones API authorization bypass exposing private project milestone metadata (AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N) | 4.3 | 15.10 | a0xnirudh |
| CVE-2026-112378 | EE: AI Gateway enablement header bypasses billing checks (AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N) | 4.3 | 17.8 | Halil Coban (internal) |
| CVE-2026-105711 | Fragment cache leaks confidential issue titles across viewers (AV:N/AC:L/PR:L/UI:R/S:U/C:L/I:N/A:N) | 3.5 | 15.0 | 20kilograma |
| CVE-2026-14288 | EE: merge request approval policy bypass (AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:N) | 3.1 | 17.4 | 3nvz |
The advisory also names CVE-2026-112326, described as an improper-cleanup-on-thrown-exception issue in security findings affecting GitLab EE. The published text ends before its scoring and version range.
Upgrading and containment
The primary mitigation is simply to install one of the fixed builds. The patched lines are 19.5.1, 19.4.3, 19.3.6, 19.2.9, 19.1.9, 19.0.10 and 18.11.13; staying on an older minor means picking the corresponding backport. For an Omnibus deployment on Debian or Ubuntu, the mechanics look like this (confirm the package name matches your edition and follow the official upgrade guide for your platform):
sudo apt-get update
sudo apt-get install gitlab-ce
sudo gitlab-ctl reconfigure
Two operational notes are worth repeating. First, confirm that your log pipeline actually ships puma_stderr.log and exceptions_json.log — native crash reports are frequently left out of default collection, which blinds the crash rule entirely. Second, keep the render-burst, Workhorse 502 and native-crash signals correlated by host and time; a crash plus a render burst from the same origin is close to conclusive.
Until the upgrade completes, reducing public exposure limits the unauthenticated Org path, since the attack requires a .org file that GitLab will render. GitLab's own hardening guidance is collected in its instance security best practices post, and the release history is indexed at docs.gitlab.com/releases.
References
- CVE-2026-110264 — NVD / CVE.org record
- CVE-2026-15723 — CVE.org
- CVE-2026-112212 — CVE.org
- CVE-2026-12664 — CVE.org
- CVE-2026-16255 — CVE.org
- GitLab detection: Onigmo native worker crash
- GitLab detection: Org-mode render error burst
- GitLab releases handbook / security FAQ
- GitLab vulnerability issue tracker