Alpine Linux's GitLab Incident: How CVE-2026-97229 Survived an Upgrade

A fix that lived in Ruby, not in GitLab, left Alpine's upgraded server open for five more days. Here is how the attack unfolded, how Alpine proved its packages were clean, and what self-hosters need to check.

Binary Buddies · 5 min read · 0 views
A lit build hall in a mountain valley at nightSnow-capped ridges under a moonlit sky, a row of small builder lights along a ridge, and a lit hall in the valley with a scaffolded wing being rebuilt.

On October 20 and 21, a freshly registered account on Alpine Linux's GitLab instance started submitting crafted CI configurations. Each one aimed at CVE-2026-97229, a critical, CVSS 10.0 flaw in how GitLab evaluates regular expressions in CI files. The instance was still on GitLab 19.3.4. GitLab had shipped the fix on October 12, eight days earlier. Eight worker processes crashed. In four of those crashes the attacker got partway through the exploit before failing at the next step.

The worse part came after the response began. On October 22 Alpine's infrastructure team upgraded to GitLab 19.3.6, which should have closed the hole. It did not. GitLab's fix is not in GitLab's own code. It is a patch to the Ruby interpreter that GitLab bundles in its official packages and container images. Alpine's image used standard Ruby, so the upgrade changed the version number and left the bug where it was.

Alpine found no evidence that the attack succeeded. It also could not prove that it failed, so it treated the server as compromised, reset every password and rebuilt GitLab from scratch. The instance reopened on October 30 with new SSH host key fingerprints. Anyone who self-hosts GitLab on their own Ruby should check their setup now.

Key takeaways

  • CVE-2026-97229 lives in Onigmo, Ruby's built-in regex engine, and GitLab exposes it through CI configuration parsing.
  • Upgrading GitLab does not fix it unless your Ruby is patched too. GitLab's official packages carry the patch. Source installs and custom images need Ruby 3.3.13, 3.4.12, 4.0.9 or later.
  • Alpine's packages, the aports repository and the October 21 releases were checked against records the attacker could not reach, and no tampering was found.
  • A successful exploit would have left no crash and no web-log trace, which is why Alpine rebuilt the server instead of declaring it clean.

A regex bug, reachable from a CI file

The vulnerability sits below GitLab. Onigmo is the regular expression engine built into Ruby, and every regex a Ruby program compiles passes through it. GitLab's CI configuration supports regular expressions, for example in rules that match branch names or variable values. Anyone who can push a CI file can therefore hand GitLab a pattern to compile. If the engine mishandles a crafted pattern, the attacker controls input to code running inside a GitLab worker.

That is how a bug in a language runtime becomes a 10.0. On an instance with open registration, the attacker needs no special privileges. They need an account and a project. Alpine's attacker created a new account for the purpose.

The fixes arrived in an unusual order. GitLab published fixed releases on October 12, delivered as patches to the Ruby it ships. Upstream Ruby released its own fix eleven days later, on October 23, in Ruby 3.3.13, 3.4.12 and 4.0.9. For those eleven days, the only patched Ruby available came inside GitLab's own packages.

Eight crashes from a brand-new account

The attack ran across two days. The new account repeatedly sent crafted CI configurations to the instance, and GitLab worker processes went down eight times. Alpine's analysis found that in four of those crashes the exploit had progressed past its first stage before failing at the next one. The Alpine team put it bluntly: the attack “came close to working”.

One more detail suggests the attempt was targeted. The attacker downloaded Alpine's GitLab container image. Memory-corruption exploits tend to depend on the exact binary layout of the target, so having the same build lets an attacker rehearse offline. That makes the crashes look like an exploit being tuned to Alpine's server, failure by failure.

How far the attack gotCrafted CI configurations from a new account caused eight worker crashes; in four the exploit got partway, then failed at the next step.Crafted CI configs, new account8 worker process crashes4 got partway throughNext step failed
Each crash is a visible failure. A clean success would have left nothing comparable to count.
10.0CVSS score of CVE-2026-97229
8GitLab worker crashes during the attack
4crashes partway into the exploit
45builders checked against aports commits

Why upgrading to GitLab 19.3.6 changed nothing

On October 22 Alpine did what any administrator would do and moved from 19.3.4 to 19.3.6, a release that included the October 12 fix. The instance stayed vulnerable. GitLab's fix is a set of patches to Ruby, applied when GitLab builds its official Omnibus-style packages and container images. Alpine runs its own image, alpinelinux/gitlab, built on standard Ruby. The GitLab code moved forward and the interpreter running it did not.

This catches every deployment that does not use GitLab's own packages: source installs, distribution packages and home-built containers. The release notes say a version fixes a CVE, and that is true only if your Ruby matches the one GitLab tested.

