GitLab RCE Chain via Malicious Jupyter Notebooks Exploiting Oj Ruby JSON Parser Flaws — Threadlinqs Intelligence
As of 2026-07-27, GitLab RCE Chain via Malicious Jupyter Notebooks Exploiting Oj Ruby JSON Parser Flaws is a critical-severity vulnerability threat, tracked by Threadlinqs Intelligence with 9 detection rules (Splunk SPL, Microsoft KQL, Sigma) and 27 indicators of compromise.
Threat ID: TL-2026-1715 · Severity: CRITICAL · Status: ACTIVE · Category: VULNERABILITY
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
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
Weaknesses (CWE)
CWE-787, CWE-125, CWE-197, CWE-200
Target sectors: technology, softwaredevelopment, financialservices, government administration, health, criticalinfrastructure, telecoms, education
Target regions: Global
Detections & IOCs
As of 2026-07-27, this threat has 9 detection rule(s) across Splunk SPL, Microsoft KQL and Sigma, and 27 indicator(s) of compromise. Detection query text and full IOC values are available to authenticated users and programmatically via the Threadlinqs MCP server (Purple tier). View plans.
VULNERABILITY, CRITICAL, threat intelligence, cybersecurity, T1592.002, T1588.005, T1588.006, T1587.004, T1190, T1078, T1059.004, T1068, T1211, T1620