# GitLab RCE Chain via Malicious Jupyter Notebooks Exploiting Oj Ruby JSON Parser Flaws

> A public exploit chain combines an unchecked nesting-stack write and a 16-bit key-length integer-truncation heap-pointer leak in the Oj Ruby JSON parser, reached through GitLab's ipynbdiff Jupyter Notebook diff renderer, to give any authenticated user with commit/push access remote code execution as the 'git' OS account on self-managed GitLab CE/EE.

- **Published:** 2026-07-27T00:00:00Z
- **Last reviewed:** 2026-07-27T00:00:00Z
- **Canonical:** https://intel.threadlinqs.com/threat/TL-2026-1715
- **ID:** TL-2026-1715
- **Severity:** CRITICAL
- **Category:** VULNERABILITY
- **Status:** ACTIVE
- **Detections:** 9 · **IOCs:** 27 (full data via the Threadlinqs MCP server — Purple tier)

## Description

Security researcher Yuhang Wu of depthfirst AI (Open Defense Initiative) published "Going depthfirst: Achieving GitLab RCE via Two Ruby Memory Corruption Vulnerabilities," documenting an authenticated remote-code-execution chain in self-managed GitLab Community and Enterprise Edition. GitLab renders diffs of committed Jupyter Notebook (.ipynb) files by handing repository-controlled JSON to the ipynbdiff gem, which calls Oj::Parser.usual.parse — Oj being a native-C-extension Ruby JSON parser. Two long-standing memory-safety bugs in Oj, present since an August 2021 commit (Oj 3.13.0) and reachable in GitLab since notebook-diff parsing switched to Oj in GitLab 15.2.0 (July 2022), combine into a full exploit primitive.

The first bug is an unchecked nesting-stack write inside Oj::Parser.usual.parse: opening a JSON array increments a depth counter and writes a one-byte ARRAY_FUN collection selector (0x01) into a fixed-size 1,024-byte in-struct parser stack without bounds checking. A crafted notebook nesting 2,132 JSON array brackets sweeps past that boundary; depth 1,024 produces the first out-of-bounds write at offset P+0x414, and the sweep of 0x01 bytes continues toward and corrupts the parser's buf.head pointer at offset P+0x868 (from P+0x880 to P+0x801). A companion 4,001-digit JSON number literal forces the parser to realloc() its internal buffer, moving the forged/corrupted pointer through jemalloc's tcache free-list, where it is reclaimed by a subsequent 446-element Ruby Array allocation (rb_ary_new_from_values()); array elements 176-177 assemble the target callback-pointer value via immediate Float/Fixnum encoding. The second bug is an unsafe narrowing of a JSON object key length (a size_t) into a signed 16-bit field: a 65,565-byte (0x10000+29) key truncates its stored length to 29, but Oj still allocates and copies the full external buffer while later reading it through the truncated inline view, leaking 8 bytes of a live heap pointer (at inline offsets buf[6]-buf[13]) that GitLab renders into the commit-diff HTML via Ruby's Hash#inspect representation.

Exploitation requires only an ordinary authenticated GitLab user who can push commits and view commit diffs — no administrator rights, CI/CD runner access, or victim interaction. The attacker commits two crafted notebooks in one push: the first (a01.ipynb, with the nested-array and oversized-key payloads) corrupts the parser's start callback pointer and leaks the heap address when its diff is rendered via GET /<project>/-/commit/<sha>/diffs_stream; the leaked pointer is used as a landmark to search a band of page-aligned candidate library bases (probing each with a callback write to candidate_base+0x8470e, a libruby 'eb fe' infinite-jmp gadget, so an incorrect guess hangs the parse and a correct one returns quickly — a process the depthfirst writeup describes taking roughly 5-10 minutes against a fresh worker). Once the libruby/libc bases are resolved, a second notebook (a02.ipynb), processed by the same vulnerable Oj::Parser.usual.parse singleton instance in the same long-lived Puma worker (shared across threads), supplies a NUL-terminated command blob (staged at register R12, max 39 bytes) and drives execution through two libruby gadgets — G1 at libruby+0x269b4a (moves the command blob into RDI and calls the function pointer at offset 0x28) and G2 at libruby+0x22d565 (adjusts the stack for System V ABI compliance) — into a final call to libc's system() at libc+0x58750, executing the attacker-supplied OS command as the git account that owns the Puma worker process. A public, MIT-licensed proof of concept (github.com/wupco/gitlab-rce-demo) demonstrates the full chain end-to-end against GitLab 18.11.3 on x86-64 inside a disposable Docker Compose lab, using a precomputed offset lookup table (table.json) and listener/exploit driver scripts (run_poc.sh, setup_env.sh); per the researcher, retargeting another GitLab build on the same architecture would generally need only minor adjustments to the hardcoded gadget/symbol offsets, not a change to the underlying reachable path.

