### 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: ```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 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).
### 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: ```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 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).
Update oj to 3.17.3 if you use the affected versions. Test the change in a non-production environment first.
Local check
hol-guard supply-chain scanOj: Stack Buffer Overflow in Oj::Doc#each_child via Deeply Nested Input affects oj (rubygems). Severity is high. ### 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: ```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 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 coding agents often install or upgrade packages automatically in rubygems. A high vulnerability in a dependency can be pulled into a project through a normal install or update without a human reviewing the change, expanding the blast radius from a single package to every agent workspace that depends on it.
| Package | Affected range | Fixed version |
|---|---|---|
| ojrubygems | <3.17.3 | 3.17.3 |
Fixed versions are reported by the source feed; confirm compatibility before updating.
Reported by GitHub Security Advisories (ghsa).
HOL Guard can help your team review package activity against supported protection paths.
Explore HOL GuardUpdate oj to 3.17.3 if you use the affected versions. Test the change in a non-production environment first.
Local check
hol-guard supply-chain scanOj: Stack Buffer Overflow in Oj::Doc#each_child via Deeply Nested Input affects oj (rubygems). Severity is high. ### 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: ```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 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 coding agents often install or upgrade packages automatically in rubygems. A high vulnerability in a dependency can be pulled into a project through a normal install or update without a human reviewing the change, expanding the blast radius from a single package to every agent workspace that depends on it.
| Package | Affected range | Fixed version |
|---|---|---|
| ojrubygems | <3.17.3 | 3.17.3 |
Fixed versions are reported by the source feed; confirm compatibility before updating.
Reported by GitHub Security Advisories (ghsa).
HOL Guard can help your team review package activity against supported protection paths.
Explore HOL Guard