Monday, November 2, 2026
Vulnerabilities

GitLab CVSS 10 Regex RCE on CISA KEV After Alpine Attack

CISA's KEV now lists a CVSS 10 GitLab RCE hiding in Ruby's regex engine — and Alpine's near-miss shows why upgrading GitLab alone may not fix it.

Illustration of a mechanical counter overflowing and shattering, symbolising an integer overflow in a regex engine
An integer that wraps past its limit is the pebble that starts the avalanche — here, inside Ruby's regex engine.

CISA has added CVE-2026-97229 to its Known Exploited Vulnerabilities catalog: a CVSS 10.0 integer overflow in GitLab Community Edition and Enterprise Edition that lets an unauthenticated attacker execute arbitrary code on the server with nothing more exotic than a specially crafted regular expression in a CI/CD configuration. Federal agencies have until November 1 to remediate. And before anyone files this under "theoretical until proven otherwise" — it isn't. Someone spent the better part of a week in October trying to land this exact bug against the Alpine Linux project's own GitLab server, came frighteningly close, and forced Alpine to nuke the instance from orbit and rebuild it.

The attack on gitlab.alpinelinux.org is the most instructive public post-mortem we have for this bug, and Alpine deserves credit for publishing it with the unknowns left in. The short version: the exploit is real enough to tune against a specific target build, a GitLab version upgrade is not necessarily a fix, and the blast radius of a compromised GitLab instance is your entire software supply chain. Let's walk all three.

The bug: one regex away from code execution

The primitive is a classic with a modern wrapper. GitLab compiles the regular expressions in CI configurations using Ruby, and Ruby's regular expression engine — Onigmo, a C library baked into the interpreter — contains an integer overflow or wraparound. Feed it a crafted pattern and the arithmetic used during compilation wraps past a boundary, corrupting memory in a process that is happily parsing attacker-controlled input. CISA's KEV entry describes the result plainly: insufficient regular expression validation, unauthenticated arbitrary code execution on the GitLab server. It's the same species of bug we dissected in the Brotli decoder RCE — one overflowed integer in a C data-processing library, and the whole process is yours.

The chain is embarrassingly short, which is what makes CVSS 10.0 feel earned rather than inflated. Register an account on a GitLab instance that allows sign-ups. Push a repository containing a malicious .gitlab-ci.yml with the crafted regex. GitLab's workers compile that config to lint and run the pipeline, Onigmo chokes, memory corrupts, and the attacker lands code execution in the worker context on the server itself. No credentials, no session, no social engineering — the CI parser is the attack surface. If your instance lets strangers register and trigger pipelines, your pre-auth exposure is the registration form.

The first exposure check costs nothing and answers the question that actually matters — which Ruby is doing the compiling:

$ ruby -v
ruby 3.3.12        # vulnerable — you need 3.3.13, 3.4.12, 4.0.9 or later
# On Alpine hosts, the distro package already carries the fix:
$ apk upgrade        # 3.21 ships Ruby 3.3.13; 3.22/3.23/3.24/edge ship 3.4.12

Eight crashes, four near-misses, one rebuilt server

According to Alpine Linux's incident write-up, GitLab shipped the fix on October 12 and the CVE was published October 13 — the day Alpine meant to upgrade but installed 19.3.4 instead of the patched 19.3.5, a mistake it owns in the timeline. A week later, on October 20, a newly registered account began probing the still-vulnerable server. Across October 20 and 21 the attacker pushed crafted CI configurations that crashed GitLab worker processes eight times; the main attack on the 21st also threw 502 errors and killed CI jobs. In four of those crashes, Alpine says, the attacker got partway through the exploit and failed at the next step.

The detail that should raise every defender's pulse: the attacker downloaded Alpine's own GitLab container image during the campaign. That's not spray-and-pray scanning; that's someone building a local replica of the target to dial in offsets and payload behavior. Alpine's read on it, quoted by The Hacker News:

"suggests they were tuning the exploit to our exact build," Alpine said of the attacker's decision to download its GitLab container image mid-attack.

Alpine blocked the account on October 23 — the same day upstream Ruby shipped the fix in 3.3.13, 3.4.12 and 4.0.9 — but the attacker returned on October 25 and 26 and tried to get back in. Those attempts failed. On October 26 Alpine made the discovery that reframed the whole incident: the server was still vulnerable despite its October 22 upgrade to GitLab 19.3.6. It closed GitLab to everyone except developers and build servers on the 27th, started a full rebuild, and finished on the 28th with every password reset and all of GitLab's secret keys replaced. Alpine found no evidence the attacker's code ever ran — but states it cannot rule it out, and treated the instance as compromised on that basis alone. That's the correct call, and it's the call most organisations would have talked themselves out of.

The patch that wasn't: your GitLab version is lying to you

Here's the trap, and it's the part of this story with the widest blast radius. GitLab's October 12 fix is not a change to GitLab's own code — it's a set of patches to the Ruby that GitLab bundles inside its official Linux packages and container images. If you installed GitLab from those official artifacts and upgraded, you're covered. But if you run GitLab on your own Ruby — built from source, or baked into a custom container image the way Alpine's was — upgrading GitLab changes nothing. The vulnerable component sits underneath the application you just patched. Alpine's 19.3.6 upgrade looked like remediation for four days while the door stayed open.

