Jul 23, 2026·6 min read·29 visits
A dynamic buffer capacity calculation error in the Ruby json gem's C extension allows attackers to trigger a heap-based buffer overflow and crash the application by sending strings close to 16 KB during IO-based streaming serialization.
A heap-based buffer overflow vulnerability exists in the native C extension of the Ruby json gem (versions 2.9.0 through 2.19.8) during IO-based streaming serialization. An incorrect buffer size calculation can lead to memory corruption and process termination when processing large strings.
The Ruby json gem relies on a native C extension for performance-critical serialization operations. Within this extension, the library manages memory through a custom dynamic buffer structure named FBuffer. This structure is designed to aggregate small chunks of serialized JSON output before writing them to an IO stream, minimizing system call overhead during serialization tasks.
This dynamic buffering process exposes an attack surface when applications stream untrusted input or serialize database fields containing user-provided strings. The vulnerability manifests when the serialized payload contains a string element close to the internal buffer's threshold, specifically near 16 KB.
The underlying flaw is a heap-based buffer overflow (CWE-122) arising from a size calculation failure (CWE-131). When triggered, the vulnerability results in memory corruption within the application heap. Under standard runtime conditions, this corruption causes immediate process termination, leading to a Denial of Service.
The technical root cause of CVE-2026-54696 lies within the capacity management logic defined in ext/json/ext/fbuffer/fbuffer.h. Specifically, the function fbuffer_do_inc_capa(FBuffer *fb, size_t requested) ensures that the buffer has sufficient space to hold an upcoming write of size requested. When writing to an IO destination, the generator flushes the existing buffer when its capacity exceeds FBUFFER_IO_BUFFER_SIZE (16 KB) and resets the length.
The logical error occurs during the validation check executed immediately after checking the IO stream status. The vulnerable code executes a conditional comparison: if (RB_LIKELY(requested < fb->capa)) { return; }. This statement assumes that if the incoming request size is smaller than the total capacity of the buffer, the data will fit safely within the remaining buffer limits.
This assumption is flawed because it ignores the current utilization of the buffer, represented by fb->len. If the buffer already contains serialized data (fb->len > 0), the actual available capacity is the total capacity minus the written length (fb->capa - fb->len). If the requested write size is smaller than fb->capa but larger than the remaining free space, the function returns prematurely without expanding the buffer or flushing the existing data.
The vulnerability can be traced by analyzing the vulnerable implementation of the dynamic capacity adjustment function. Below is the vulnerable code segment:
static void fbuffer_do_inc_capa(FBuffer *fb, size_t requested)
{
if (RB_UNLIKELY(fb->io)) {
if (fb->capa < FBUFFER_IO_BUFFER_SIZE) {
fbuffer_realloc(fb, FBUFFER_IO_BUFFER_SIZE);
} else {
fbuffer_flush(fb);
}
// VULNERABLE CHECK: Only compares requested size against total capacity
// Ignores the remaining space available (fb->capa - fb->len)
if (RB_LIKELY(requested < fb->capa)) {
return;
}
}
// ... standard resizing code ...
}The remediation commit (996bac686d64e4e3aaeae03b14a7f9ee9695ebdb) refactors this logic to ensure that memory is flushed cleanly and capacity calculations are robust. The patched version operates as follows:
static void fbuffer_do_inc_capa(FBuffer *fb, size_t requested)
{
if (RB_UNLIKELY(fb->io)) {
if (fb->capa != 0) {
fbuffer_flush(fb); // Unconditionally flush existing buffer contents
if (RB_LIKELY(requested < fb->capa)) {
return; // Safe because fb->len is now guaranteed to be 0
}
}
}
size_t new_capa = fb->capa ? fb->capa : fb->initial_length;
size_t needed_capa = requested + fb->len; // Correctly includes existing length
while (new_capa < needed_capa) {
new_capa *= 2;
}
fbuffer_realloc(fb, new_capa);
}By ensuring that fbuffer_flush is executed and sets fb->len to 0, the check requested < fb->capa becomes safe. Additionally, the subsequent logic calculates needed_capa using the sum of the requested size and the current length, preventing sizing omissions during non-IO operations.
Exploitation of CVE-2026-54696 requires specific operational prerequisites. The target application must serialize data directly to an IO object, such as a file descriptor, network socket, or StringIO wrapper, using the native C extension engine. This pattern is commonly implemented via JSON.dump(obj, io) or when setting up custom generator states with stream destinations.
The attacker must supply a payload string that is processed and serialized by the application. To trigger the vulnerability, the payload must contain a string element designed to match the boundary state of the dynamic buffer. Specifically, the string must be large enough to trigger the capacity calculation mismatch (near 16 KB) without triggering an automatic flush via alternative logic blocks.
When the serialization engine invokes the flawed check, the capacity validation logic returns without resizing the heap allocation. The subsequent copy instruction executes a MEMCPY operation that writes beyond the bounds of the allocated buffer. This action corrupts adjacent heap metadata, typically causing the interpreter process to abort immediately.
The primary security impact of this vulnerability is a Denial of Service (DoS) condition. Because the memory corruption occurs on the heap, the Ruby interpreter's internal state becomes unstable, leading to an immediate crash. In web applications or background workers processing queue messages, repeated exploit attempts can result in a complete disruption of service availability.
The CVSS v3.1 base score is 3.7 (Low Severity), reflecting the high complexity of the attack vector and the restricted impact. The vector string is CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L. This indicates that while the vulnerability can be reached remotely without authentication, triggering the precise memory layout conditions requires specific configurations and execution paths.
At present, there are no weaponized public exploits or documented instances of active exploitation in the wild. The vulnerability does not appear in the CISA Known Exploited Vulnerabilities (KEV) catalog, and its Exploit Prediction Scoring System (EPSS) score remains low at 0.00301 (22.29th percentile), indicating a low probability of imminent widespread exploitation.
The definitive mitigation is upgrading the json gem to version 2.19.9 or higher. This release integrates the corrected capacity check implementation and enforces clean buffer flushes. Systems relying on vendored Ruby gems or pre-packaged distribution libraries should verify their package configurations to ensure the patched versions are utilized.
When immediate patching of the native extension is not feasible due to environment constraints, applications can apply a temporary code-level workaround. By serializing the target object entirely in memory before writing it to the IO destination, developers can bypass the vulnerable streaming code path.
# Vulnerable dynamic streaming pattern
JSON.dump(user_data, io_stream)
# Temporary mitigated pattern (bypasses dynamic streaming buffer)
serialized_json = JSON.fast_generate(user_data)
io_stream.write(serialized_json)This workaround shifts the serialization burden to the standard Ruby virtual machine heap allocation, which does not utilize the vulnerable FBuffer IO-stream capacity management code. However, this may result in elevated transient memory utilization for very large objects.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L| Product | Affected Versions | Fixed Version |
|---|---|---|
json gem Ruby | >= 2.9.0, <= 2.19.8 | 2.19.9 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-122 |
| Attack Vector | Network (AV:N) |
| CVSS v3.1 Score | 3.7 |
| EPSS Score | 0.00301 (22.29th percentile) |
| Impact | Low (Denial of Service via process crash) |
| Exploit Status | None |
| KEV Status | Not Listed |
A heap-based buffer overflow condition is a buffer overflow where the buffer that can be overwritten is allocated in the heap portion of memory, which generally means that the buffer was allocated using a function such as malloc().
A path traversal vulnerability (CWE-22) in Contao CMS allows unauthenticated remote attackers to bypass directory boundary restrictions in ImagesController and access files within the project directory.
Contao Open Source CMS versions 4.0.0 through 5.3.49 and 5.4.0-RC1 through 5.7.11 contain a Cross-Site Request Forgery (CSRF) vulnerability in backend parameter handling. The `RequestTokenListener` component validates anti-CSRF tokens solely for HTTP POST requests, while GET-based declarative guards run only when an `act` parameter is present in the query string. Consequently, custom backend actions dispatched via alternative parameters such as `key=` can execute without CSRF token verification when triggered by an authenticated user.
Contao CMS versions 4.1.0 through 5.3.49 and 5.4.0-RC1 through 5.7.11 fail to validate form submission tokens and enforce rate limiting when processing activation email resend requests via HTTP POST, enabling resource exhaustion and account state enumeration.
An information disclosure vulnerability in Contao CMS allows unauthenticated site visitors to view protected page titles, URLs, and text excerpts through search queries when protected page indexing is disabled after previously being enabled.
In Vikunja prior to version 2.6.0, relation creation via the CalDAV endpoint fails to invoke the TaskRelation.CanCreate authorization check. This missing access control allows an authenticated user to establish unauthorized relationships and perform write operations against any task, provided its unique identifier (UID) is known.
A cross-project information disclosure vulnerability in Vikunja allows authenticated users with read access to one project to view private task details from unauthorized projects via subtask expansion parameters.