CVE-2026-54592: Oj Stack Buffer Overflow Vulnerability

HIGH
Advisory Identifier: GHSA-3m6q-jj5j-38c9
CVSS 7.5
CWE-125 CWE-787
Published
Advisory Summary

CVE-2026-54592 is a HIGH (CVSS 7.5) stack buffer overflow vulnerability (CWE-125: Out-of-bounds Read) affecting Oj libraries in the RubyGems ecosystem. The issue involves stack buffer overflow in oj::doc#each_child via deeply nested input. Remediation is available by upgrading affected packages to version 3.17.3.

CVE-2026-54592 at a Glance

CVE Identifier CVE-2026-54592
GHSA Identifier GHSA-3m6q-jj5j-38c9
Severity HIGH
CVSS v3.1 Base Score 7.5
CWE Classification CWE-125: Out-of-bounds Read
Affected Product Oj
Package Ecosystem RubyGems
Published Date
Last Updated

Security Impact

CVSS: 7.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity None (I:N)
Availability High (A:H)

How to Fix CVE-2026-54592

To remediate this vulnerability, upgrade all affected Oj dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

oj fixed: 3.17.3
Upgrade oj to 3.17.3
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for oj

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-54592, inspect your project dependencies for Oj:

inspect dependencies for oj

Analyze the terminal output to locate any version of Oj in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 3.17.3), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-125: Out-of-bounds Read, where the product reads data past the end, or before the beginning, of the intended buffer. In Oj, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

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

CVE-2026-54592 Affected Oj Versions

1 component(s)
Fixed Versions:
3.17.3
Product / Package Ecosystem Affected Versions Fixed Version
oj
RubyGems
< 3.17.3
3.17.3

References

Frequently Asked Questions

What is CVE-2026-54592?
CVE-2026-54592 is a security vulnerability in Oj involving Stack Buffer Overflow in Oj::Doc#each_child via Deeply Nested Input under CWE-125: Out-of-bounds Read.
Which Oj versions are affected?
Affected versions include releases prior to 3.17.3 across affected packages: oj.
What version fixes CVE-2026-54592?
Upgrading to 3.17.3 (or the latest patched release for your respective branch) resolves the vulnerability.
Is oj affected?
Yes, oj is affected in releases prior to 3.17.3. Upgrade your dependency to 3.17.3 or later.
What is the CVSS score of CVE-2026-54592?
CVE-2026-54592 has a CVSS v3.1 base score of 7.5 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H.
What CWE is associated with CVE-2026-54592?
CVE-2026-54592 is classified under CWE-125: Out-of-bounds Read (The product reads data past the end, or before the beginning, of the intended buffer.).
How can I check RubyGems dependencies for CVE-2026-54592?
Run the dependency tree command: 'inspect dependencies for oj'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-3m6q-jj5j-38c9",
  "modified" : "2026-06-19T19:36:28Z",
  "published" : "2026-06-19T19:36:28Z",
  "aliases" : [ "CVE-2026-54592" ],
  "summary" : "Oj: Stack Buffer Overflow in Oj::Doc#each_child via Deeply Nested Input",
  "details" : "### Summary\n\n`Oj::Doc#each_child`, when invoked recursively over a deeply nested JSON\ndocument, overflows a fixed-size stack buffer and aborts the process. This is a\ndenial of service reachable from untrusted JSON.\n\n### Details\n\nTwo-step chain in `ext/oj/fast.c`:\n\n1. **`doc_each_child` (~line 1501)** increments `doc->where` past the\n   `where_path[MAX_STACK = 100]` array with no bounds check, and never restores\n   it (`doc->where--` is missing). Calling `each_child` recursively from inside\n   the yield block therefore drives `doc->where` beyond the array.\n\n2. **On the next entry (~line 1478)** the function copies the path into a\n   stack-local buffer:\n\n   ```c\n   Leaf  save_path[MAX_STACK];           // 800-byte stack buffer\n   size_t wlen = doc->where - doc->where_path;\n   if (0 < wlen) {\n       memcpy(save_path, doc->where_path, sizeof(Leaf) * (wlen + 1));\n   }\n   ```\n\n   When the previous recursive call left `doc->where` past `where_path[100]`,\n   `wlen` exceeds `MAX_STACK` and the `memcpy` overflows `save_path` on the C\n   stack.\n\nThe `Oj::Doc` parser imposes no JSON nesting-depth limit (it relies on a\nC-stack pressure check), so deeply nested attacker input reaches this path.\n\n### Proof of Concept\n\n```ruby\nrequire 'oj'\ndepth = 200\npayload = '[' * depth + '1' + ']' * depth\nOj::Doc.open(payload) do |doc|\n  r = lambda { doc.each_child { |_| r.call } }\n  r.call\nend\n```\n\nRecursion depth <= 99 iterates normally; depth >= 101 aborts. lldb backtrace\non the affected build (`ruby 3.3.8 / arm64-darwin24`):\n\n```\nSIGABRT\n#2 __abort\n#3 __stack_chk_fail\n#4 doc_each_child   (oj.bundle, fast.c)\n```\n\n### Impact\n\nReliable denial of service: any endpoint that calls\n`Oj::Doc.open(untrusted) { |d| d.each_child ... }` recursively can be crashed\nwith a small deeply-nested payload. On builds with a stack protector (the\ndefault, `-fstack-protector-strong`) the canary aborts the process before the\nsaved return address is used. The Step-1 heap OOB writes into `struct _doc`\nfields do occur, but are masked in practice because the Step-2 stack overflow\ncrashes first; turning them into anything beyond a crash has not been\ndemonstrated.\n\n### Patches\n\nFixed in **3.17.3**: `doc_each_child` now bounds-checks before incrementing\n`doc->where` (raising `Oj::DepthError`) and restores `doc->where` after the\nloop, matching the existing `each_leaf` pattern. Verified on the fixed build:\ndepth >= 101 raises a clean `Oj::DepthError` instead of aborting.\n\n### Credit\n\nReported by Zac Wang (@7a6163).",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "RubyGems",
      "name" : "oj"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "3.17.3"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/ohler55/oj/security/advisories/GHSA-3m6q-jj5j-38c9"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/ohler55/oj"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-125", "CWE-787" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-06-19T19:36:28Z",
    "nvd_published_at" : null
  }
}