Skip to main content

Oj (Ruby gem) CVE-2026-54592

HIGH
Out-of-bounds Read (CWE-125)
2026-06-19 https://github.com/ohler55/oj GHSA-3m6q-jj5j-38c9
7.5
CVSS 3.1 · Vendor: https://github.com/ohler55/oj
Share

Severity by source

Vendor (https://github.com/ohler55/oj) PRIMARY
7.5 HIGH
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
vuln.today AI
7.5 HIGH

Remote unauthenticated JSON input crashes the worker with no C/I impact; stack canary makes the outcome a reliable abort, so A:H and AC:L.

3.1 AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
4.0 AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N
Red Hat
7.5 HIGH
qualitative

Primary rating from Vendor (https://github.com/ohler55/oj).

CVSS VectorVendor: https://github.com/ohler55/oj

Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
High

Lifecycle Timeline

2
Source Code Evidence Fetched
Jun 19, 2026 - 21:26 vuln.today
Analysis Generated
Jun 19, 2026 - 21:26 vuln.today

DescriptionCVE.org

Summary

Oj::Doc#each_child, when invoked recursively over a deeply nested JSON document, overflows a fixed-size stack buffer and aborts the process. This is a denial of service reachable from untrusted JSON.

Details

Two-step chain in ext/oj/fast.c:

  1. doc_each_child (~line 1501) increments doc->where past the

where_path[MAX_STACK = 100] array with no bounds check, and never restores it (doc->where-- is missing). Calling each_child recursively from inside the yield block therefore drives doc->where beyond the array.

  1. On the next entry (~line 1478) the function copies the path into a

stack-local buffer:

c
   Leaf  save_path[MAX_STACK];           // 800-byte stack buffer
   size_t wlen = doc->where - doc->where_path;
   if (0 < wlen) {
       memcpy(save_path, doc->where_path, sizeof(Leaf) * (wlen + 1));
   }

When the previous recursive call left doc->where past where_path[100], wlen exceeds MAX_STACK and the memcpy overflows save_path on the C stack.

The Oj::Doc parser imposes no JSON nesting-depth limit (it relies on a C-stack pressure check), so deeply nested attacker input reaches this path.

Proof of Concept

ruby
require 'oj'
depth = 200
payload = '[' * depth + '1' + ']' * depth
Oj::Doc.open(payload) do |doc|
  r = lambda { doc.each_child { |_| r.call } }
  r.call
end

Recursion depth <= 99 iterates normally; depth >= 101 aborts. lldb backtrace on the affected build (ruby 3.3.8 / arm64-darwin24):

SIGABRT
#2 __abort
#3 __stack_chk_fail
#4 doc_each_child   (oj.bundle, fast.c)

Impact

Reliable denial of service: any endpoint that calls Oj::Doc.open(untrusted) { |d| d.each_child ... } recursively can be crashed with a small deeply-nested payload. On builds with a stack protector (the default, -fstack-protector-strong) the canary aborts the process before the saved return address is used. The Step-1 heap OOB writes into struct _doc fields do occur, but are masked in practice because the Step-2 stack overflow crashes first; turning them into anything beyond a crash has not been demonstrated.

Patches

Fixed in 3.17.3: doc_each_child now bounds-checks before incrementing doc->where (raising Oj::DepthError) and restores doc->where after the loop, matching the existing each_leaf pattern. Verified on the fixed build: depth >= 101 raises a clean Oj::DepthError instead of aborting.

Credit

Reported by Zac Wang (@7a6163).

AnalysisAI

Denial of service in the Oj Ruby JSON parser (versions <3.17.3) allows remote unauthenticated attackers to crash any process that parses untrusted JSON via Oj::Doc.open followed by a recursive each_child call. A deeply nested JSON document drives doc->where past the 100-element where_path array, causing a subsequent memcpy to overflow an 800-byte stack buffer and trigger the stack canary (SIGABRT). No public exploit identified at time of analysis beyond the vendor-published PoC, and EPSS data was not supplied; the issue is not listed in CISA KEV.

Technical ContextAI

Oj is a high-performance Ruby C-extension JSON parser distributed as the rubygems/oj package (CPE pkg:rubygems/oj). The bug lives in ext/oj/fast.c, the SAX-style Oj::Doc API. doc_each_child increments doc->where without bounds-checking against the fixed MAX_STACK=100 where_path[] array and never decrements it after the yield loop. On re-entry, the function computes wlen = doc->where - doc->where_path and memcpy's wlen+1 Leaf entries into the 800-byte stack-local save_path[MAX_STACK], producing an out-of-bounds write past the stack frame. Although NVD tags this CWE-125 (out-of-bounds read), the technical root cause is more precisely a stack-based out-of-bounds write (CWE-121/CWE-787) caused by a missing recursion-depth check - Oj::Doc relies on C-stack pressure rather than an explicit JSON nesting limit. Because Ruby builds typically use -fstack-protector-strong, the canary detects the corruption and aborts before the saved return address is consumed, limiting impact to a crash.

RemediationAI

Vendor-released patch: upgrade the oj gem to 3.17.3 or later, where doc_each_child now bounds-checks doc->where before incrementing it (raising Oj::DepthError) and restores it after the yield loop, mirroring the existing each_leaf pattern; the fix is documented in GHSA-3m6q-jj5j-38c9. If you cannot upgrade immediately, avoid the Oj::Doc.open + recursive each_child pattern on untrusted input - either switch the affected code path to Oj.load / Oj.parse (which use different internals), or enforce an application-level JSON nesting-depth cap (e.g., reject payloads with more than ~50 nested brackets) before handing data to Oj::Doc; both workarounds change parsing semantics and require regression testing on consumers that legitimately handle deep JSON.

Vendor StatusVendor

Share

CVE-2026-54592 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy