CVE-2026-97229: GitLab Incident and Ruby Fix
Learn how CVE-2026-97229 affected Alpine Linux’s GitLab, why upgrading GitLab missed the Ruby fix, what data was at risk, and how operators should respond.
Why Alpine Linux Rebuilt GitLab After a Critical Ruby Vulnerability
Alpine Linux's GitLab incident is a good reminder that a version number alone doesn't tell you whether a system is actually patched. On October 20 and 21, 2026, an attacker attempted to exploit CVE-2026-97229 against Alpine's GitLab instance. The vulnerability carries a CVSS score of 10.0. Even after Alpine upgraded GitLab, the instance was still exposed - because the missing fix was in Ruby, not in GitLab's own code.
Alpine says it found no evidence the attacker executed code or altered repositories, packages, or releases. Still, some logs were missing, and a successful exploit can leave surprisingly little behind. So the team treated the server as possibly compromised and rebuilt it from scratch. That also meant resetting passwords and replacing GitLab's secret keys.
This incident matters beyond Alpine. It's a practical case study in dependency patching, incident response when you can't be sure, and supply-chain verification. Here's what happened, why the upgrade didn't close the gap, what Alpine could and couldn't confirm, and what operators should check in their own GitLab deployments.
What happened in Alpine Linux's GitLab incident?
Alpine reported that an attacker created a new account on its GitLab instance on October 20 and started probing it. On October 21, the attacker submitted crafted CI configurations meant to trigger CVE-2026-97229. At the time, the instance was running GitLab 19.3.4, which did not include the relevant fix.
Alpine says the attempts caused GitLab worker processes to crash eight times. In four cases, the attacker got partway through the exploit before failing later in the process. The attacker also downloaded Alpine's GitLab container image, which suggests they were tailoring the attempt to the specific build. That's more than routine scanning - but it still doesn't prove the exploit succeeded.
Why Alpine treated the server as potentially compromised
Alpine found no evidence that attacker-controlled code ran on the server. Investigators reported no timing delays consistent with the probes, and they found no unexpected files, keys, hooks, or processes. They also found no private data returned through web or API requests they could inspect.
Those findings are useful, but they don't prove execution was impossible. A working exploit might not trigger a crash or leave a clean trail in web logs. Some background-job logs and the old container's filesystem were no longer available, and Alpine had not recorded outbound network connections. The hosting provider didn't retain those records either.
That's what drove the response. The team couldn't show the attacker gained access, but it also couldn't confidently rule it out with the evidence it still had. Instead of continuing to run a server with an uncertain security history, Alpine closed GitLab to the public and rebuilt the instance from scratch. The rebuild was finished on October 28, with all passwords reset and GitLab's secret keys replaced.
The incident timeline in context
The timeline matters because exposure continued after the first attempted attack:
- October 12: GitLab released a fix.
- October 13: The CVE was published. Alpine upgraded to GitLab 19.3.4 by mistake instead of 19.3.5, which contained the fix.
- October 20-21: The attacker registered an account, probed the instance, and triggered worker crashes.
- October 22: Alpine upgraded to GitLab 19.3.6, believing the issue was resolved.
- October 25-26: The attacker returned and made further unsuccessful attempts. Alpine later discovered on October 26 that its upgraded instance was still vulnerable.
- October 27-28: Public access was closed and the instance was rebuilt.
Two control failures show up here: the initial release-selection mistake, and then the assumption that a GitLab upgrade also meant the needed Ruby patch was in place. The second issue wasn't just a delayed patch - it was a packaging gap.
Why did upgrading GitLab fail to fix CVE-2026-97229?
CVE-2026-97229 affects how GitLab evaluates regular expressions in CI configuration. The underlying defect is in Onigmo, the regular-expression engine built into Ruby. GitLab's October 12 security releases patched the Ruby version bundled in GitLab's official packages and container images.
That packaging detail is the heart of what went wrong. Alpine's image used a standard Ruby build rather than the patched Ruby provided via GitLab's official distribution. When Alpine upgraded GitLab to version 19.3.6, it moved the application version forward - but it didn't necessarily swap out the Ruby runtime underneath. In other words: the vulnerable component could still be present even after the GitLab upgrade.
The difference between the application and its runtime
A GitLab deployment isn't just the GitLab application. It also depends on the runtime and other components around it. Security fixes can land at different layers, and the person who needs to apply them depends on how you installed and packaged the system.
In this case, GitLab's fix was delivered as patches to the Ruby version bundled in its packages and container images. Upstream Ruby later shipped the fix in Ruby 3.3.13, 3.4.12, and 4.0.9 on October 23. If your GitLab package includes GitLab's patched Ruby, updating that package may deliver the correction. If you build GitLab with your own Ruby or use a custom container, you have to verify and update the runtime yourself.
So "we upgraded GitLab" isn't a complete patch record. It only confirms one component's version, not the effective security state of the deployed system. The real questions are: which package or image did you deploy, what runtime does it contain, and does that runtime include the fix?
What Alpine's image guidance means
Alpine says the alpinelinux/gitlab:19.3.6 image published on October 27 contains Ruby 3.3.13. Earlier copies of the 19.3.6 tag - and older tags - were vulnerable. This matters because tags can be republished: the image you pull before and after a rebuild may not match byte-for-byte.
For operators, the takeaway is straightforward: verify what you actually deployed, not just what the tag says. Pinning and recording image digests can help confirm the exact image identity, but it still doesn't remove the need to check whether that image includes the patched runtime. A solid deployment record should tie together the application release, the image identity, and the runtime version.
What data and releases were at risk?
If the exploit had worked, Alpine says the GitLab instance could have exposed user and project data. The potential impact included email addresses, names, IP addresses, password hashes, two-factor authentication secrets, CI/CD variables, runner tokens, webhook and integration credentials, push-mirror credentials, personal access tokens stored as hashes, private repositories, and confidential issues and merge requests.
That list describes what could have been reachable from a successful server compromise. It isn't a claim that Alpine confirmed those items were stolen. Alpine found no evidence of data being returned through the web or API requests it could review, but the logging gaps meant it couldn't make an absolute statement about every possible access path.
What Alpine verified about its software supply chain
Separately, Alpine investigated whether the incident affected its packages and releases. It reported no signs of tampering in the aports repository, packages, or the October 21 stable releases: 3.21.9, 3.22.7, 3.23.7, and 3.24.3. Those checks covered release artifacts including ISO images and minirootfs tarballs used to build official Alpine Docker images.
Alpine's verification used records outside the potentially affected GitLab instance. It compared repository commits with a developer's clone and records from all 45 builders. It also checked that every package built from October 20 through 24, across architectures, came from commits matching the published records. The October 21 releases were built from those same verified commits.
This approach is stronger than relying only on the compromised system's internal history. Independent records let you test whether repository and build outputs match what was published. Alpine said it initially covered commits from October 20 to 22 and packages built through October 24, and that it extended the checks through October 27 because GitLab remained vulnerable until it was closed.
Alpine also said its package and release-signing keys were not exposed. Those keys weren't stored on the GitLab server. That separation limited the blast radius of a possible GitLab compromise: access to source hosting wouldn't automatically give someone the ability to sign official packages or releases.
What should GitLab operators do now?
Start with how you deploy, not just what version you see. Determine whether you run GitLab's official packages or container images, or whether you provide Ruby via a custom build. Then check the Ruby version that's actually present in the running service. For this vulnerability, the fixed upstream versions named by Alpine are Ruby 3.3.13, 3.4.12, and 4.0.9, or later.
If you use a custom image, rebuild and redeploy it with a fixed Ruby version. Don't assume upgrading the GitLab application also upgraded the runtime. If you use Alpine's alpinelinux/gitlab image, follow Alpine's guidance: the 19.3.6 image published on October 27 contains Ruby 3.3.13, while earlier copies of that tag and older tags are vulnerable. Confirm what your registry and running hosts actually pulled.
Use an incident decision path, not a version-only checklist
A practical response can be organized into four decisions:
- Establish exposure. Record the GitLab release, image digest or package source, Ruby version, and the date the instance received its current build. If you can't establish the runtime version, treat that as an unresolved exposure.
- Patch the vulnerable layer. Update Ruby or deploy a verified package or image that includes the fix. Restart affected services as your process requires, then confirm the running processes use the corrected runtime.
- Assess evidence of access. Review application, job, host, and network records for unexpected accounts, CI activity, processes, files, credentials, and outbound connections. Be specific about missing logs; "nothing suspicious showed up" doesn't mean the right telemetry was ever collected.
- Choose containment based on uncertainty and impact. If you can't rule out code execution and the system held sensitive credentials or private data, isolate or rebuild it and rotate secrets based on your risk assessment. Rebuilding takes time, but leaving an unknown foothold can reduce confidence and drag out the investigation.
Review credentials and monitoring
Alpine reset all GitLab passwords and replaced the instance's secret keys. It also told users to change any password reused on other sites after it noticed automated login attempts using leaked credentials. If you operate a potentially exposed instance, assess whether credentials or tokens stored there need rotation, and prioritize secrets tied to production systems, runners, repositories, and integrations.
The response also pointed to operational changes: apply critical GitLab security releases within a day, check whether each release includes patches to Ruby or other bundled components, keep logs longer than 14 days, record outbound and database connections, and sign release tags so they can be verified independently of GitLab.
These aren't interchangeable controls. Faster patching reduces the exposure window. Component-aware release checks prevent a false sense of security. Longer, broader logging helps incident investigation. Independent signatures help validate releases. The right mix depends on your threat model, but this incident shows what happens when those checks aren't in place - even while exploit attempts are already happening.
Conclusion
Alpine's incident is a reminder that patch status is a property of what you deployed, not a single product version number. A GitLab upgrade can leave a vulnerable runtime in place if your packaging model doesn't include the patched Ruby. And when the evidence is incomplete, "we found no sign of compromise" isn't the same as "we proved no compromise occurred."
For operators, the immediate work is concrete: identify the Ruby build behind GitLab, apply the fix, verify the deployed artifact, and check whether available logs are enough to support a reliable incident decision. If execution can't be ruled out and the instance held sensitive data or credentials, containment and rebuild may be the more defensible choice. The longer-term fix is to make component ownership, build provenance, and logging explicit before the next incident.