depthfirst reported the two Oj bugs to the Oj maintainer on 2026-05-21; fixes merged 2026-05-27 and shipped in Oj 3.17.3 on 2026-06-04. depthfirst privately disclosed the full end-to-end GitLab exploit chain on 2026-06-05; GitLab confirmed it on 2026-06-08 and shipped GitLab 18.10.8, 18.11.5, and 19.0.2 (bundling Oj 3.17.3) on 2026-06-10 — notably filed as a bug fix rather than a security advisory, so it does not appear in GitLab's own security release notes. No CVE or CVSS score was requested or assigned to either of the two chained bugs or to the GitLab exploit chain itself; the broader Oj security audit behind this research reportedly triaged 181 candidate vulnerabilities, confirming 77 as memory-safety flaws, of which ten were published as separate GitHub Security Advisories (one on 2026-06-05, nine on 2026-06-16) covering stack/heap buffer overflows, multiple use-after-frees, a negative-size memcpy, and an integer overflow — none of which is the specific write/read primitive used in this GitLab chain. Successful exploitation exposes GitLab repository source code, Rails application secrets, CI/CD and service credentials (personal, project, group, deploy, OAuth, runner, and registry tokens), and any internal service reachable from the git account; there is no public evidence of in-the-wild exploitation and the vulnerability chain is not listed in the CISA KEV catalog as of publication, but the public PoC materially lowers the bar for exploitation of any unpatched self-managed instance.

## MITRE ATT&CK

- T1592.002 Software
- T1588.005 Exploits
- T1588.006 Vulnerabilities
- T1587.004 Exploits
- T1190 Exploit Public-Facing Application
- T1078 Valid Accounts
- T1059.004 Unix Shell
- T1068 Exploitation for Privilege Escalation
- T1211 Exploitation for Stealth
- T1620 Reflective Code Loading
- T1552 Unsecured Credentials
- T1552.001 Credentials In Files
- T1528 Steal Application Access Token
- T1082 System Information Discovery
- T1098 Account Manipulation
- T1021 Remote Services
- T1005 Data from Local System
- T1213.003 Code Repositories
- T1571 Non-Standard Port

## Sources

- [GitLab RCE Flaws via Malicious Jupyter Notebooks](https://gbhackers.com/gitlab-rce-flaws-malicious-jupyter-notebooks/)
- [Going depthfirst: Achieving GitLab RCE via Two Ruby Memory Corruption Vulnerabilities](https://depthfirst.com/research/going-depthfirst-achieving-gitlab-rce-via-two-ruby-memory-corruption-vulnerabilities)
- [Researcher Publishes GitLab RCE PoC Letting Authenticated Users Run Commands as Git](https://thehackernews.com/2026/07/researcher-publishes-gitlab-rce-poc.html)
- [GitLab Oj notebook-diff authenticated RCE chain](https://threat.wiki/ops/gitlab-oj-notebook-diff-authenticated-rce/)
- [GitLab Vulnerabilities Allow Attackers to Execute Remote Code on Default GitLab Installations](https://cybersecuritynews.com/gitlab-vulnerabilities-enable-code-execution/)
- [Public Exploit Lands for GitLab Bug Patched Without a CVE](https://www.cyberkendra.com/2026/07/public-exploit-lands-for-gitlab-bug.html)
- [GitLab Vulnerabilities Allow Attackers to Execute Remote Code on Default GitLab Installations](https://www.cryptika.com/gitlab-vulnerabilities-allow-attackers-to-execute-remote-code-on-default-gitlab-installations/)
- [wupco/gitlab-rce-demo — public GitLab Oj RCE proof-of-concept](https://github.com/wupco/gitlab-rce-demo)
- [Oj: Stack Buffer Overflow in Oj::Doc#each_child via Nested Input (CVE-2026-54592 / GHSA-3m6q-jj5j-38c9)](https://github.com/advisories/GHSA-3m6q-jj5j-38c9)
- [CVE-2026-54900 Detail (Oj negative-size memcpy in create_id attribute handling)](https://nvd.nist.gov/vuln/detail/CVE-2026-54900)
- [ohler55/oj Security Advisories](https://github.com/ohler55/oj/security/advisories)
- [GitLab Patch Release: 19.0.2, 18.11.5, 18.10.8](https://docs.gitlab.com/releases/patches/patch-release-gitlab-19-0-2-released/)

## Full data

Detection queries (Splunk SPL / Microsoft KQL / Sigma) and IOC values require the Threadlinqs MCP server (Purple tier): https://intel.threadlinqs.com/mcp

Canonical: https://intel.threadlinqs.com/threat/TL-2026-1715
