Sources: Rapid7 Technical Analysis | GitLab 19.4.2 Patch Release | NVD


CVE-2026-97229 Overview
I saw CVE-2026-97229 pop up on the CISA KEV list last week and flagged it immediately. An unauthenticated RCE in GitLab's CI/CD pipeline that scores a perfect 10.0 and runs inside every self-hosted GitLab CE/EE instance – that's a bug you patch before coffee, not after. Rapid7's Jonah Burgess released a public PoC on the same day as the disclosure, and CISA confirmed active exploitation within two weeks. This post walks through the root cause, lab reproduction of the attack primitives, detection rules from GitLab's advisory, and the exact fixed builds to deploy. Earlier this year I covered a similar RCE chain in WordPress core and the PHP2Shell KEV series – the pattern of a single overflow leading to remote code execution is common across all these cases.
| Property | Value |
|---|---|
| CVE | CVE-2026-97229 |
| CVSS | **10.0 (Critical)** CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H |
| CWE | CWE-190: Integer Overflow or Wraparound |
| Attack Vector | Network, Unauthenticated RCE via crafted CI/CD regex |
| Affected | GitLab CE/EE 19.2.x, 19.3.x, 19.4.x (before patch) |
| Fixed | 19.2.8, 19.3.5, 19.4.2 |
| CISA KEV | Yes, added 2026-10-29 (due 2026-11-01); exploit code available |
| EPSS | 27.30% (98th percentile) |
| Discovered by | joaxcar (via HackerOne) |
| PoC Author | Jonah Burgess (Rapid7) |
Technical Deep Dive
The vulnerability lives in Ruby’s Onigmo regular-expression engine, bundled with GitLab. When GitLab processes a project’s .gitlab-ci.yml file, it compiles any regex patterns used in rules:exists:regexp or rules:changes:regexp. An attacker who can push to a public project supplies two things: a CI configuration containing a regex with an inline comment that hides a massive repeat count ({5(?#)7805} – Onigmo sees {57805}, GitLab’s scanner sees {5}), and a repository path with four-byte UTF-8 characters. When GitLab’s CI lint endpoint (unauthenticated after setup) evaluates the configuration, Ruby recompiles the regex for the UTF-8 subject encoding, which expands POSIX character classes such as [[:word:]] into large Unicode range tables. The product of the expanded body (~37 KB) and the hidden repeat count (57,805) crosses INT_MAX. Onigmo’s signed comparison tlen * upper <= 1000 wraps to true, and the compiler emits 2.15 GiB of pcode. The wrapped branch offset lands inside a controlled character-class bitmap, which redirects the Onigmo VM stack into the repository path and eventually reaches a system() call site in libruby.
Root Cause Analysis
The bug sits in Onigmo's compile_quantifier_node() in regcomp.c. The check at Rapid7's annotation [12] uses a signed integer multiplication:
(tlen + SIZE_OP_PUSH) * qn->upper <= QUANTIFIER_EXPAND_LIMIT_SIZE (1000)When the body is 37,151 bytes under UTF-8 and the repeat count is 57,805, the multiplication wraps past INT_MAX and the result is negative - which satisfies <= 1000. Onigmo then enters the expansion path and emits all 57,805 copies. The patched version substitutes a division-based check that cannot overflow:
#define IS_EXPAND_LIMIT_OK(tlen, n) \
((n) <= 0 || (tlen) <= QUANTIFIER_EXPAND_LIMIT_SIZE / (n))
#define MAX_COMPILED_PROGRAM_SIZE (INT_MAX_LIMIT / 4)Lab Verification
The scanner bypass is straightforward - (?#) is an Onigmo inline comment that GitLab's repeat scanner does not recognise:
import re
# GitLab's repeat scanner (simplified)
REPEAT_BOUND = re.compile(r'(?
# Attacker pattern with inline comment hiding the real count
pattern = r'\A(?:[[:word:]]){5(?#)7805}|)\b\B'
matches = REPEAT_BOUND.findall(pattern)
print('Scanner matches:', matches)
if not matches or all(int(lo) <= 1000 for lo, _ in matches):
print('-> PASS: no bound > 1000 detected')
# Confirm Onigmo reads {57805} via equivalence test
import subprocess
r = subprocess.run(['ruby', '-e',
'puts /a{2(?#)5}/.match?("a"*25) && !/a{2(?#)5}/.match?("a"*24)'],
capture_output=True, text=True)
print('Bypass confirmed:', r.stdout.strip() == 'true')The integer overflow arithmetic was verified in C:
=== CVE-2026-97229: Integer Overflow Demo ===
Body size (tlen): 37151
Repeat bound (upper): 57805
SIZE_OP_PUSH: 9
Product = (37151+9)*57805 = -2146933496
As hex: 0x80086508
[VULNERABLE] Product -2146933496 <= 1000
-> expansion PATH entered
(Negative product passes <= limit guard)
[PATCHED] Expansion REJECTED:
tlen (37151) > LIMIT/upper (1000/57805 = 0)The same pattern was verified in Ruby 3.3.8: {2(?#)5} behaves identically to {25}, confirming Onigmo reads the full bound through the inline comment. The UTF-8 recompilation was triggered by matching against a 4-byte emoji string - VmPeak remained flat for small patterns, but extrapolating from a single [[:word:]] class at ~6.2 KB per copy under UTF-8, the full 57,805-repeat compaction would require 2.15 GiB of pcode.
Note: End-to-end execution against the gitlab/gitlab-ce:19.4.1-ce.0 Docker image requires ~9 GB of RAM per Rapid7's findings. The lab sandbox had 3 GB, so I verified the root-cause primitives (scanner bypass, integer overflow arithmetic, UTF-8 recompilation) on the local toolchain. The reproduction data is synthetic and uses RFC 5737 TEST-NET ranges.
Detection and Defence
GitLab shipped six native threat detections alongside the patch. On a self-managed instance, the loudest signal is a Puma worker crash accompanied by GET /api/v4/projects/*/ci/lint?content_ref=* in the Workhorse access log followed by a 502 and an EOF. A 2-GiB memory jump in a Puma worker that normally lives under 500 MB is a strong secondary indicator.
| Layer | Detection Signal |
|---|---|
| Workhorse logs | /var/log/gitlab/gitlab-workhorse/current: GET .../ci/lint?content_ref= + 502 Bad Gateway |
| Puma worker crash | Worker restart + stack trace containing regexp_matcher.rb |
| GitLab Threat Detections | 6 built-in detections (see GitLab advisory) |
| Memory anomaly | ~2 GiB worker-memory spike during lint evaluation |
| Repo forensic | Commit with (?#)-split repeat + repeated [[:word:]] + 4-byte UTF-8 path |
Mitigation
Self-managed GitLab instances must upgrade to one of the fixed builds. GitLab.com and GitLab Dedicated were patched before disclosure. As a temporary workaround, restrict credential-free access to attacker-writable public projects or block the GET /api/v4/projects/*/ci/lint route at your reverse proxy. Route controls do not fix the vulnerable Onigmo compiler - patch the instance as soon as possible.
| Version | Fixed Build |
|---|---|
| 19.2.x | 19.2.8 |
| 19.3.x | 19.3.5 |
| 19.4.x | 19.4.2 |
CVE-2026-97229 is a sobering example of how a single integer overflow deep inside a regex compiler can turn a developer workflow into a remote root shell. If you run a self-hosted GitLab, check your version now and upgrade to 19.4.2 (or 19.3.5/19.2.8) today. The KEV deadline for this CVE passed on 1 November.