Where the fix livesThe same GitLab 19.3.6 code is fixed when paired with GitLab's patched Ruby and still vulnerable when paired with standard Ruby.Same GitLab version, different RubyGitLab official packageGitLab 19.3.6 codeRuby with GitLab patchAlpine's own imageGitLab 19.3.6 codeStandard RubyFix appliedStill vulnerable
Look at the bottom row of each box: the GitLab code is identical, and the Ruby underneath decides the outcome.
A server room with one empty rackRows of racks with blinking status lights, warm light spilling from a doorway, and one rack stripped bare with loose cables hanging.

Fifteen days exposed, and the decision to rebuild

From GitLab's fix on October 12 until Alpine closed the instance on October 27, the server was exploitable. That is fifteen days. The last five were spent on 19.3.6, which looked patched and was not. Alpine closed the instance on the same day it published a corrected image containing Ruby 3.3.13. While GitLab was down, the package mirrors, cgit, the mailing lists and IRC stayed up.

  1. GitLab ships the fix

    Fixed GitLab releases carry patches to the bundled Ruby. Alpine's instance is on 19.3.4.

  2. Exploitation attempts begin

    A newly registered account submits crafted CI configurations. The attempts continue on October 21, the same day Alpine publishes four stable releases.

  3. Upgrade to 19.3.6

    The upgrade leaves the vulnerability in place because the image uses standard Ruby.

  4. Upstream Ruby fixes Onigmo

    Ruby 3.3.13, 3.4.12 and 4.0.9 are released.

  5. Integrity checks

    Commits from October 20 to 22 and packages built through October 24 are verified against records outside GitLab.

  6. GitLab closed, image fixed

    The instance is taken offline. A new alpinelinux/gitlab:19.3.6 image with Ruby 3.3.13 is published.

  7. GitLab reopens

    The rebuilt instance returns with reset passwords and new SSH host key fingerprints.

Exposure windowThe instance was exposed from October 12 to 27, ran unpatched 19.3.6 from October 22 to 27, and upstream Ruby's fix became available on October 23.Fifteen days exposedAttackClosedInstance exposedOn 19.3.6, unpatchedRuby fix publicOct 12Oct 20Oct 23Oct 27Oct 30
The amber bar is the trap: five days on a release that should have been safe.

Alpine had mitigated the attack, so the rebuild was not forced. The team chose it because of what the logs could not show. A successful exploit would not crash a worker and would leave no trace in the web logs. Background job logs from the attack day were gone, along with the old container's filesystem. Alpine had no logging of outbound network connections, and neither did its hosting provider. With the evidence that could confirm code execution no longer available, Alpine rebuilt the instance to remove any foothold a successful exploit might have left.

Proving the packages were clean without trusting GitLab

For a Linux distribution, the important question is not whether GitLab leaked data but whether anyone shipped poisoned packages. Alpine's aports repository, which holds every package build recipe, is hosted on that GitLab. If the server was compromised, nothing it reported about its own history could be trusted. The checks had to rely on records outside it.

Every aports commit added from October 20 to 22 arrived through a recorded push by a known developer. Alpine confirmed this against a developer's local clone and all 45 builders. Every package built from October 20 to 24, on every architecture, came from commits as published on git.alpinelinux.org. The October 21 stable releases, 3.21.9, 3.22.7, 3.23.7 and 3.24.3, were built from those same verified commits. That covers their ISO images and the minirootfs tarballs that the official alpine Docker images are built from.

Verification outside GitLabAports commits from October 20 to 22 were checked against a developer's clone, all 45 builders and git.alpinelinux.org, covering packages built October 20 to 24 and the October 21 releases. Signing keys were never on GitLab.Checked against records outside GitLabA developer's cloneAll 45 buildersgit.alpinelinux.orgaports commitsOct 20 to 22Packages Oct 20 to 24Releases of Oct 21Signing keys never stored on GitLab
None of the evidence on the left lives on the GitLab server, which is what makes it trustworthy here.

The signing keys add a second layer of protection. The keys that sign Alpine packages and releases are not stored on the GitLab server and never have been. Even full control of GitLab would not have let an attacker produce a validly signed package. One gap remains open. The October 24 checks covered commits up to October 22 and packages up to October 24, but GitLab stayed vulnerable until October 27. Alpine is extending the same checks to that date.

What could have leaked, and what users must do

The repositories and packages check out, but user data is less certain. Alpine found no private data returned to the attacker through any web or API request it can see, and no sign that the attacker's code ran. Timing probes showed no delays, and there were no unexpected files, keys, hooks or processes. Those findings come with the same caveat as the rest. If the exploit had succeeded, the attacker could have reached:

  • 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, which are stored as hashes
  • Private repositories, confidential issues and merge requests

