If you run a self-managed GitLab instance, stop reading this sentence and go check your version number. I'll wait.
On July 24, 2026, a security research group called depthfirst published a working proof-of-concept exploit that lets any authenticated GitLab user—someone who can push to a project, nothing more—run arbitrary commands as the git system account. No administrator rights. No CI runner access. No interaction required from any other user. Just a crafted Jupyter notebook committed to a repository and a diff page loaded in a browser.
GitLab patched this vulnerability on June 10. They didn't call it a security fix. There is no CVE. There is no CVSS score. It appeared in the release notes as a bug fix.
Six weeks passed. The PoC is now public.
What the Exploit Actually Does
The vulnerability lives at the intersection of three things that shouldn't be dangerous: a Ruby JSON parsing gem, a Jupyter notebook renderer, and GitLab's commit diff view. Let me walk through the chain, because understanding it matters for understanding why this class of bug keeps showing up.
GitLab renders human-readable diffs for .ipynb (Jupyter Notebook) files using an in-tree gem called ipynbdiff. When you open a commit diff that touches a notebook, ipynbdiff calls the Oj Ruby gem—a fast, C-backed JSON parser—to validate that the notebook's JSON contains a "cells" field. This is a completely ordinary operation: parse some JSON, look for a key.
The problem is that Oj had two memory corruption bugs that together enable a complete ASLR bypass and arbitrary code execution:
- Nesting stack overflow in
Oj::Parser.usual.parse: Oj's native parser allocates a 1,024-byte nesting stack on the heap for tracking JSON structure depth. A crafted notebook with deeply nested objects writes past that boundary, giving an attacker control of the parser's internalstartcallback pointer. - Unsafe 16-bit key-length narrowing: Object keys longer than 65,535 bytes get their length stored in a signed 16-bit field. A 65,565-byte key truncates to 29 bytes. The resulting mismatch leaks a heap pointer into the rendered diff output that the browser returns.
Chaining these two bugs works like this: the attacker commits a crafted notebook and opens the diff. The nesting overflow positions data for a later write. The key-length truncation leaks a heap pointer that reveals where libc is loaded in memory—defeating address-space layout randomization. On a fresh GitLab install, finding libc takes five to ten minutes; on a system that's been running for months, it can take one to two hours as the heap grows more fragmented.
Then a second and third notebook commit fires the actual payload, redirecting the start callback to call system(). Commands execute as the git account that drives GitLab's Puma web workers.
What the git account can reach: all repository source code, the Rails application's secrets (database credentials, encryption keys, session secrets), CI/CD pipeline data and stored artifacts, service credentials used by integrations, and anything reachable on the internal network from the GitLab host. In most organizations that means the git account is effectively a skeleton key to the software development supply chain.
The Disclosure That Wasn't
The Oj bugs were reported to the gem's maintainer on May 21. Fixes were merged on May 27. Oj 3.17.3 shipped on June 4. The GitLab-specific exploit chain was reported to GitLab on June 5; GitLab confirmed it on June 8 and issued a patch on June 10 across versions 18.10.8, 18.11.5, and 19.0.2.
That timeline is fine. The disclosure process between depthfirst, the Oj maintainer, and GitLab worked reasonably well. But GitLab then made a decision that significantly increased risk for every organization running a self-managed instance: they published the fix as a bug fix, not a security fix. No CVE was filed. No CVSS score was assigned. No security advisory was issued. The release notes mentioned stability improvements to notebook diff rendering.
Think about what that means operationally. Security teams that filter on CVEs and security advisories when prioritizing patches saw nothing. Automated vulnerability scanners found nothing. Organizations that apply security patches aggressively but defer bug-fix releases—a completely reasonable policy—were left exposed without knowing it. Six weeks later, with a public exploit available, those organizations are discovering that their six-week-old "bug fix" backlog contains an active remote code execution vulnerability.
This is not a hypothetical concern. I've run patch management processes for hosting infrastructure. The first question when a patch lands is always: "Is this security-relevant?" When the vendor answers "no," that patch goes into the next scheduled maintenance window, not an emergency change. GitLab's framing cost the broader ecosystem six weeks of informed decision-making.
The Oj Supply Chain Problem
There's a second story inside this one: the Oj bugs had been silently present in the gem for nearly five years before depthfirst found and weaponized them. The nesting stack overflow and the key-length truncation weren't new code; they were old bugs that nobody had bothered to look at closely because Oj had a strong performance reputation and was trusted widely across the Ruby ecosystem.
GitLab itself ships a bundled Oj as a transitive dependency. Most organizations running GitLab have no idea they're running a C extension that parses untrusted JSON data in a process that handles web requests. That's the nature of third-party dependencies: they bring capabilities you want, and they bring attack surface you didn't know you were accepting.
This is exactly the kind of vulnerability that a dependency inventory scan wouldn't catch without knowing what to look for. The gem version was "supported." There was no known CVE. It passed normal dependency checks. The only thing that would have flagged it was either depthfirst's specific research or a memory-safety audit of the C code—neither of which is part of most organizations' routine security practice.
The broader lesson here applies to any polyglot stack where a dynamic language (Ruby, Python, Node) calls native C extensions for performance. Those C extensions don't get the same scrutiny as the higher-level code. They're often old. They often handle data formats (JSON, YAML, XML, image codecs, compression) that are structurally complex and difficult to validate. And they run in the same process as your web workers, with access to everything in memory.
Which Versions Are Affected and What to Do
The affected versions span a wide range, which means a lot of organizations running self-managed GitLab are exposed:
- GitLab CE/EE 15.2.0 through 18.10.7 — upgrade to 18.10.8
- GitLab CE/EE 18.11.0 through 18.11.4 — upgrade to 18.11.5
- GitLab CE/EE 19.0.0 through 19.0.1 — upgrade to 19.0.2
Important: GitLab 15.2 through 18.9 are outside the current patch train. GitLab will not issue backports for those versions. If you're running anything older than 18.10, you need to upgrade to a current release—not just apply a hotfix.
The depthfirst PoC targets x86-64 GitLab 18.11.3 specifically. Retargeting it to other builds on the same architecture requires "minor offset updates" according to the researchers; moving to ARM64 needs substantial rework. That's meaningful context: this isn't a click-and-run tool for any version, but it also isn't a proof-of-concept that exists only in a lab. The barrier to adaptation is low for anyone with memory exploitation experience.
If you cannot patch immediately: GitLab has no official workaround. Restricting untrusted user access to projects—so that only known-good accounts can push commits—reduces your exposure but doesn't eliminate it. In practice, if you have a multi-tenant GitLab instance where developers you don't fully control can push code, you need to treat this as an emergency change.
GitLab.com (the hosted service) is not affected; Anthropic and similar managed services handle patching on your behalf. This is a self-managed instance problem.
What I'd Do If This Were My Infrastructure
I've patched a lot of security issues in running systems over the years. Here's the practical sequence I'd run through this weekend:
- Check your version immediately.
gitlab-rake gitlab:env:infoor look at your Admin > Dashboard page. If you're on any affected version, this is your Friday afternoon. - Review recent commit history for
.ipynbfiles. The exploit requires committing a crafted notebook, so any recent Jupyter notebook additions from unexpected users are worth investigating. This isn't a guarantee of compromise, but it's a reasonable first indicator. - Check your GitLab logs for anomalous processes running as the
gituser. The payload executessystem()calls from a Puma worker. Anything unexpected in process history attributed togitis suspicious. - Upgrade to the patched version and verify the upgrade with
gitlab-rake gitlab:check. - Rotate your Rails secrets and service credentials as a precaution if your instance has been accessible to untrusted users. These are cheap operations that eliminate a large risk surface.
The Bigger Picture
What bothers me most about this incident isn't the vulnerability itself—memory safety bugs in C extensions parsing complex data formats are a known class of problem, and security researchers finding and responsibly disclosing them is exactly how the ecosystem is supposed to work. What bothers me is the disclosure decision.
When you run infrastructure for other people's code, you're managing risk on their behalf. That contract requires timely, accurate information about what's dangerous. A security vulnerability labeled as a bug fix is a fundamental breakdown in that information chain. Every organization that didn't patch because they thought it was a bug fix was making a reasonable decision with inaccurate information.
GitLab's own security advisory process has explicit guidance for what constitutes a security fix. This should have been one. The fact that it wasn't—and that the decision wasn't revisited when a working PoC became available six weeks later—is the part of this story I'll be thinking about for a while.
If you manage self-managed GitLab instances, patch them today. And if your organization's patch prioritization process depends on CVEs and vendor security advisories, this week is a good time to have a conversation about whether that process has an adequate buffer for the cases where vendors get the classification wrong.