GitLab closed a code-execution hole in its self-managed servers on June 10. It did not call it one. The Oj JSON-parser bump that fixed the bug sits under bug fixes in the release notes, with no CVE, no severity score, and no mention of the notebook chain it closed. An administrator who triages updates by reading the security-fix table had no reason to rush it. On July 24 the researchers who reported the flaw, depthfirst, published a working exploit. The servers still on the old build are now the ones a public tool aims at.
This is the rare vulnerability story where the technical detail is not the hard part. The hard part is that the fix arrived unlabeled, so the exposure is as much about how teams schedule updates as about the bug itself. We wrote up GitLab's session-hijack patch earlier this year; this one is quieter and, for anyone who defers non-security releases, more dangerous.
What the flaw is, at a defender's level
Two memory-corruption bugs live in Oj, a fast Ruby JSON parser whose core is written in native C. A Jupyter notebook is, underneath, a JSON document. GitLab's own ipynbdiff gem feeds those notebook files straight from the repository into Oj::Parser.usual.parse when it draws a commit diff, and that call runs within a Puma worker process that stays alive across many requests. So bytes an attacker chose land in hand-managed C memory, inside the running application rather than off in some sandbox.
The first overruns a 1,024-byte buffer that caps how deep JSON nesting can go, and the overflow corrupts the parser's own bookkeeping. The second mishandles a very large object key in a signed 16-bit length field, and the miscount leaks an 8-byte memory address that ends up drawn into the diff on screen. Used together, they reveal enough of the process layout to find its loaded libraries, then bend one of the parser's callbacks toward running a command. depthfirst withheld the working payload; so will we, because a defender needs the mechanism, not the shellcode.
Who can pull the trigger, and what they reach
Read the precondition carefully, because it is the part most summaries blur. This is not an unauthenticated, internet-facing bug. It wants a logged-in project member who is allowed to push, who then opens the diff of a notebook they just committed. From there the command runs as the git account sitting behind those Puma workers. That account can touch the repositories themselves, GitLab's Rails secret keys, the credentials wired to connected services, pipeline data, and internal-only endpoints.
Here is why "authenticated" should not calm you down. On most self-managed GitLab instances, push access is granted broadly: every developer, a pile of contractors, service accounts, and anyone holding a personal access token that leaked into a log or a laptop. A single phished developer session is enough. In your risk model this behaves much closer to a remote takeover than to a narrow privilege bug, and it should be triaged that way.
| GitLab release train | Affected versions | First fixed |
|---|---|---|
| Up to 18.10 | 15.2.0 up to 18.10.7 | 18.10.8 |
| 18.11 series | 18.11.0, 18.11.1, up to 18.11.4 | 18.11.5 |
| 19.0 series | 19.0.0 and 19.0.1 | 19.0.2 |
The gap between the patch and the exploit is the story
The vulnerable code was not new. depthfirst reports the flawed logic sat in Oj for roughly 1,753 days before anyone reported it, and GitLab began routing notebook diffs through the affected call in version 15.2.0 back in mid-2022. The recent sequence is fast, though: reported to Oj on May 21, fixed upstream and released as Oj 3.17.3 on June 4, disclosed to GitLab on June 5, confirmed June 8, patched June 10. The public exploit landed on July 24.
Six weeks separated the fix from the exploit. depthfirst reports no sign of anyone using this in the wild yet, and the published exploit only works against an 18.11.3 server on x86-64, so pointing it at another build or a different chip takes real effort. Treat that as a short runway, not immunity. The memory offsets that make an exploit portable are exactly what a motivated attacker files down over a weekend.
Why an unlabeled fix is its own exposure
The Hacker News reviewed the June 10 release notes and found the Oj 3.17.3 upgrade listed under bug fixes rather than in the security-fix table, with no CVE assigned and no CVSS score. GitLab.com, the managed service, is patched centrally, so its users are already covered. The people carrying the risk are self-managed operators, and many of them run a patch cadence that fast-tracks security releases and lets feature and bug-fix updates wait for a maintenance window.
That cadence is reasonable, and it is exactly what this flaw slips through. When a remote code execution fix ships without the labels your triage keys on, the label is the vulnerability. It is the same lesson as when a trusted dependency shipped the problem with a valid signature: the control everyone relies on to sort urgent from routine quietly passed the dangerous thing through. Note too that GitLab's security-maintained patch trains do not backport this to 15.2 through 18.9, so older self-managed installs get no fix at all short of a real upgrade.
Detection while you are still on the old build
Patching closes the hole. It does not tell you whether someone already probed it. Because the trigger is a notebook diff render inside a Puma worker, the useful signals sit on the host, not at the perimeter:
-
Puma workers crashing or segfaulting around notebook-diff rendering. A failed exploit attempt is far noisier than a clean one, and repeated worker restarts tied to diff views are worth a look.
-
Unexpected child processes under the
gitaccount. That account runs application code, so a shell, a network client, or an interpreter spawning beneath it is a strong lead. -
Bursts of
.ipynbcommits and diff views from a new, low-privilege, or rarely-active account. The exploit needs the attacker to push notebooks and open their diffs, which is an odd pattern for most real users.
None of these is a signature you can buy off the shelf, which is the point of watching host behavior rather than waiting for a rule. It is also worth pulling the list of who actually holds push access right now; the answer is usually longer than anyone expects.
Patch GitLab, then audit your own Ruby
The first action is unambiguous: upgrade self-managed GitLab to 18.10.8, 18.11.5, or 19.0.2, whichever train you are on. There is no workaround, and the fixed builds carry the corrected Oj.
The wider action is the one the wire coverage skips. The root bugs are not GitLab's code; they are in Oj, a general-purpose gem that thousands of Ruby applications pull in, often without knowing it sits under a higher-level JSON call. Any Ruby service that parses untrusted JSON through Oj 3.13.0 to 3.17.1 is worth the same scrutiny, and the fix is Oj 3.17.3 or later. Audit your dependency trees for native C extensions handling attacker-reachable input; that is where memory safety leaves the language and becomes your problem.
There is a pattern behind the discovery worth sitting with. depthfirst found these with an automated system that ranked 18 candidate flaws in Oj, seven of them memory-safety bugs, before a human chained two into an exploit. It is the same shape as when an AI system flags bugs in old, trusted code, when researchers reopened a filesystem audited years earlier, and when a vendor's own AI found the flaws in its appliances. The economics of finding old memory-corruption bugs in widely-used native code just changed. The decade-old dependency you never think about is now cheap to fuzz at scale, and the update that fixes it may not arrive wearing a label that tells you to hurry.