All passwords were reset. Alpine has also seen automated login attempts against its GitLab using passwords leaked from other sites. That is a separate threat, and it makes reused passwords the most urgent issue for users.

  1. Set a new password

    Use “Forgot your password?” on the gitlab.alpinelinux.org sign-in page. Old passwords no longer work.

  2. Change reused passwords elsewhere

    If your Alpine GitLab password was also used on any other site, change it there.

  3. Update SSH host keys

    The rebuilt instance has new SSH host key fingerprints. Check them against Alpine's “GitLab is back: what you need to do” post before accepting the new key.

  4. Rotate what you stored

    2FA secrets, CI/CD variables, runner tokens, access tokens and webhook credentials were all in the exposure list. Treat anything sensitive you kept in a project there as worth replacing, and follow Alpine's reopening instructions for specifics.

Not just GitLab: any Ruby that compiles untrusted regex

The bug is in Onigmo, so GitLab is just the most visible way to reach it. Any Ruby program that compiles regular expressions from untrusted input is exposed. That includes a search box that accepts patterns, a rules engine that takes user-defined filters, or a web app that builds a Regexp from a query parameter. These are common in internal tools and are rarely reviewed as attack surface.

On Alpine itself, the fix is already packaged. Alpine 3.22, 3.23, 3.24 and edge ship Ruby 3.4.12, and Alpine 3.21 ships Ruby 3.3.13. Both versions include the fix. For self-hosted GitLab, the checks are short:

apk upgrade                               # Alpine hosts: pull in the fixed ruby
ruby -v                                   # need 3.3.13, 3.4.12, 4.0.9 or later
docker pull alpinelinux/gitlab:19.3.6     # re-pull: the Oct 27 image has Ruby 3.3.13

What Alpine is changing

The first change is the obvious one: the rebuilt instance runs a Ruby with the fix, 3.3.13 or later. The second addresses the eight-day gap that let the attack happen at all. Alpine now commits to applying critical GitLab security releases within a day. Had that policy been in place on October 12, the October 20 attempts would have hit a patched server. That assumes the patch had actually included Ruby, which is the lesson of October 22.

A one-day policy alone would not have prevented this incident. Alpine's real failure was a gap in understanding how the upstream fix was delivered, and that kind of gap affects every downstream packager. Distributions, container maintainers and anyone else who repackages a large application on their own runtime face the same risk. Faster patching helps only if the patch reaches the component that contains the bug.

Dawn over the valleyMorning light rising behind mountain ridges, mist drifting over the valley, and a path of small lights leading to an open, lit doorway.

The limits of “no evidence”

Alpine's write-up is careful about what it does not know. The four partial crashes show the attacker getting close, and the downloaded image shows intent and method. Nothing shows success, but nothing can rule it out either. A clean exploit was designed to look like silence, and the logs that might have recorded it, background jobs, the old container's disk and outbound connections, were either deleted or never kept. Missing outbound-connection logs are the clearest operational gap, and they are cheap to fix compared with a full rebuild.

Several things are worth watching. The extended integrity checks covering October 22 to 27 are still to come. Downstream images and distribution packages of GitLab that bundle their own Ruby deserve scrutiny now that this failure mode is public. Ruby applications outside GitLab that compile user-supplied patterns are the long tail of this CVE, and unlike GitLab, few of them will announce that they are affected.

Were Alpine Linux packages or Docker images tampered with?

No tampering was found. Packages built from October 20 to 24 and the October 21 releases (3.21.9, 3.22.7, 3.23.7, 3.24.3), including the minirootfs tarballs behind the official alpine Docker images, trace back to commits verified against a developer's clone and all 45 builders.

I upgraded GitLab after October 12. Am I safe from CVE-2026-97229?

Only if you use GitLab's official packages or images, which include the Ruby patch. If GitLab runs on your own Ruby, you also need Ruby 3.3.13, 3.4.12, 4.0.9 or later.

Which alpinelinux/gitlab image is fixed?

The alpinelinux/gitlab:19.3.6 image published on October 27, which contains Ruby 3.3.13. Earlier pulls of 19.3.6 and all older tags are vulnerable, so pull the tag again.

Does this affect Ruby programs other than GitLab?

Yes. Any program that compiles regular expressions from untrusted input on an unpatched Ruby is exposed. On Alpine, apk upgrade installs Ruby 3.4.12 (3.22, 3.23, 3.24, edge) or 3.3.13 (3.21), both fixed.

Who do I contact about my account?

Alpine's infrastructure team at infra@alpinelinux.org.

This article draws on Transport security has blocked a cleartext HTTP and answers to it (1, 2), on the Stack Exchange network, licensed under CC BY-SA 4.0.

  • #Alpine Linux
  • #CVE-2026-97229
  • #GitLab CI security
  • #GitLab security
  • #Onigmo
  • #Ruby regex vulnerability

Comments

Add a comment
Sign in with Google to join the conversation