
Researchers chained two Oj parser bugs to achieve GitLab RCE via Jupyter notebook diffs, affecting authenticated users on unpatched versions.
Depthfirst researchers published a working remote code execution exploit for GitLab on July 24, chaining two memory corruption bugs in Oj, a Ruby JSON parser with a native C implementation, into full command execution inside a GitLab Puma worker.
Any authenticated user who can push to a project and view commit diffs could run it. The researchers pointed out that no admin rights, no CI access, and no victim interaction are needed. GitLab patched the bugs on June 10 but listed the fix under bug fixes, not the security table, so operators triaging the release had no reason to treat it as urgent.
“The system surfaced 18 prioritized vulnerabilities, including 7 memory-safety bugs. Two of them, an out-of-bounds write and a heap-pointer disclosure, had survived in Oj for nearly five years.” reads the report published by Depthfirst. “Oj is a low-level dependency used by GitLab, and we combined the two bugs to achieve remote code execution on a default GitLab installation.”
The bugs entered Oj’s parser on August 8, 2021. They remained there for 1,753 days. The vulnerable notebook path shipped in GitLab starting with version 15.2.0 in July 2022 and wasn’t fixed until June 10, 2026.
The attack path starts with GitLab’s notebook diff renderer, a small in-tree gem called ipynbdiff that converts Jupyter .ipynb files into human-readable diffs. When a user opens a commit diff for a notebook file, GitLab passes the raw repository bytes to Oj::Parser.usual.parse inside the Puma worker process. That parse call is the entry point.
“The investigation ran in reverse: depthfirst found the vulnerable parser below the application, then traced its callers back to a user-controlled product boundary.” continues the report. “A real request follows that chain in the opposite direction, from a GitLab commit diff into native C code.”
Because Oj::Parser.usual returns a process-wide parser singleton shared across threads in the same Puma worker, corrupting it once affects every subsequent parse in that process.
The first bug is an unchecked nesting stack overflow. Oj keeps track of JSON nesting depth in a fixed 1,024-byte array inside the parser object. When an attacker opens more than 1,024 nested arrays, Oj keeps writing one-byte collection selectors past the end of that buffer, into adjacent parser fields. The attacker can’t control each byte precisely, arrays always write 0x01, but they can control how far the sweep goes. With enough levels, that single byte changes buf.head, the parser’s internal buffer-start pointer, shifting it backward by 127 bytes into memory the parser doesn’t own. From there, a forced realloc caches that forged interior pointer, a Ruby Array allocation reclaims it, and the Array’s element contents, constrained to valid Ruby values but still attacker-influenced, overwrite the parser’s p->start callback pointer with a chosen address.
The second bug provides the address. An oversized object key, 65,565 bytes, gets its length truncated from a size_t to a signed 16-bit integer, which wraps to 29. Oj allocates heap memory for the full key and stores a pointer to it, but the truncated length tells the return path to read 29 bytes from the inline buffer instead. Those 29 bytes happen to contain the heap pointer in bytes 6 through 13. GitLab renders that into the diff as part of the cell ID.
“Returned bytes 0 through 5 came from the external view’s alignment padding. Bytes 6 through 13 were the live key pointer. The final 15 returned bytes came from union storage not written by the external view and could contain stale key-entry contents. This was a fixed 29-byte disclosure, not an arbitrary-address read: it exposed bytes already present in that Key entry and never dereferenced an attacker-selected pointer.” states the report. “These offsets came from the tested x86_64 ABI, so the compiler layout was part of the primitive’s boundary.”
With a heap landmark in hand, the attacker probes candidate libc base addresses by writing a self-looping instruction address into p->start and watching which parse hangs. On a fresh two-worker GitLab install, the ASLR search took five to ten minutes. On a mature long-running instance, the projected time is one to two hours.
Once the library base is recovered, the exploit chains the two primitives in a single diffs_stream request. Two lexically ordered notebook files in the same commit carry the stages: the first notebook arms the callback pointer, raises an exception to avoid immediate execution, and lets GitLab continue streaming diffs. The second notebook’s old side triggers p->start in the same Puma worker, which jumps through two libruby gadgets into system() with the attacker’s command in the right register. Commands run as git, the account behind Puma, which puts source code, Rails secrets, service credentials, and any internal service the application can reach within range.
“Successful exploitation executed commands as git, the account running GitLab’s Puma workers. Its effective reach depends on deployment isolation, but it may include repository data, Rails secrets, service credentials, and internal services available to the application.” concludes the report. “This can lead to source-code disclosure, credential theft, repository modification, persistence, or lateral access.”
GitLab CE and EE versions 15.2.0 through 18.10.7, 18.11.0 through 18.11.4, and 19.0.0 through 19.0.1 are affected across all tiers. Releases before 15.2 used a different JSON parser in this path and aren’t vulnerable. Releases from 15.2 through 18.9 are outside GitLab’s security-maintained patch trains and won’t receive backports — those installs need to move to a supported release. For Helm and Operator deployments, the relevant version is the GitLab version inside the Webservice image running Puma, not the chart version. Upgrade to 18.10.8, 18.11.5, or 19.0.2. No configuration-only workaround has been validated by depthfirst or GitLab.
Follow me on Twitter: @securityaffairs and Facebook and Mastodon
(SecurityAffairs – hacking, GitLab)




