GitLab RCE via Oj: a memory-corruption chain hidden in a non-security patch
> In short: A GitLab RCE chain built on two memory-corruption bugs in Oj (a Ruby JSON parser) lets any authenticated user who can push to a project run commands as git on self-managed GitLab. Fixed in 18.10.8, 18.11.5, and 19.0.2 — but the June 10 fix was filed under bug fixes, not security, with no CVE. depthfirst published working PoC code on July 24.
Why this matters to you
GitLab is where your source code, CI/CD secrets, and deploy keys live. A self-managed GitLab compromise is a supply-chain incident: the attacker gets your code and your pipeline in one move. If you self-host GitLab for sovereignty or cost reasons (many French SaaS vendors do), this chain lands directly on your infrastructure.
The most uncomfortable detail is not the bug itself — it's that the fix shipped six weeks before the PoC, and most operators never knew it was security-relevant. GitLab listed the Oj 3.17.3 bump under bug fixes. There is no CVE, no CVSS, no advisory. Teams that triage releases against the security table had no signal to upgrade urgently.
The chain in plain terms
GitLab's notebook renderer (ipynbdiff) passes repository-controlled .ipynb JSON to Oj::Parser.usual.parse inside a long-lived Puma worker. Two Oj bugs make the chain work:
start callback.The attacker commits a crafted Jupyter notebook and opens its commit diff. That leaks a heap pointer. Repeat enough times to locate libc in memory, then two more notebooks fire the payload pointing the callback at system(). No admin rights, no CI/runner access, no victim interaction, no access to anyone else's project.
The affected versions and the Helm trap
All tiers are affected, CE and EE, Free through Ultimate:
The trap: if you deploy GitLab via Helm or the Operator, check the GitLab version inside the Webservice image running Puma, not the chart or Operator version. Anything on 15.2 through 18.9 gets no backport — those lines sit outside GitLab's security-maintained patch trains, so those installs must move to a supported release.
What to do
git reach source code, Rails secrets, service credentials, and CI/CD data.The wider lesson: security fixes that don't look like security fixes
The most important takeaway for an ops team is procedural. Not every security fix is published as one. depthfirst's wider Oj review produced nine more CVE advisories, none of them this chain. The GitLab fix went through HackerOne but was filed as a bug fix in the release notes. Your patching process cannot rely on the "security fixes" table alone — it has to triage dependency bumps on security-sensitive parsers (Oj, libxml2, Jackson, Fastjson) as if they were CVEs even when no CVE is attached. That's how you would have caught this on June 10 instead of July 24.
Building HR, payroll, or recruiting software? CleanIssue performs security audits for HR SaaS in real-world conditions, no source code access needed. For a first read of your exposure, start with an external review of your application.
Related articles
Three adjacent analyses to keep exploring the same attack surface.
GitLab and CVE-2023-7028: why this password-reset flaw worried everyone
CVE-2023-7028 allowed account takeover through password reset without user interaction in some GitLab versions. Here is why it left such a mark.
Gitea CVE-2026-20896: a one-header auth bypass shipped in the official Docker image
The official Gitea Docker image shipped `REVERSE_PROXY_TRUSTED_PROXIES=*`, so with reverse-proxy auth enabled, an unauthenticated internet client became whoever it claimed to be via the X-WEBAUTH-USER header. CVSS 9.8, actively exploited. Fix: Gitea 1.26.3 / 1.26.4.
Supply chain: npm, composer, pip — when your dependencies are the attack
Supply chain attacks via package managers: typosquatting, dependency confusion, maintainer compromise, and how to protect yourself.
Sources
Related services
If this topic maps to a real risk in your stack, these are the most relevant CleanIssue audits.