A newly disclosed exploit chain has revealed how two memory corruption vulnerabilities buried inside GitLab’s JSON processing pipeline can be combined to achieve remote code execution on default self-managed GitLab installations. Rather than exploiting GitLab’s own application logic, the attack abuses vulnerabilities in Oj (Optimized JSON), a native C extension used by Ruby applications for high-performance JSON parsing. By chaining a limited memory write primitive with an information disclosure vulnerability, researchers demonstrated reliable code execution as the git system user, potentially exposing repositories, application secrets, and internal infrastructure.
The research was presented by Yuhang Wu of Depthfirst as part of the Open Defense Initiative. Using an automated vulnerability analysis framework, the team analyzed Oj, one of the most widely deployed JSON parsing libraries in the Ruby ecosystem. The automated analysis identified eighteen high-priority vulnerabilities, seven of which were memory-safety issues. Two of those bugs, both present for nearly five years before discovery, ultimately formed the basis of a fully working GitLab remote code execution chain.
Unlike many recent GitLab vulnerabilities that relied on server-side request forgery, deserialization bugs, or authorization bypasses, this attack targets the memory-unsafe native code running underneath an otherwise memory-safe Ruby application. The research serves as another reminder that memory-safe languages such as Ruby can still inherit severe security risks when native extensions written in C are part of the application stack.
How GitLab Processes Jupyter Notebook Files
The attack revolves around GitLab’s handling of Jupyter Notebook (.ipynb) files.
A notebook file is simply a JSON document containing metadata, code cells, markdown cells, execution outputs, and notebook configuration. Whenever GitLab displays differences between notebook revisions, it uses an internal component named ipynbdiff to generate a readable visual diff instead of showing raw JSON.
Before the notebook can be rendered, GitLab validates the uploaded JSON using the Oj parser. One of the validation steps checks that the notebook contains the expected cells field before continuing with diff generation. Since the notebook itself is attacker-controlled JSON, every authenticated user capable of pushing commits and viewing commit diffs can supply arbitrary JSON input directly to Oj’s native parser.
This parsing operation became reachable beginning with GitLab 15.2.0, when GitLab integrated the affected parsing path during notebook diff processing. As a result, the vulnerable native parser became exposed through normal GitLab functionality without requiring administrators to enable any special features.
The Two Vulnerabilities Behind the Attack
Individually, neither vulnerability appears capable of producing remote code execution.
The first vulnerability exists inside Oj::Parser.usual.parse. During parsing, Oj maintains an internal stack representing nested JSON objects and arrays. The parser failed to properly validate the maximum nesting depth before writing to this internal stack. An attacker could repeatedly overwrite a single byte beyond the intended boundary, producing only a very limited memory corruption primitive.
Normally, a one-byte overwrite is not enough to hijack execution. Modern allocators, compiler protections, and address randomization significantly reduce the usefulness of such small corruptions.
The second vulnerability involved an unsafe narrowing of a JSON key length into a 16-bit value. Under carefully crafted conditions, this caused Oj to disclose a fixed slice of heap memory containing approximately twenty-nine bytes. Although relatively small, the disclosed memory included enough information to recover a heap pointer.
Information disclosure vulnerabilities are often underestimated, but they frequently become the missing piece required to bypass modern exploit mitigations. Address Space Layout Randomization (ASLR) relies on attackers being unable to determine where executable code and libraries reside in memory. Once even a small memory leak reveals valid pointers, attackers can calculate the locations of important runtime components such as libc and libruby.
Why Combining Both Bugs Changes Everything
Modern exploitation rarely depends on a single catastrophic vulnerability. Instead, attackers combine multiple individually weak bugs into a reliable exploit chain.
The researchers first abused the unchecked nesting depth to corrupt an internal parser pointer. Through carefully controlled heap allocations, they manipulated the allocator so that a Ruby Array overlapped with memory containing one of Oj’s callback pointers.
The subsequent heap leak then disclosed enough address information to defeat ASLR. Once library addresses became known, the corrupted callback pointer could be redirected toward a controlled target.
Because GitLab’s Puma application server processes multiple requests using shared worker threads, both malicious notebook files could be handled within the same parser instance during a single commit diff request. The first notebook established the corrupted parser state, while the second activated the overwritten callback pointer. Ultimately, execution reached the standard C library’s system() function, allowing arbitrary shell commands to execute as the git user.
Although the exploit relies on several low-level implementation details, the overall attack demonstrates a classic modern exploitation strategy:
- Gain a limited memory write.
- Leak process memory.
- Bypass ASLR.
- Corrupt a function pointer.
- Redirect execution to existing executable code.
This approach is significantly more reliable than attempting to inject executable payloads into modern protected processes.
Why Native Ruby Extensions Continue to Create Security Risks
Ruby itself is considered memory-safe because the language runtime prevents direct pointer arithmetic and out-of-bounds memory access.
However, performance-sensitive operations frequently rely on native extensions written in C. Oj exists precisely because parsing JSON entirely in Ruby is slower than parsing it with optimized native code.
Native extensions execute inside the Ruby interpreter with the same privileges as the application itself. Consequently, any memory corruption inside those extensions immediately becomes part of the application’s attack surface.
This is an increasingly important supply chain concern. Organizations often assume that applications written in memory-safe languages automatically inherit those safety guarantees. In reality, the security properties of the entire application become limited by its least secure native dependency.
The Oj research illustrates this clearly. GitLab itself was not introducing the memory corruption. Instead, its legitimate use of a trusted third-party parsing library exposed a pathway for remote code execution.
Required Access for Exploitation
One of the most concerning aspects of the attack is the relatively modest permissions required.
An attacker only needs:
- A valid authenticated GitLab account.
- Permission to push commits to a repository.
- Permission to view commit differences.
No administrator privileges are necessary. No CI/CD runner access is required. No victim interaction is involved. No browser exploitation or client-side compromise occurs. Because many collaborative GitLab deployments grant developers commit access across internal projects, these permission requirements are considered relatively low for enterprise environments.
Potential Impact
Successful exploitation results in arbitrary command execution under the git account, the account responsible for GitLab’s repository operations.
Depending on the deployment, attackers could potentially access repository contents, Rails application secrets, configuration files, authentication tokens, SSH keys, service credentials, and internal services reachable from the GitLab host. The compromise may also provide a foothold for lateral movement inside corporate infrastructure if additional credentials or trusted network paths are exposed.
While execution does not initially occur as root, compromising the git account on a production GitLab server often provides access to highly sensitive development assets, making the impact severe for software supply chains.
Affected Versions
GitLab has released patches for all supported branches.
| Product | Vulnerable Versions | Fixed Version |
|---|---|---|
| GitLab CE/EE | 15.2.0 through 18.10.7 | 18.10.8 |
| GitLab CE/EE | 18.11.0 through 18.11.4 | 18.11.5 |
| GitLab CE/EE | 19.0.0 through 19.0.1 | 19.0.2 |
| Oj Gem | 3.13.0 through 3.17.1 | 3.17.3 |
GitLab.com had already been updated before public disclosure, and GitLab Dedicated customers were protected without requiring customer action. Self-managed deployments running affected versions should upgrade immediately.
Timeline
The vulnerable parser code entered Oj several years before its discovery.
GitLab later incorporated the affected parsing path during notebook diff processing in July 2022 with GitLab 15.2.0. The underlying Oj vulnerabilities remained undiscovered for approximately 1,753 days before researchers reported them on May 21, 2026. Oj merged fixes on May 27 and released version 3.17.3 on June 4. The GitLab-specific exploit chain was reported on June 5, confirmed on June 8, and GitLab shipped fixes on June 10 through versions 18.10.8, 18.11.5, and 19.0.2.
More Vulnerabilities Found in Oj
The exploit chain used only two vulnerabilities, but the broader audit uncovered several additional security issues affecting Oj.
The published advisories include multiple stack and heap buffer overflows, several use-after-free conditions affecting parser callbacks and document iterators, integer overflow scenarios, and other memory corruption bugs that were addressed in Oj 3.17.3. While not all of these vulnerabilities are known to be exploitable in GitLab, they reinforce how extensively memory-safety defects can accumulate inside mature native libraries over time.
Mitigation Recommendations
Organizations operating self-managed GitLab instances should prioritize upgrading to a supported patched release. Systems outside GitLab’s supported maintenance window should be upgraded to a currently supported version because no backported fixes are available.
Security teams should also review software dependency inventories for native Ruby extensions. Libraries implemented in C, even when used by applications written in memory-safe languages, deserve the same level of scrutiny as standalone native software. Automated dependency scanning, routine patch management, and software composition analysis can significantly reduce the likelihood of similar supply chain risks remaining unnoticed for years.
Administrators may also wish to review recent activity involving unusually crafted Jupyter Notebook files or unexpected notebook diff requests, although patching remains the only reliable mitigation for this vulnerability. Public proof-of-concept code has now been released, increasing the likelihood of exploitation attempts against unpatched self-managed installations.
Conclusion
This research highlights a growing reality in application security: adopting a memory-safe language does not eliminate memory corruption risks if critical functionality depends on native extensions. GitLab itself was not directly vulnerable because of unsafe Ruby code. Instead, a trusted C-based JSON parser introduced an exploitable weakness that ultimately allowed authenticated users to execute arbitrary commands on default installations.
The attack also demonstrates how modern exploitation increasingly relies on chaining together individually modest vulnerabilities rather than discovering a single critical flaw. A one-byte overwrite and a small heap disclosure would normally appear low impact in isolation, yet together they became sufficient to bypass modern exploit mitigations and achieve reliable remote code execution. For organizations running self-managed GitLab, prompt patching is the most effective defense, while the broader lesson extends far beyond GitLab: every native dependency embedded inside a memory-safe application should be treated as part of the application’s primary attack surface.









