VYPR
High severity7.5GHSA Advisory· Published Jun 19, 2026· Updated Jun 19, 2026

Oj: Stack Buffer Overflow in Oj::Doc#each_child via Deeply Nested Input

CVE-2026-54592

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

1

Patches

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

2

News mentions

0

No linked articles in our index yet.