DEV Community

jamilxt
jamilxt

Posted on

GitLab 19.3.6 Wasn't Enough for Alpine's Custom Image: Verify Ruby Too

An administrator upgrades GitLab, checks the version, and sees a newer number. The dashboard looks reassuring. But if the deployment uses a custom container with its own Ruby, the vulnerable regular-expression engine may still be running underneath the upgraded application.

That is what happened to Alpine Linux's GitLab instance. The project says it upgraded to GitLab 19.3.6 on October 22, but later found the instance remained vulnerable to CVE-2026-97229. GitLab's fix included patches to the Ruby runtime bundled in official GitLab packages. Alpine's custom image used standard Ruby and did not include that downstream patch. The incident is a useful reminder: application version and dependency patch state are related, but they are not interchangeable. Alpine's incident report explains the gap in detail.

This guide is for teams running GitLab in a custom container or with a separately managed Ruby. It shows how to build an evidence trail for the deployed image instead of treating a version string as proof. It does not include exploit instructions.

Disclosure: This is a source-based operational guide, not a report of hands-on testing on a GitLab deployment. Verify every command and remediation against your own package, image, and deployment documentation before applying it.

The incident in four dates

The vulnerability affects GitLab's evaluation of regular expressions in CI configuration. Alpine's report describes the underlying flaw as an integer overflow in Onigmo, the regular-expression engine built into Ruby. The CVE record rates CVE-2026-97229 as critical with a CVSS 10.0 score and says unauthenticated remote code execution was possible under the described conditions. The CVE record and Alpine's incident timeline establish the scope.

The dates expose the version-check trap:

  • October 12: GitLab released fixed versions for the affected GitLab branches.
  • October 20 and 21: An attacker attempted exploitation against Alpine's instance, which was running GitLab 19.3.4.
  • October 22: Alpine upgraded to GitLab 19.3.6, but that upgrade alone did not deliver the missing Ruby patch in its custom image.
  • October 23: Upstream Ruby released versions containing the fix: Ruby 3.3.13, 3.4.12, and 4.0.9.

Those dates and version details come from Alpine's report. They describe Alpine's deployment, not every GitLab installation. The CVE record identifies the affected version ranges. GitLab's October 12 patch release lists the fixes for those branches, while its October 21 patch release shows later releases, including 19.3.6. Check the latest patch information for your branch rather than relying on a single remembered version.

The important difference is how the fix arrived. GitLab applied the necessary Ruby changes in its official packages. A custom build using unpatched upstream Ruby did not inherit those changes merely by changing the GitLab application version. When upstream Ruby later shipped the fixed versions, Alpine could update to a runtime containing the fix. Alpine's explanation is unusually concrete about this dependency boundary.

First decide which deployment path you have

Do not start with a generic command copied from a forum. Start by identifying how GitLab and Ruby enter the running service.

  • Official GitLab package or image: The vendor may include a downstream patch before upstream publishes a new Ruby version. Verify the exact GitLab security release and the vendor's package or image provenance. A lower-looking Ruby version alone may not reveal a backported patch.
  • Custom image with system Ruby: You own the Ruby package and need to verify it includes the upstream fix or a documented distribution backport.
  • Build from source or a custom runtime: You own the Ruby source, patch set, build process, and the relationship between the GitLab code and runtime. Verify both components.
  • Image mirrored or rebuilt internally: The tag may look current while the internal artifact is stale. Record and inspect the image digest actually deployed, not just the tag written in a manifest.

This distinction matters because 19.3.6 was not enough for Alpine's specific custom image, but the report does not say that every official GitLab 19.3.6 package was vulnerable. Avoid generalizing the incident beyond its stated configuration.

A practical verification sequence

1. Record the GitLab version reported by the running instance. Use the version shown by your supported GitLab administration interface or documented command for your installation method. For an Omnibus-style installation, GitLab documents sudo gitlab-rake gitlab:env:info as an environment report. If you run it inside a container, execute it in the actual GitLab container, not in a build shell or on the host.

The GitLab version is one piece of evidence. Keep it alongside the package source, image reference, and deployment timestamp. It does not alone prove which Ruby binary the service is executing.

2. Identify the exact image or package in production. For Docker, record the running container's configured image and immutable image ID:

docker inspect --format='image={{.Config.Image}} image_id={{.Image}}' gitlab
Enter fullscreen mode Exit fullscreen mode

Replace gitlab with the actual container name. If your platform deploys by digest, record that digest from the workload specification as well. For Kubernetes, inspect the live Pod's image and imageID, not only the Deployment template: a rollout can leave old Pods serving traffic while the manifest already names a new tag.

