Key Takeaways
- Two long‑standing memory‑safety bugs in the Oj JSON parser (an unchecked nesting‑stack write and an unsafe 16‑bit key‑length narrowing) were chained to achieve remote code execution on default GitLab installations.
- The exploit requires only ordinary push and diff‑view privileges; no admin rights, CI/CD access, or user interaction are needed.
- Successful execution runs as the “git” system user, exposing source code, Rails secrets, service credentials, and any internal services reachable from the GitLab host.
- Affected GitLab CE/EE versions span 15.2.0 through 18.10.7, 18.11.0‑18.11.4, and 19.0.0‑19.0.1; patches are available in 18.10.8, 18.11.5, and 19.0.2 (Oj 3.17.3).
- Organizations should upgrade immediately, audit Ruby gem dependencies for native C extensions, and consider runtime mitigations such as ASLR hardening and sandboxing of worker processes.
Background
GitLab is a widely deployed DevOps platform that relies on the Ruby programming language for its core application logic. While Ruby itself provides memory‑safety guarantees, many performance‑critical components are implemented as native C extensions. One such extension is the Oj gem, a high‑speed JSON parser used throughout GitLab to validate and manipulate JSON payloads, including the parsing of Jupyter Notebook (.ipynb) files for diff rendering. Because Oj is written in C, it is susceptible to classic memory‑corruption bugs that can undermine the safety of the surrounding Ruby code when exploited.
Discovery via Automated Analysis
As part of the Open Defense Initiative, researcher Yuhang Wu employed an automated fuzzing and static‑analysis pipeline to scrutinize the Oj library. The system flagged 18 high‑priority issues, seven of which were classified as memory‑safety defects. Notably, two of these defects had remained dormant in the codebase for almost five years before being weaponized. The automated approach allowed the researcher to isolate subtle flaws—such as off‑by‑one writes and integer truncations—that manual review might overlook, demonstrating the value of continuous security testing for native dependencies.
Vulnerabilities Identified
The first flaw resides in Oj::Parser.usual.parse where the parser fails to validate the depth of nested JSON structures. When an excessively deep array or object is encountered, the code writes past the end of a fixed‑size stack buffer, giving an attacker a repeatable one‑byte write primitive. The second flaw is an unsafe narrowing of a 16‑bit length field used to store the length of JSON object keys. By supplying a key longer than 65 535 bytes, the truncation leaks a heap pointer into the parser’s internal state, disclosing a fixed 29‑byte slice of memory. Individually, each primitive is limited: the stack write can only corrupt a single byte per invocation, and the heap leak reveals only a small, predictable memory region.
Exploit Chain Mechanics
To turn these modest primitives into full remote code execution, the attacker must manipulate the heap layout so that the one‑byte write overwrites a critical pointer, while the heap leak provides the address needed to calculate the location of essential libraries (e.g., libc, libruby). The chain proceeds in two stages within a single HTTP request:
-
Stack‑based corruption – The first malicious notebook file contains JSON with an extreme nesting depth (far beyond typical usage). This triggers the unchecked stack write, shifting an internal buffer pointer so that subsequent heap allocations overlap with a Ruby Array’s internal structure. By carefully shaping the preceding heap state, the overflow can replace the
startfield of a Ruby Array callback pointer with an address chosen by the attacker. - Heap‑pointer leak and ASLR bypass – The second notebook file embeds an oversized JSON object key. Because the key length field is narrowed to 16 bits, the parser leaks a heap pointer that is later rendered into the diff’s HTML output. The attacker reads this leaked address from the diff page, calculates the base address of loaded libraries, and thus defeats Address Space Layout Randomization (ASLR).
With the library base known, the attacker can compute the address of system() or a suitable gadget, write it into the corrupted callback pointer, and trigger its execution when the parser invokes the callback during processing of the second file. Because GitLab’s Puma server runs multiple Ruby threads sharing a single native Oj instance per worker, both files are processed by the same vulnerable parser within the same request, allowing the second file to trigger the hijacked callback and execute arbitrary shell commands as the git user.
Proof‑of‑Concept Demonstration
The researcher released a proof‑of‑concept (PoC) showing two specially crafted .ipynb files pushed in a single commit‑diff request. The first file’s deep nesting overwrites the stack buffer; the second file’s oversized key leaks a heap pointer visible in the generated diff HTML. When GitLab renders the diff, the leaked pointer is disclosed to the attacker, who then uses it to compute libc’s location. The corrupted callback subsequently calls system("id"), confirming code execution. The PoC requires only a standard GitLab account with push rights, highlighting the low barrier to exploitation.
Why the Attack Bypasses Defenses
Traditional GitLab SSRF mitigations and input‑validation layers focus on blocking external requests to internal services such as Redis or HTTP endpoints. This exploit, however, operates entirely within the in‑process JSON parser; no network calls are made to internal services. Consequently, SSRF rules, web‑application firewalls, and request‑size limits are ineffective. The attack leverages a trusted native extension, meaning that the memory‑unsafe code runs with the same privileges as the Ruby application itself, bypassing sandboxing that might isolate pure‑Ruby code.
Impact and Privilege Escalation
Execution as the git system user grants the attacker access to everything that the GitLab worker processes can reach:
- All repository source code stored on the host.
- Rails application secrets (e.g.,
secret_key_base, database credentials) located in the GitLab configuration directory. - Service tokens and keys used for integrations with CI/CD runners, container registries, or external APIs.
- Any internal services bound to localhost or accessible via the host’s network stack (e.g., PostgreSQL, Redis, Sidekiq).
From this foothold, an attacker could exfiltrate sensitive data, inject malicious code into repositories, pivot to other internal systems, or maintain persistence by adding unauthorized SSH keys or cron jobs. The vulnerability is especially perilous in multi‑tenant or shared self‑managed GitLab instances, where a single compromised project could jeopardize the entire installation.
Affected Versions and Patch Timeline
The vulnerable Oj parser code was introduced in August 2021, and GitLab began invoking the affected parsing path in July 2022 with the release of GitLab 15.2.0. The impacted version ranges are:
- GitLab CE/EE 15.2.0 – 18.10.7
- GitLab CE/EE 18.11.0 – 18.11.4
- GitLab CE/EE 19.0.0 – 19.0.1
Corresponding patched releases are:
- 18.10.8
- 18.11.5
- 19.0.2
The Oj gem itself was fixed in version 3.17.3, released on June 4 2026, after the upstream bugs had lingered for 1,753 days. GitLab’s security team confirmed the exploit chain on June 8 2026 and rolled out the patches across the three release lines on June 10 2026. GitLab.com was already updated at disclosure, and Dedicated customers required no action; self‑managed operators must upgrade immediately.
Broader Implications for the Ruby Ecosystem
Beyond the two bugs used in this chain, the same audit uncovered nine additional CVEs in Oj, covering stack and heap buffer overflows, use‑after‑free conditions in SAJ callbacks and document iterators, a negative‑size memcpy, and a large‑file integer overflow. This underscores how memory‑corruption risks can lurk deep inside native extensions that are otherwise considered “safe” because they are invoked from memory‑safe Ruby code. Organizations that rely heavily on native gems should treat them as potential attack surfaces and apply the same rigor used for standalone C/C++ projects.
Recommendations for Organizations
- Upgrade Promptly – Apply the patched GitLab releases (18.10.8, 18.11.5, 19.0.2) or newer, which bundle Oj 3.17.3 or later.
- Dependency Auditing – Regularly scan Ruby gemfiles for native C extensions using tools like
bundler-auditorgemnasium, and prioritize updates for any gem with known memory‑safety issues. - Runtime Hardening – Enable ASLR and stack‑smashing protections at the OS level, consider using seccomp or AppArmor profiles to limit system calls from Puma workers, and explore sandboxing technologies (e.g., gVisor, Firecracker) for isolating worker processes.
- Monitoring and Detection – Deploy endpoint detection and response (EDR) agents that can flag abnormal
system()orexecve()calls originating from thegituser, and monitor for unexpected spikes in diff‑rendering CPU usage. - Least‑Privilege Principle – Restrict project members’ permissions where feasible; disable the ability to push notebook files or view diffs for roles that do not require them, reducing the attack surface.
By combining timely patching, diligent dependency management, and layered runtime defenses, organizations can mitigate the risk posed by memory‑unsafe native extensions and preserve the integrity of their GitLab deployments.

