# GitLab RCE via Chained Oj JSON Parser Memory-Safety Flaws (Code Execution as 'git' User)

> Security researcher Yuhang Wu (depthfirst / Open Defense Initiative) chained two memory-safety bugs in Ruby's Oj native JSON parser — an unchecked nesting-depth stack write and a 16-bit key-length integer narrowing that leaks a heap pointer — into a full remote code execution chain against self-managed GitLab CE/EE via crafted Jupyter notebook (.ipynb) diffs. Any authenticated user with ordinary push and commit-diff-view access can trigger the chain to run arbitrary commands as the 'git' system user, with no admin rights, CI/CD access, or victim interaction required.

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

## Description

GitLab's commit-diff renderer for Jupyter notebooks (lib/gitlab/diff/rendered/notebook/diff_file.rb, via the in-tree gems/ipynbdiff gem) feeds repository-controlled notebook JSON directly into Oj::Parser.usual.parse, a high-performance native-C JSON parser. Researcher Yuhang Wu found two independent memory-safety bugs in Oj's native extension code that, combined, produce a reliable RCE primitive reachable from ordinary GitLab push/diff-view privileges.

The first bug is an unchecked nesting-depth stack write in ext/oj/parser.h: opening a JSON array increments a 'depth' counter and selects a callback-table entry without any bounds check against Oj's fixed internal arrays. Driving depth past 1,024 produces a forward sweep of repeated 0x01 bytes (the ARRAY_FUN selector) across adjacent parser memory, eventually flipping the low byte of the parser's internal buf.head pointer at depth 2,132. A subsequent 4,001-digit number forces a realloc() onto the corrupted, attacker-influenced address, and a 446-element Ruby Array reclaims that memory via rb_ary_new_from_values(), letting the attacker overwrite two adjacent VALUE slots that encode the parser's p->start callback pointer.

The second bug is a 16-bit integer-narrowing flaw in Oj's union _key length fields (ext/oj/usual.h): submitting a JSON object key of length 0x10000+29 (65,565 bytes) truncates the stored length to 16 bits, yielding 29. The parser then reads the key back through its short inline buffer path even though the data was written through the heap/pointer path, disclosing a heap pointer at bytes 6-13 of the returned 29-byte slice. That pointer establishes an offset from a known heap allocation to the libruby/libc base, letting the attacker defeat ASLR via a timing-based self-loop probe (writing candidate_base + 0x8470e, i.e. a jmp $, into p->start and observing request timeouts vs. clean HTTP 200 responses).

With ASLR defeated, the attacker rewrites the hijacked callback pointer to a short libruby/libc gadget chain (libruby+0x269b4a -> libruby+0x22d565 -> system()) and supplies a command string (up to 39 bytes) via a trigger blob. The full chain is delivered as two crafted Jupyter notebooks pushed in a single commit ('a01' corrupts parser/heap state and raises a trailing exception; 'a02' triggers the corrupted p->start callback when its diff is viewed), executing arbitrary commands in the context of the GitLab Puma worker process running as the 'git' system user — which has read/write access to all repository storage on the instance.

The underlying Oj bugs were introduced in Oj 3.13.0 (released 2021-08-08) and shipped in GitLab from 15.2.0 onward (2022-07-07), remaining unpatched for roughly 1,753 days. Both bugs were fixed upstream in Oj 3.17.3 (2026-06-04); GitLab shipped the fix in 18.10.8, 18.11.5, and 19.0.2 (2026-06-10). GitLab's official patch-release advisory for that release lists 12 unrelated CVEs and does not credit or reference this specific Oj/RCE chain, and no CVE identifier or CVSS score has been assigned to the chain itself as of publication (2026-07-25) — distinct from the eleven individually-CVE'd Oj bugs (CVE-2026-54500, 54502, 54592, 54896-54903) fixed in the same 3.17.3 release, none of which maps 1:1 to the two primitives used here. A working PoC and Docker demo environment (targeting GitLab 18.11.3) are public at github.com/wupco/gitlab-rce-demo.

## MITRE ATT&CK

- T1595 Active Scanning
- T1190 Exploit Public-Facing Application
- T1078 Valid Accounts
- T1203 Exploitation for Client Execution
- T1059 Command and Scripting Interpreter
- T1068 Exploitation for Privilege Escalation
- T1055 Process Injection
- T1211 Exploitation for Stealth
- T1083 File and Directory Discovery
- T1005 Data from Local System
- T1587 Develop Capabilities
- T1552 Unsecured Credentials
- T1033 System Owner/User Discovery
- T1213 Data from Information Repositories
- T1095 Non-Application Layer Protocol
- T1565 Data Manipulation

## Sources

- [GitLab Vulnerabilities Enable Code Execution](https://cybersecuritynews.com/gitlab-vulnerabilities-enable-code-execution/)
- [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)
- [wupco/gitlab-rce-demo (PoC repository)](https://github.com/wupco/gitlab-rce-demo)
- [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/)
- [Oj: Stack Buffer Overflow in Oj::Doc#each_child via Deeply Nested Input (CVE-2026-54592)](https://github.com/advisories/GHSA-3m6q-jj5j-38c9)
- [Oj: Use-After-Free in Oj::Parser array_class/hash_class GC Marking (CVE-2026-54901)](https://github.com/advisories/GHSA-vwm4-62gf-x745)
- [Oj: Use-After-Free in Oj::Parser SAJ Callback via Input Mutation (CVE-2026-54898)](https://github.com/advisories/GHSA-q2gm-54r6-8fwm)
- [Oj: Heap Buffer Overflow in Oj.dump Exception Serialization via Large Indent (CVE-2026-54896)](https://github.com/advisories/GHSA-35w3-pjm6-wj95)
- [CVE-2026-54500: Oj Ruby Gem Information Disclosure Flaw](https://www.sentinelone.com/vulnerability-database/cve-2026-54500/)
- [CVE-2026-54900: Oj Ruby Gem Buffer Overflow Vulnerability](https://www.sentinelone.com/vulnerability-database/cve-2026-54900/)
- [CVE-2026-54899 - Oj: Use-After-Free in Oj::Parser Symbol Key Cache Toggle](https://cvefeed.io/vuln/detail/CVE-2026-54899)
- [CVE-2026-54903: Oj Integer Overflow / Heap Corruption via >2GB JSON Strings](https://www.strix.ai/cve/CVE-2026-54903)
- [Researcher Publishes GitLab RCE PoC Letting Authenticated Users Run Commands as Git](https://www.techaiapp.com/cybersecurity/researcher-publishes-gitlab-rce-poc-letting-authenticated-users-run-commands-as-git/)
- [Researcher Publishes GitLab RCE PoC Letting Authenticated Users Run Commands as Git](https://www.guardianmssp.com/2026/07/25/researcher-publishes-gitlab-rce-poc-letting-authenticated-users-run-commands-as-git/)
- [depthfirst | Autonomous Security from Design to Production](https://depthfirst.com/)

## 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-1691
