Skip to content

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

High severity GitHub Reviewed Published Jun 16, 2026 in ohler55/oj • Updated Jun 19, 2026

Package

bundler oj (RubyGems)

Affected versions

< 3.17.3

Patched versions

3.17.3

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).

References

@ohler55 ohler55 published to ohler55/oj Jun 16, 2026
Published to the GitHub Advisory Database Jun 19, 2026
Reviewed Jun 19, 2026
Last updated Jun 19, 2026

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

EPSS score

Weaknesses

Out-of-bounds Read

The product reads data past the end, or before the beginning, of the intended buffer. Learn more on MITRE.

Out-of-bounds Write

The product writes data past the end, or before the beginning, of the intended buffer. Learn more on MITRE.

CVE ID

CVE-2026-54592

GHSA ID

GHSA-3m6q-jj5j-38c9

Source code

Credits

Loading Checking history
See something to contribute? Suggest improvements for this vulnerability.