There's a second, quieter trap in how Alpine shipped its own fix. Its alpinelinux/gitlab:19.3.6 image published before October 27 used standard Ruby and is vulnerable; the same tag re-published on October 27 contains Ruby 3.3.13. Alpine's instruction is to pull alpinelinux/gitlab:19.3.6 again, and to treat anything pulled before October 27 — plus all older tags — as vulnerable. Re-using a mutable tag for a security fix means anyone pinning by tag, or running a cached pull, can be running the vulnerable build while believing they're patched. Check digests and pull dates, not tag names. And zoom out one level further: the bug lives in Ruby's regex engine, so any program that compiles regular expressions from untrusted input is exposed, not just GitLab. Your other Ruby services deserve the same version question this week.

Illustration of old keys and tokens being melted down while fresh ones are forged, symbolising credential rotation
When a GitLab server might be compromised, the patch is the cheap part — rotating everything it held is the real bill.

What a popped GitLab server is actually worth

Alpine's write-up includes something rare in incident disclosure: an explicit list of what would have been exposed had the exploit landed. It's worth reading as a checklist for your own instance — email addresses, names and IP addresses of all users; password hashes; 2FA secrets; CI/CD variables and runner tokens; webhook, integration and push-mirror credentials; personal access tokens (stored as hashes); private repositories; and confidential issues and merge requests. Read that list again as an attacker. Runner tokens and CI/CD variables are the keys to your build infrastructure; webhooks and push mirrors are paths into every system GitLab talks to; private repos are the product. This is why the remediation cost calculus for a suspected GitLab compromise isn't "apply the patch" — it's "rotate everything," and Alpine itself replaced GitLab's secret keys and reset all passwords while, as The Hacker News notes, it has not said whether runner tokens, CI/CD variables, access tokens or integration credentials were rotated.

Two architectural decisions saved Alpine from a much worse week. First, the keys that sign Alpine's packages and releases have never been stored on the GitLab server — so the one secret that would have turned this into a distribution-level supply-chain emergency simply wasn't there to steal. Second, verification didn't depend on the possibly-compromised system: every aports commit from October 20 to 22 arrived via a recorded push by a known developer, checked against records the attacker couldn't reach — a developer's clone and all 45 build servers — and every package built from October 20 to 24, plus the October 21 releases (3.21.9, 3.22.7, 3.23.7, 3.24.3, including the minirootfs tarballs behind the official Docker images), traced back to commits as published on git.alpinelinux.org. Alpine is extending the checks through October 27, when the server closed. Trust-but-verify, with the verify anchored outside the blast radius.

Detection is a coin flip — so hunt the crashes and fix the logging

Now the uncomfortable part. The only reason Alpine saw this attack at all is that the exploit kept failing. Eight worker crashes, 502 errors and dead CI jobs are a detection signal with the subtlety of a smoke alarm — but a working exploit is silent, and Alpine says so directly:

"A successful exploit would leave no crash and no trace in the web logs," Alpine said, explaining why it rebuilt the server despite finding no evidence of code execution.

Compounding that, some of the evidence that could have settled the question no longer exists: background job logs and the old container's filesystem are gone, and neither Alpine nor its hosting provider logged outbound network connections — so even a successful data exfiltration would be invisible in retrospect. If you take one lesson from this incident beyond the patch, take that one. You cannot retroactively investigate telemetry you never collected.

Concretely, here's the order of operations I'd run this week, mirroring the changes Alpine committed to in its own response:

  • Confirm the Ruby underneath GitLab is 3.3.13, 3.4.12, 4.0.9 or later — especially on source builds and custom images, where a GitLab upgrade alone leaves the bug open.
  • Hunt worker crash logs, 502 spikes and failed CI jobs correlated with recently registered accounts and newly pushed CI configurations; treat repeat crashes as an exploitation attempt, not instability.
  • If you can't rule out code execution, rotate the full list: runner tokens, CI/CD variables, personal access tokens, webhook, integration and push-mirror credentials, and user passwords.
  • Enable outbound-connection and database logging on the GitLab host and extend retention well beyond 14 days — Alpine's gap was no egress logging at all.
  • Keep package and release signing keys off the GitLab server permanently, and sign release tags so artifacts can be verified independently of GitLab.
  • If you run the alpinelinux/gitlab image, re-pull 19.3.6; images pulled before October 27 and all older tags are vulnerable.

CISA's KEV listing — the second CI/CD-adjacent entry to demand this kind of response after the Arista VeloCloud SQLi — lands with a November 1 deadline for federal agencies, and the Alpine incident is Exhibit A for why everyone else should move at the same tempo. What is confirmed: the bug is a CVSS 10.0 unauthenticated RCE, the fix exists in GitLab's official packages and in upstream Ruby, and a real attacker mounted a determined, target-tuned exploitation attempt against a high-value open-source project. What is claimed: Alpine found no evidence of code execution and no tampering with packages or releases, verified against records outside the compromised system. What is unknown — and will stay unknown — is whether one of those four partway-successful attempts, or a quieter one, ever finished the chain. Alpine's answer to that uncertainty was to rebuild everything and reset every credential. If your GitLab holds your source code, your pipeline secrets and your signing-adjacent infrastructure, that isn't paranoia. It's the price of admission.

Sources

This article draws on reporting and advisories from: