Oj (Ruby gem) CVE-2026-54592
HIGHSeverity by source
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
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.
Primary rating from Vendor (https://github.com/ohler55/oj).
CVSS VectorVendor: https://github.com/ohler55/oj
Lifecycle Timeline
2DescriptionCVE.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:
doc_each_child(~line 1501) incrementsdoc->wherepast 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.
- On the next entry (~line 1478) the function copies the path into a
stack-local buffer:
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
require 'oj'
depth = 200
payload = '[' * depth + '1' + ']' * depth
Oj::Doc.open(payload) do |doc|
r = lambda { doc.each_child { |_| r.call } }
r.call
endRecursion 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.
Same weakness CWE-125 – Out-of-bounds Read
View allSame technique Buffer Overflow
View allVendor StatusVendor
Share
External POC / Exploit Code
Leaving vuln.today
GHSA-3m6q-jj5j-38c9