1. Executive summary
GitLab released 19.4.2, 19.3.5 and 19.2.8 on 2026-10-12 to fix CVE-2026-97229, an integer overflow in the CI/CD regular-expression compiler that Rapid7 reproduced end to end as unauthenticated remote code execution on the official gitlab/gitlab-ce:19.4.1-ce.0 Linux amd64 image with ASLR enabled. The verified reference data records CVSS 10.0 CRITICAL, CWE-190, EPSS 1%, and no CISA KEV listing; GitLab's own advisory and Rapid7 both report no confirmed in-the-wild exploitation at disclosure. Self-managed GitLab CE and EE instances from 19.2 before 19.2.8, 19.3 before 19.3.5 and 19.4 before 19.4.2 are affected across all deployment types; GitLab.com was patched before disclosure and GitLab Dedicated customers need no action. For EMEA financial services the exposure is concentrated in self-managed GitLab used for CI/CD on internal networks, where a successful exploit yields command execution as the git operating-system account inside a Puma worker, and where Rapid7's demonstrated route requires only that an attacker can plant a CI configuration and a crafted path in a public project.
2. Regulatory framing
| Article | Trigger (the fact in this item) | Practical impact |
|---|---|---|
| DORA Art. 28: ICT third-party risk — general principles | Affected firms run self-managed GitLab CE/EE as an ICT service supporting software delivery, and the fix is a vendor patch that must be applied on the firm's own infrastructure rather than by the provider. | The patching obligation and the residual risk sit with the firm, not the vendor. Track the upgrade to 19.2.8/19.3.5/19.4.2 as a third-party risk action with an owner and a date. |
| DORA Art. 24: digital operational resilience testing — general requirements | Rapid7 reproduced the exploit against an official image, so the flaw is testable in a controlled environment; firms cannot rely on the absence of in-the-wild exploitation as evidence of resilience. | Where GitLab is in scope for resilience testing, the crafted-regex path is a candidate test case. Confirm the test environment is isolated before running anything derived from the PoC. |
| NIS2 Art. 21(2)(d): supply chain security measures | The vulnerability is in a third-party CI/CD component that firms deploy themselves, and the demonstrated delivery path depends on who can create or write to public projects on that instance. | Review who can create public projects and push branches on self-managed GitLab. This is a supply chain control on the software delivery toolchain, not only a patch task. |
3. Technical analysis & attack chain
CVE-2026-97229 is an integer overflow (CWE-190) in GitLab's CI/CD regular-expression handling. The verified reference data records CVSS 10.0 CRITICAL, EPSS 1%, and no CISA KEV entry. GitLab assigned the same 10.0 score and described possible code execution by an unauthenticated user. Rapid7's technical analysis covers one end-to-end route and states plainly that it begins after an attacker has pushed both the malicious CI configuration and a specially named path to a public project, so only the final CI lint request is unauthenticated. Rapid7 notes that this prerequisite conflicts with GitLab's PR:N rating and that, because the underlying HackerOne report by joaxcar remains private, it does not know how the reported attack avoids the prerequisite. Treat the unauthenticated rating as the vendor's position and the demonstrated route as requiring prior write access.
Confirmed steps in the demonstrated chain:
- The attacker commits a malicious
.gitlab-ci.ymland a repository path containing valid four-byte UTF-8 characters to a branch in a public project. On instances where ordinary accounts can create public projects, this is the only access required. - The crafted expression is placed on a conditional include, for example
include: - local: /benign.ymlwithrules: - exists: regexp: "ATTACKER_PATTERN". GitLab supports regex matching over repository paths inrules:exists:regexpandrules:changes:regexp. RegexpMatcherexpands the expression, compiles it with Ruby's native regex engine, and tests it against every filename in the selected commit. The source-length check runs before compilation; the match subject is the repository path.- The quantifier bound is hidden from GitLab's repeat scanner by an inline regex comment:
{5(?#)7805}. Onigmo ignores(?#)and reads the bound as 57,805, while the scanner atREPEAT_BOUNDrecognises only contiguous digits and does not see a bound above 1,000. - The checked expression is only 986 bytes, but six
[[:word:]]classes expand when matching begins against a path containing valid four-byte UTF-8. Ruby recompiles the same source for UTF-8 viaonig_new_without_allocbefore the timed matcher starts, so the unsafe compilation sits outside the 50-millisecond timeout. GitLab never checks the size of this new program. - Under UTF-8 one copy of the repeated body is 37,151 bytes. With a bound of 57,805 the size calculation is 37,151 × 57,805 = 2,147,513,555 = 0x800074d3, which is 29,908 bytes (0x74D4) beyond signed INT_MAX. The negative result satisfies Onigmo's "small enough to expand" test in
compile_quantifier_node(), and the compiler emits all 57,805 copies. - The
|immediately after the quantifier makes the repeated body the first alternative and leaves the second empty. TheOP_JUMPemitted after the giant first alternative is calculated from the already wrapped length: it is emitted at offset 2,147,513,698 with a displacement of -136, sending execution into the first payload bitmap in the last expanded copy, 140 bytes from the end of the pcode. - The payload sits in four further character classes generated by
character_class(stage). A class such as[abc]compiles to a bitmap with bits set for a, b and c; the helper works backwards from each desired pcode sequence and picks class members whose bitmap reproduces those bytes, placing attacker-controlled VM instructions at the end of every expanded copy. - The controlled pcode redirects Onigmo's backtracking stack into the repository path. On a failed match the VM restores the next instruction from the path and
FinishCode, a pointer from its bottom stack frame. The path instruction sends control to the final controlled bitmap, which adjusts the recovered libruby pointer to a short sequence that passes the path command tosystem()and writes that address intomatch_at()'s native return slot.FinishCodeand the call site are in the same libruby mapping, so the distance between them stays constant under ASLR. - The trigger is the CI lint API.
GET /api/v4/projects/<project>/ci/lint?content_ref=<ref>checks read authorization, resolves the supplied ref, loads.gitlab-ci.ymlfrom that commit and runs the static linter. Public visibility makesauthorize_read_code!succeed without credentials, and the token check does not reject an absentcurrent_user. No pipeline is created and no runner is contacted. - The command runs synchronously in a Puma worker as the
gitaccount (Rapid7's PoC output showsuid=998(git) gid=998(git)). Aftersystem()returns, the corrupted worker crashes; GitLab Workhorse sees the Rails backend connection close and returns HTTP 502, and Puma replaces the worker. The callback output, not the HTTP status, is the success signal.
Resource requirements and target specificity matter for triage. The exploit needs a little over 2 GiB in the worker, and Rapid7's complete single-node target needed about 9 GiB available; low-memory instances may swap, reject the allocation, or invoke the OOM killer. The target-specific values in the PoC, including SYSTEM_STEP_COUNT, match official GitLab CE 19.4.1 on Linux amd64; other vulnerable packages or architectures need adjusted pcode and libruby offsets. Rapid7 validated the chain against 19.4.1 CE with ASLR enabled and the same input did not execute against the patched 19.4.2 CE image.
The vendor fix replaces the multiplication in compile_quantifier_node() with a division-based test that cannot overflow (IS_EXPAND_LIMIT_OK), and rejects compiled programs above roughly 512 MiB via MAX_COMPILED_PROGRAM_SIZE (INT_MAX_LIMIT / 4). On the patched image the crafted quantifier remains a compact loop and the request returns a normal false match. A repeat-range double-free fixed in the same release affects an error path and is not part of this chain.
Confidence caveats. The end-to-end route is single-sourced to Rapid7; the underlying HackerOne report is private and GitLab's release notes do not describe the mechanism. The claim that exploitation is unauthenticated rests on GitLab's CVSS rating and on the lint endpoint's authorization logic as reproduced by Rapid7, not on an observed attack. No exploitation in the wild was reported at disclosure by either GitLab or Rapid7, and the verified reference data shows no CISA KEV listing. The EPSS value of 1% is consistent with that picture.
4. Mitigation & containment
P1, within 24 hours.
- Upgrade every self-managed GitLab CE/EE instance to the fixed patch for its release line: 19.2.8, 19.3.5 or 19.4.2. This is the only action that repairs the vulnerable Ruby compiler. Rapid7 recommends upgrading outside normal patch cycles.
- Identify instances that permit ordinary accounts to create public projects, or that allow anonymous read of public projects. On those instances the demonstrated prerequisite is satisfied by any account holder, and the lint route is reachable without credentials.
- If an instance cannot be patched immediately, restrict credential-free access to attacker-writable public projects and block the CI lint route. Rapid7 is explicit that these controls address only the demonstrated delivery path and do not remediate the vulnerable compiler, so treat them as hours-long containment, not a fix.
P2, within 72 hours.
- Search GitLab logs for the exploitation pattern. In
/var/log/gitlab/gitlab-workhorse/current, look for a several-secondGET /api/v4/projects/<project>/ci/lint?content_ref=<ref>, followed bybadgateway: failed to receive response: EOFand HTTP 502. Correlate with a Puma worker crash or restart at the same time; any saved trace containingregexp_matcher.rbstrengthens the match. - Review recent commits to public projects for the referenced pattern: an
(?#)-split repeat, repeated POSIX word classes, and a path containing four-byte UTF-8 characters. A shell child process or a roughly 2 GiB worker-memory increase strengthens the match. Rapid7 notes the undisclosed path may differ from the one it demonstrated. - Confirm GitLab.com and GitLab Dedicated are out of scope for your estate. GitLab.com was patched before disclosure and Dedicated customers do not need to act.
P3, within 7 days.
- Where GitLab is in scope for resilience testing under DORA Art. 24, add the crafted-regex path as a test case in an isolated environment. Do not run PoC-derived input against production.
- Record the upgrade as a third-party risk action under DORA Art. 28, with the version applied and the date, so the residual risk is documented rather than assumed closed by the vendor advisory.
5. Indicators of compromise
No atomic indicators (hashes, domains, IPs, file paths of dropped artefacts) are present in the source material. The sources describe observable behaviours and log patterns instead.
| type | value | confidence | source |
|---|---|---|---|
| behaviour | Several-second GET /api/v4/projects/<project>/ci/lint?content_ref=<ref> in GitLab Workhorse logs |
medium | Rapid7 technical analysis |
| behaviour | badgateway: failed to receive response: EOF followed by HTTP 502 on the lint request |
medium | Rapid7 technical analysis |
| behaviour | Puma worker crash or restart coincident with the lint request | medium | Rapid7 technical analysis |
| behaviour | Saved trace containing regexp_matcher.rb |
medium | Rapid7 technical analysis |
| behaviour | Committed CI configuration containing an (?#)-split repeat and repeated POSIX word classes |
medium | Rapid7 technical analysis |
| behaviour | Repository path containing four-byte UTF-8 characters | medium | Rapid7 technical analysis |
| behaviour | Shell child process spawned by a Puma worker, or roughly 2 GiB worker-memory increase | medium | Rapid7 technical analysis |
Behavioural indicators
| behaviour | where to observe | confidence |
|---|---|---|
Lint request with attacker-selected content_ref taking several seconds and ending in 502 |
/var/log/gitlab/gitlab-workhorse/current |
medium |
| Puma worker termination and replacement at the same timestamp | Puma and GitLab application logs | medium |
Ruby trace referencing regexp_matcher.rb |
Any saved crash trace | medium |
| Commit adding a regex with an inline comment splitting a large quantifier bound | GitLab project commit history and CI configuration | medium |
| Child shell process under a Puma worker | Host process accounting or EDR | medium |
These behaviours are single-sourced to Rapid7's technical analysis; verify against your own baseline before enforcing. Rapid7 states the undisclosed path may differ from the one it demonstrated, so absence of these patterns is not evidence of absence of exploitation.
6. Detection
The sources give usable artefacts for a YARA rule: the inline-comment quantifier construction, the repeated POSIX word classes, and the CI configuration keys that carry the expression. These appear in the malicious CI configuration and in the regex source itself.
rule AT_GitLab_CVE_2026_97229_Regex_Overflow_CI
{
meta:
author = "Adverse Trace"
date = "2026-10-16"
reference = "https://www.rapid7.com/blog/post/ra-gitlab-ci-cd-regex-integer-overflow-technical-analysis-cve-2026-97229"
description = "Detects CI configuration and regex source carrying the CVE-2026-97229 quantifier-bound bypass and repeated POSIX word classes"
strings:
$a = "exists:"
$b = "regexp:"
$c = "(?#)"
$d = "[[:word:]][[:word:]][[:word:]][[:word:]][[:word:]][[:word:]]"
$e = "rules:"
condition:
uint32(0) != 0x04034b50 and
(($a and $b and $c) or ($b and $d)) and
any of ($e)
}
The Sigma rule below targets the log and process behaviour Rapid7 describes for the demonstrated route.
title: GitLab CI Lint Request Triggering Regex Compiler Overflow
id: 7c1f4a92-3b6e-4d21-9a5c-0e8f2d6b1a44
status: experimental
description: Detects the GitLab CI lint request pattern and worker crash associated with CVE-2026-97229 exploitation
author: Adverse Trace
date: 2026/10/16
references:
- https://www.rapid7.com/blog/post/ra-gitlab-ci-cd-regex-integer-overflow-technical-analysis-cve-2026-97229
logsource:
category: webserver
product: gitlab
detection:
selection_request:
cs-method: 'GET'
cs-uri-stem|contains: '/api/v4/projects/'
cs-uri-stem|endswith: '/ci/lint'
cs-uri-query|contains: 'content_ref='
selection_status:
sc-status: 502
condition: selection_request and selection_status
falsepositives:
- Legitimate CI lint requests against large repositories that time out
level: medium
A second Sigma rule covers the worker-level signal.
title: GitLab Puma Worker Crash With Regexp Matcher Trace
id: 2d8b6e10-5a47-4c93-b1f2-9e0a7c3d5f88
status: experimental
description: Detects a Puma worker crash whose trace references regexp_matcher.rb, consistent with CVE-2026-97229
author: Adverse Trace
date: 2026/10/16
references:
- https://www.rapid7.com/blog/post/ra-gitlab-ci-cd-regex-integer-overflow-technical-analysis-cve-2026-97229
logsource:
product: gitlab
service: puma
detection:
selection:
message|contains:
- 'regexp_matcher.rb'
- 'badgateway: failed to receive response: EOF'
condition: selection
falsepositives:
- Unrelated Ruby exceptions raised from the same file
level: medium
CVE assessment
1 referenced CVE — 1 critical (CVSS ≥ 9.0)
| CVE | CVSS | Exploited | EPSS | Summary |
|---|---|---|---|---|
| CVE-2026-97229 | 10.0 Critical | — | 1% | GitLab has remediated an issue in GitLab CE/EE affecting all versions from 19.2 before 19.2.8, 19.3 before 19.3.5, and 19.4 bef… |
7. Sources
- Rapid7, "Rapid7 Analysis: GitLab CI/CD Regex Integer Overflow (CVE-2026-97229)", https://www.rapid7.com/blog/post/ra-gitlab-ci-cd-regex-integer-overflow-technical-analysis-cve-2026-97229, 2026-10-16.
- Rapid7, "CVE-2026-97229: Critical Unauthenticated Remote Code Execution in GitLab", https://www.rapid7.com/blog/post/etr-cve-2026-97229-critical-unauthenticated-remote-code-execution-in-gitlab, 2026-10-13.
- Help Net Security, "GitLab admins urged to patch critical RCE flaw (CVE-2026-97229)", https://www.helpnetsecurity.com/2026/10/13/gitlab-ci-cd-regular-expression-compiler-vulnerability-cve-2026-97229/, 2026-10-13.
8. Adverse Trace position
Adverse Trace assesses CVE-2026-97229 as CVSS 10.0 CRITICAL with EPSS 1% and no CISA KEV listing, and we do not inflate that: the technical severity is maximum, but there is no confirmed in-the-wild exploitation and the demonstrated route requires prior write access to a public project, which Rapid7 itself flags as conflicting with the vendor's unauthenticated rating. The gap between the two positions is unresolved because the underlying HackerOne report is private, and we will not present the unauthenticated claim as established fact. For EMEA financial services clients running self-managed GitLab, the practical exposure is command execution as the git account inside a CI/CD worker, on infrastructure that typically sits close to source code and build credentials, so we treat the upgrade to 19.2.8, 19.3.5 or 19.4.2 as urgent regardless of the exploitation status. We will track GitLab's advisory and any change to the exploitation picture, and we will update this note if the private report becomes public or if exploitation is observed. Clients who cannot confirm their GitLab version and public-project settings should contact us today.
Published via PulseTrace — Adverse Trace threat intelligence.