A tag such as 19.3.6 may point to different image content over time if a publisher updates it. Alpine specifically says its alpinelinux/gitlab:19.3.6 image published on October 27 contained Ruby 3.3.13, while an image with that tag pulled before October 27 and older tags remained vulnerable. The report's image-specific warning is why the digest and pull date matter.

3. Inspect the Ruby used by the running service. In a custom container, first determine which executable is actually available and used:

docker exec gitlab sh -lc 'command -v ruby; ruby --version'
Enter fullscreen mode Exit fullscreen mode

This is a starting point, not universal proof. Some products bundle an embedded Ruby at a non-default path; the shell's ruby may be unrelated to the process runtime. Check the service's launch configuration and vendor documentation, then run the version check against that exact executable. If your image intentionally has no interactive shell, inspect its build recipe and software inventory, or run an approved diagnostic container from the same immutable image.

For a custom build relying on upstream Ruby's fixed releases, Alpine lists Ruby 3.3.13, 3.4.12, and 4.0.9 as versions containing the fix. Distributions can also backport security patches without changing the upstream version number in the same way. Therefore, compare your runtime against its maintainer's advisory or package changelog. Do not declare a build vulnerable or fixed from the first three digits of ruby --version alone. Alpine's incident FAQ confirms that its own Ruby packages received the fixed versions.

4. Verify the build provenance. For a custom image, save the Dockerfile or build recipe, base-image digest, Ruby package version, and the CI run that produced the deployed digest. Confirm the deployment uses that digest. If your organization publishes an SBOM, check it for the Ruby component and retain it with the release record.

This creates an auditable chain: source and package versions, build output, pushed artifact, deployed digest, and runtime observation. A tag match without this chain can still point to an old artifact.

5. Roll out a known-fixed artifact and verify the new Pods or containers. Update through the channel that owns the affected component. Official GitLab deployments should follow GitLab's patch guidance for the supported branch. Custom Ruby deployments should update Ruby to a fixed upstream version or a verified distribution backport, then rebuild the image. Deploy the rebuilt artifact and confirm the running image ID or digest changed.

Avoid replacing a running runtime binary in place unless the vendor documents that process. Rebuilding and redeploying gives you a reproducible artifact and a rollback point.

A small evidence checklist for incident response

For each self-managed GitLab instance, capture:

  • GitLab edition, branch, and exact patch version.
  • Installation method: official package, official image, custom image, or source build.
  • Running image digest or package build identifier.
  • The Ruby executable used by the GitLab process and its package provenance.
  • The advisory or changelog that establishes the fix for that build.
  • The build and deployment records connecting the fixed artifact to the running instance.
  • The timestamp when the updated runtime became active.

This is more useful than a screenshot of a version banner because each item answers a separate question: what application is installed, what artifact is running, which runtime it uses, and why that runtime is considered fixed.

If exploitation may have occurred, patching is not the whole response

Alpine did not claim that its attacker successfully executed code. The project said it found no evidence of code execution but could not rule it out because some logs were unavailable and the exploit could leave little evidence. It treated the instance as potentially compromised, rebuilt it, rotated secrets, reset passwords, and verified package and release integrity against records outside GitLab. The incident report carefully separates what was found from what could not be ruled out.

That is a response to uncertainty, not a universal instruction to rebuild every server after any vulnerable version is discovered. If your logs, exposure, and deployment history suggest possible exploitation, involve your incident-response team. Consider the credentials and materials accessible to the GitLab service, including CI/CD variables, runner tokens, integrations, and repository data. Preserve evidence and follow your organization's containment and rotation process.

Alpine also noted that it had not stored package-signing keys on the GitLab server and checked repository changes against developer clones and build systems outside the affected host. The broader lesson is to keep critical signing and verification records independent of the service they protect. Alpine's account of its integrity checks is a useful example of separating service recovery from package trust.

Keep this checklist for the next critical patch

  1. Identify whether the fix is in the application, a dependency, or both.
  2. Determine whether you use vendor packages or a custom build.
  3. Record immutable artifact IDs, not only mutable tags.
  4. Check the component version and the patch provenance for the actual runtime.
  5. Rebuild and redeploy custom artifacts through a traceable pipeline.
  6. Verify the running instance uses the intended artifact.
  7. Treat suspected exploitation as an incident-response question, not just an upgrade task.
  8. Keep signing keys, build records, and integrity checks outside the service they validate.

The useful question after an upgrade is not just “What version does the dashboard show?” It is “Which artifact is running, what dependency does it execute, and what evidence says that dependency contains the fix?” Alpine's experience shows why those questions belong in the same patch checklist.

I write about practical software engineering, developer tools, and security. Subscribe, it's free, for more source-based guides. Have you found a production patch that depended on a separately managed runtime or custom image? What evidence helped you confirm the fix?

Sources

Top comments (0)