CVE-2026-54592
Oj: Stack Buffer Overflow in Oj::Doc#each_child via Deeply Nested Input
Oj (Optimized JSON) is a JSON parser and Object marshaller packaged as a Ruby gem. In versions prior to 3.17.3, Oj::Doc#each_child, when invoked recursively over a deeply nested JSON document, overflows a fixed-size stack buffer and aborts the process, leading to DoS. In a two-step chain in ext/oj/fast.c, doc_each_child increments doc->where past the where_path[MAX_STACK = 100] array with no bounds check and never restores it (the doc->where-- is missing), so calling each_child recursively from inside the yield block drives doc->where beyond the array. On the next entry the function copies the path into the 800-byte stack-local buffer save_path[MAX_STACK] using wlen = doc->where - doc->where_path, so when the previous recursive call left doc->where past where_path[100] the wlen exceeds MAX_STACK and the memcpy overflows save_path on the C stack; because the Oj::Doc parser imposes no JSON nesting-depth limit (relying on a C-stack pressure check), deeply nested attacker input reaches this path. This issue has been fixed in version 3.17.3.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- None
- Integrity
- None
- Availability
- High
Exploit likelihood
What each source reports
CVE List Score used
7.5
Oj: Stack Buffer Overflow in Oj::Doc#each_child via Deeply Nested Input
Oj (Optimized JSON) is a JSON parser and Object marshaller packaged as a Ruby gem. In versions prior to 3.17.3, Oj::Doc#each_child, when invoked recursively over a deeply nested JSON document, overflows a fixed-size stack buffer and aborts the process, leading to DoS. In a two-step chain in ext/oj/fast.c, doc_each_child increments doc->where past the where_path[MAX_STACK = 100] array with no bounds check and never restores it (the doc->where-- is missing), so calling each_child recursively from inside the yield block drives doc->where beyond the array. On the next entry the function copies the path into the 800-byte stack-local buffer save_path[MAX_STACK] using wlen = doc->where - doc->where_path, so when the previous recursive call left doc->where past where_path[100] the wlen exceeds MAX_STACK and the memcpy overflows save_path on the C stack; because the Oj::Doc parser imposes no JSON nesting-depth limit (relying on a C-stack pressure check), deeply nested attacker input reaches this path. This issue has been fixed in version 3.17.3.
- Record
CVE-2026-54592- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-125, CWE-787
- Updated
- 2026-11-02
EUVDEUVD-2026-40851
7.5
Oj (Optimized JSON) is a JSON parser and Object marshaller packaged as a Ruby gem. In versions prior to 3.17.3, Oj::Doc#each_child, when invoked recursively over a deeply nested JSON document, overflows a fixed-size stack buffer and aborts the process, leading to DoS. In a two-step chain in ext/oj/fast.c, doc_each_child increments doc->where past the where_path[MAX_STACK = 100] array with no bounds check and never restores it (the doc->where-- is missing), so calling each_child recursively from inside the yield block drives doc->where beyond the array. On the next entry the function copies the path into the 800-byte stack-local buffer save_path[MAX_STACK] using wlen = doc->where - doc->where_path, so when the previous recursive call left doc->where past where_path[100] the wlen exceeds MAX_STACK and the memcpy overflows save_path on the C stack; because the Oj::Doc parser imposes no JSON nesting-depth limit (relying on a C-stack pressure check), deeply nested attacker input reaches this path. This issue has been fixed in version 3.17.3.
- Record
EUVD-2026-40851- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-3m6q-jj5j-38c9
7.5
Oj: Stack Buffer Overflow in Oj::Doc#each_child via Deeply Nested Input
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:
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->wherepastwhere_path[100],wlenexceedsMAX_STACKand thememcpyoverflowssave_pathon 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).
- Record
GHSA-3m6q-jj5j-38c9- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-125, CWE-787
- Updated
- 2026-11-02
NVD
7.5
Oj (Optimized JSON) is a JSON parser and Object marshaller packaged as a Ruby gem. In versions prior to 3.17.3, Oj::Doc#each_child, when invoked recursively over a deeply nested JSON document, overflows a fixed-size stack buffer and aborts the process, leading to DoS. In a two-step chain in ext/oj/fast.c, doc_each_child increments doc->where past the where_path[MAX_STACK = 100] array with no bounds check and never restores it (the doc->where-- is missing), so calling each_child recursively from inside the yield block drives doc->where beyond the array. On the next entry the function copies the path into the 800-byte stack-local buffer save_path[MAX_STACK] using wlen = doc->where - doc->where_path, so when the previous recursive call left doc->where past where_path[100] the wlen exceeds MAX_STACK and the memcpy overflows save_path on the C stack; because the Oj::Doc parser imposes no JSON nesting-depth limit (relying on a C-stack pressure check), deeply nested attacker input reaches this path. This issue has been fixed in version 3.17.3.
- Record
CVE-2026-54592- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-125, CWE-787
- Updated
- 2026-11-02
OSV
7.5
Oj: Stack Buffer Overflow in Oj::Doc#each_child via Deeply Nested Input
Oj (Optimized JSON) is a JSON parser and Object marshaller packaged as a Ruby gem. In versions prior to 3.17.3, Oj::Doc#each_child, when invoked recursively over a deeply nested JSON document, overflows a fixed-size stack buffer and aborts the process, leading to DoS. In a two-step chain in ext/oj/fast.c, doc_each_child increments doc->where past the where_path[MAX_STACK = 100] array with no bounds check and never restores it (the doc->where-- is missing), so calling each_child recursively from inside the yield block drives doc->where beyond the array. On the next entry the function copies the path into the 800-byte stack-local buffer save_path[MAX_STACK] using wlen = doc->where - doc->where_path, so when the previous recursive call left doc->where past where_path[100] the wlen exceeds MAX_STACK and the memcpy overflows save_path on the C stack; because the Oj::Doc parser imposes no JSON nesting-depth limit (relying on a C-stack pressure check), deeply nested attacker input reaches this path. This issue has been fixed in version 3.17.3.
- Record
CVE-2026-54592- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-125, CWE-787
- Updated
- 2026-11-02
OSVDEBIAN-CVE-2026-54592
7.5
Oj (Optimized JSON) is a JSON parser and Object marshaller packaged as a Ruby gem. In versions prior to 3.17.3, Oj::Doc#each_child, when invoked recursively over a deeply nested JSON document, overflows a fixed-size stack buffer and aborts the process, leading to DoS. In a two-step chain in ext/oj/fast.c, doc_each_child increments doc->where past the where_path[MAX_STACK = 100] array with no bounds check and never restores it (the doc->where-- is missing), so calling each_child recursively from inside the yield block drives doc->where beyond the array. On the next entry the function copies the path into the 800-byte stack-local buffer save_path[MAX_STACK] using wlen = doc->where - doc->where_path, so when the previous recursive call left doc->where past where_path[100] the wlen exceeds MAX_STACK and the memcpy overflows save_path on the C stack; because the Oj::Doc parser imposes no JSON nesting-depth limit (relying on a C-stack pressure check), deeply nested attacker input reaches this path. This issue has been fixed in version 3.17.3.
- Record
DEBIAN-CVE-2026-54592- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Updated
- 2026-11-02
OSVGHSA-3m6q-jj5j-38c9
7.5
Oj: Stack Buffer Overflow in Oj::Doc#each_child via Deeply Nested Input
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:
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->wherepastwhere_path[100],wlenexceedsMAX_STACKand thememcpyoverflowssave_pathon 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).
- Record
GHSA-3m6q-jj5j-38c9- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-125, CWE-787
- Updated
- 2026-11-02
OSVUBUNTU-CVE-2026-54592
7.5
Oj (Optimized JSON) is a JSON parser and Object marshaller packaged as a Ruby gem. In versions prior to 3.17.3, Oj::Doc#each_child, when invoked recursively over a deeply nested JSON document, overflows a fixed-size stack buffer and aborts the process, leading to DoS. In a two-step chain in ext/oj/fast.c, doc_each_child increments doc->where past the where_path[MAX_STACK = 100] array with no bounds check and never restores it (the doc->where-- is missing), so calling each_child recursively from inside the yield block drives doc->where beyond the array. On the next entry the function copies the path into the 800-byte stack-local buffer save_path[MAX_STACK] using wlen = doc->where - doc->where_path, so when the previous recursive call left doc->where past where_path[100] the wlen exceeds MAX_STACK and the memcpy overflows save_path on the C stack; because the Oj::Doc parser imposes no JSON nesting-depth limit (relying on a C-stack pressure check), deeply nested attacker input reaches this path. This issue has been fixed in version 3.17.3.
- Record
UBUNTU-CVE-2026-54592- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
ojRubyGems |
before 3.17.3 | 3.17.3 |
ruby-ojDebian:12 |
before 3.14.2-1+deb12u1, plus listed versions 3.14.2-1 |
3.14.2-1+deb12u1 |
ruby-ojDebian:13 |
before 3.16.3-1+deb13u1, plus listed versions 3.16.3-1 |
3.16.3-1+deb13u1 |
ruby-ojDebian:14 |
before 3.17.3-1, plus listed versions 3.16.12-13.16.15-13.16.16-13.16.3-13.17.0-13.17.1-1 |
3.17.3-1 |
ojRubyGems |
before 3.17.3, plus listed versions 0.50.5.10.5.20.6.00.7.0 Show 45 moreShow less0.8.00.9.01.0.01.0.11.0.21.0.31.0.41.0.51.0.61.1.01.1.11.2.01.2.11.2.101.2.111.2.121.2.131.2.21.2.31.2.41.2.51.2.61.2.71.2.81.2.91.3.01.3.21.3.41.3.51.3.61.3.71.4.01.4.11.4.21.4.31.4.41.4.51.4.61.4.6a21.4.72.0.02.0.12.0.102.0.112.0.12 |
3.17.3 |
ruby-ojUbuntu:14.04:LTS |
All versions, plus listed versions 2.0.10-12.1.4-12.4.0-12.5.3-1 |
None known |
ruby-ojUbuntu:16.04:LTS |
All versions, plus listed versions 2.12.10-32.12.10-3build12.12.10-42.12.10-4build1 |
None known |
ruby-ojUbuntu:18.04:LTS |
All versions, plus listed versions 2.17.4-13.4.0-13.4.0-1build1 |
None known |
ruby-ojUbuntu:20.04:LTS |
All versions, plus listed versions 3.8.1-13.9.1-13.9.2-13.10.1-13.10.1-1build1 |
None known |
ruby-ojUbuntu:22.04:LTS |
All versions, plus listed versions 3.11.0-13.13.7-13.13.7-1build13.13.9-13.13.9-1build1 |
None known |
Show 4 moreShow less
| Package | Affected | Fixed |
|---|---|---|
ruby-ojUbuntu:24.04:LTS |
All versions, plus listed versions 3.15.0-13.16.1-13.16.3-13.16.3-1build13.16.3-1build2 |
None known |
ruby-ojUbuntu:25.10 |
All versions, plus listed versions 3.16.3-1build3 |
None known |
ruby-ojUbuntu:26.10 |
All versions, plus listed versions 3.16.15-13.17.0-13.17.1-13.17.3-13.17.4-1 |
None known |
ruby-ojUbuntu:26.04:LTS |
All versions, plus listed versions 3.16.3-1build33.16.12-13.16.15-1 |
None known |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-54592 is a high vulnerability (CVSS 7.5) in Ohler55 Oj, published June 19, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.33%, the 26th percentile. Fixed in oj 3.17.3.
References
github.com/ohler55/oj/security/advisories/GHSA-3m6q-jj5j-38c9
github.com/advisories/GHSA-3m6q-jj5j-38c9
github.com/CVEProject/cvelistV5/tree/main/cves/2026/54xxx/CVE-2026-54592.json
nvd.nist.gov/vuln/detail/CVE-2026-54592
security-tracker.debian.org/tracker/CVE-2026-54592
github.com/ohler55/oj
ubuntu.com/security/CVE-2026-54592
cve.org/CVERecord?id=CVE-2026-54592
Left out here: 244 versions. The JSON record has them.