Alpine Linux has published an incident report on its GitLab instance with a lesson I think is very useful for anyone maintaining their own container images: they upgraded GitLab to a fixed version and were still vulnerable.
The bug
CVE-2026-97229 is a critical vulnerability, with the maximum score of 10, in how GitLab evaluates regular expressions in CI configuration. According to GitLab’s advisory, a crafted regular expression in a CI file could allow code execution on the server. It affects GitLab CE/EE 19.2 before 19.2.8, 19.3 before 19.3.5 and 19.4 before 19.4.2.
The important detail is where the bug really is: not in GitLab’s code, but in Onigmo, the regular expression engine built into Ruby. GitLab released its fixed versions on 12 October, but the fix consists of patches to the Ruby that GitLab bundles in its own packages and images. Upstream Ruby didn’t fix it until 23 October, with versions 3.3.13, 3.4.12 and 4.0.9, which fix several regular expression engine bugs related to this and other GitLab CVEs.
What happened at Alpine
The timeline, as they tell it, is a series of small stumbles anyone could make:
| Date | What happened |
|---|---|
| 12 October | GitLab releases the fix |
| 13 October | The CVE is published. Alpine upgrades, but by mistake to 19.3.4 instead of 19.3.5, which had the fix |
| 20 October | A newly registered account starts probing |
| 21 October | Main attack: 502 errors and failed CI jobs |
| 22 October | They upgrade to GitLab 19.3.6 |
| 23 October | The attacker’s account is blocked. Ruby releases the fix |
| 24 October | They verify aports, packages and the 21 October releases against outside records |
| 25 and 26 October | The attacker tries again, without success |
| 26 October | They find the instance is still vulnerable after the upgrade |
| 27 October | GitLab closed to everyone except developers and build servers; rebuild starts |
| 28 October | Rebuild complete, passwords reset and GitLab’s secret keys replaced |
The attacker sent crafted CI configurations that crashed GitLab’s worker processes 8 times. In 4 of them they got partway through the exploit and failed at the next step. They also downloaded Alpine’s GitLab container image, which suggests they were tuning the exploit to that exact build.
Why upgrading didn’t help
Here’s the lesson. Alpine’s GitLab image doesn’t use GitLab’s official packages, but standard Ruby. So upgrading GitLab to 19.3.6 updated GitLab’s code, but not the Ruby it runs on, which is where the bug was. As the report itself says:
If you run GitLab with your own Ruby (from source or in your own container image) rather than GitLab’s packages, upgrading GitLab is not enough: you also need Ruby 3.3.13, 3.4.12, 4.0.9 or later.
flowchart LR
A["Official GitLab packages"] --> B["GitLab 19.3.5+ · patched Ruby"]
C["Own image with standard Ruby"] --> D["GitLab 19.3.6 · unpatched Ruby"]
B --> E["Protected"]
D --> F["Vulnerable until Ruby 3.3.13"]If you use the alpinelinux/gitlab image, pull alpinelinux/gitlab:19.3.6 again: the one published on 27 October ships Ruby 3.3.13, and any 19.3.6 pulled earlier, or any older tag, is vulnerable.
The problem isn’t limited to GitLab. As Alpine explains, the bug is in Ruby’s regular expression engine, so any program that compiles regular expressions from untrusted input is exposed. Alpine 3.22, 3.23, 3.24 and edge already ship Ruby 3.4.12, and Alpine 3.21 ships 3.3.13, so apk upgrade is enough.
What they know and what they can’t rule out
What I like most about the report is how clearly it separates what’s been verified from what can’t be ruled out:
- Verified: Alpine releases and packages weren’t modified. They checked every aports commit from 20 to 22 October against records the attacker couldn’t reach (a developer’s clone and the 45 builders), and every package built between 20 and 24 October came from those commits. The keys that sign packages have never been on the GitLab server.
- No sign of code execution: they found no unexpected files, keys, hooks or processes.
- But they can’t be sure: a successful exploit would leave no crash and no trace in the web logs, some logs from the attack day are gone and they weren’t logging outbound connections.
So they took the hard route: treat the instance as compromised and rebuild it from scratch. They’ve reset every password and replaced the secret keys, because had the attack worked, emails, password hashes, 2FA secrets, CI/CD variables, runner tokens and private repositories could have been exposed. They also warn that they’ve seen automated login attempts with passwords leaked from other sites, so anyone who reused the same password elsewhere should change it.
What they’re changing
- Running the rebuilt instance with a fixed Ruby.
- Applying critical GitLab security releases within a day and checking each release for Ruby or other component patches their own build has to apply.
- Logging outbound and database connections, and keeping logs for longer than 14 days.
- Signing release tags, so releases can be verified without depending on GitLab.
My takeaway
This one hits close to home. Recently I wrote about how I maintain a PHP Docker image that can build PHP from source. And Alpine’s lesson is exactly the question worth asking with any image like that: when a security patch comes out, where is it really? In the application code, in the interpreter, in a system library? And does it reach my image, or only the vendor’s official packages?
Building your own images gives you control, but also makes you responsible for something official packages handle for you. And the Alpine case, a project with very capable people, shows how easily it can slip through: a wrong version by mistake and a patch that wasn’t where it was expected.
I also appreciate the honesty of the report. Saying “there’s no sign of it, but we can’t rule it out, so we’re rebuilding everything” is much harder than saying “nothing happened”, and it’s the right call.
Source: GitLab security incident: attempted exploitation of CVE-2026-97229, Alpine Linux.




