Oj: Stack Buffer Overflow in Oj::Doc#each_child via Deeply Nested Input
Description
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.
2. 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
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).
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Affected products
1Patches
Vulnerability mechanics
Root cause
"Missing bounds check on `doc->where` increment and missing decrement after loop in `doc_each_child` allows recursive calls to overflow a fixed-size stack buffer."
Attack vector
An attacker sends a deeply nested JSON payload (e.g., 200 levels of nested arrays) to an endpoint that calls `Oj::Doc.open(untrusted) { |d| d.each_child ... }` recursively. The `Oj::Doc` parser imposes no JSON nesting-depth limit, so the deeply nested input reaches the vulnerable code path. The recursive `each_child` calls increment `doc->where` past the fixed-size array, and on the next entry the `memcpy` overflows the stack-local buffer, triggering a stack canary check failure and aborting the process. This results in a reliable denial of service.
Affected code
The vulnerability resides in `ext/oj/fast.c` in the `doc_each_child` function. The function increments `doc->where` past the fixed-size `where_path[MAX_STACK=100]` array without a bounds check and never decrements it, so recursive calls from the yield block drive `doc->where` beyond the array. On re-entry, the function copies the path into a stack-local `save_path[MAX_STACK]` buffer using a `memcpy` whose length is derived from the corrupted `doc->where`, causing a stack buffer overflow.
What the fix does
The fix in version 3.17.3 adds a bounds check before incrementing `doc->where` and restores `doc->where` after the loop, matching the existing `each_leaf` pattern. When the nesting depth exceeds `MAX_STACK`, the function now raises an `Oj::DepthError` instead of overflowing the buffer. This prevents both the heap OOB write into `struct _doc` fields and the subsequent stack buffer overflow.
Preconditions
- configThe application must call Oj::Doc.open on untrusted JSON input and then invoke each_child recursively inside the block.
- inputThe attacker must supply a JSON payload with nesting depth >= 101 (e.g., 200 levels of nested arrays).
- configNo JSON nesting-depth limit is enforced by the parser, so deeply nested input reaches the vulnerable code path.
Reproduction
```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 with SIGABRT due to stack canary failure.
Generated on Jun 19, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
2News mentions
0No linked articles in our index yet.