Aug 8, 2026·6 min read·42 visits
A Use-After-Free (UAF) bug in the native C extension of the Ruby json gem allows remote attackers to trigger a process crash and denial of service via malformed streaming JSON data with duplicate keys.
A technical analysis of the use-after-free (UAF) vulnerability in the Ruby JSON gem (CVE-2026-71847) that impacts versions 2.20.0 through 2.21.1. This vulnerability occurs when parsing incomplete stream data containing duplicate keys.
CVE-2026-71847 is a high-severity use-after-free vulnerability identified within the native C extension of the Ruby json gem. The vulnerability resides specifically within the JSON::ResumableParser class, which handles chunk-by-chunk stream parsing. Under conditions involving incomplete stream inputs containing duplicate keys, the parser invokes memory-unsafe operations on deallocated storage.
The vulnerability is categorized under CWE-416 (Use After Free). In multi-threaded or memory-sensitive Ruby environments, dereferencing these stale pointers can lead to memory leakage, immediate process crash via segmentation fault, or potential memory corruption. This compromise directly affects system availability and potentially system integrity.
The issue impacts versions of the json gem starting from 2.20.0 up to, but excluding, 2.21.2. Applications that rely on streaming parsers to handle large, unauthenticated user payloads are particularly exposed. Remediation requires an immediate update to version 2.21.2.
The root cause of this vulnerability lies in the memory management of the native C extension inside ext/json/ext/parser/parser.c. During the streaming parse cycle, when a chunk of data is completed, the function cResumableParser_parse clears the associated input buffer by calling json_str_clear(parser->buffer). While the buffer's Ruby object reference is updated, the internal parsing state (parser->state) retains dangling pointers.
Specifically, the structure fields state.start, state.cursor, and state.end are not re-initialized to null. These fields continue pointing to the memory address of the newly freed heap allocation. This creates a classic dangling pointer condition where subsequent execution flows can access invalid locations.
The execution path triggers when a consumer calls JSON::ResumableParser#partial_value to extract the current parsing state of an incomplete stream. If the parsed stream contains duplicate object keys, the native parser triggers a warning path via emit_parse_warning to issue a deprecation alert. This warning path invokes cursor_position which reads and dereferences the stale pointer values to calculate the line and column numbers of the duplicate key.
Here is a sequence diagram illustrating the lifecycle of the dangling pointers during resumable parsing:
To understand the mechanism of the vulnerability, we inspect the vulnerable version of the source code in ext/json/ext/parser/parser.c. In the vulnerable implementation, the clearing of the buffer is done without safety checks or pointer nullification:
// Vulnerable implementation in cResumableParser_parse
if (eos(&parser->state)) {
json_str_clear(parser->buffer);
parser->buffer = Qfalse;
// state.start, state.cursor, and state.end are left pointing to the freed buffer
}This vulnerability is resolved in commit 2c332bfe2bfb0e754da07e2a0310ef106bf46482 by explicitly nullifying these pointers upon freeing the buffer. The patch also prevents coordinate calculations for warnings when a resumable parser context is active:
// Patched implementation in cResumableParser_parse
if (eos(&parser->state)) {
json_str_clear(parser->buffer);
parser->buffer = Qfalse;
// Nullify dangling pointers to prevent Use-After-Free
parser->state.start = parser->state.cursor = parser->state.end = 0;
}Additionally, the patch alters emit_parse_warning to avoid calling cursor_position when the parsing context runs in resumable mode, since tracking absolute positions on incomplete streams is unreliable:
// Patched warning emission path
static void emit_parse_warning(const char *message, JSON_ParserState *state)
{
VALUE warning;
if (state->parser) {
// Avoid calculation entirely; use plain message to bypass cursor_position
warning = rb_utf8_str_new_cstr(message);
} else {
long line, column;
cursor_position(state, &line, &column);
warning = rb_sprintf("%s at line %ld column %ld", message, line, column);
}
rb_funcall(mJSON, rb_intern("deprecation_warning"), 1, warning);
}An attack targeting this vulnerability relies on sending malformed, streaming JSON inputs to an application that processes data using the JSON::ResumableParser class. The payload must satisfy two specific structural properties to trigger the vulnerable path: duplicate object keys and a truncated or incomplete structure.
First, the attacker must initiate a stream with duplicate keys, such as {"key": 1, "key": 2, ...}. The presence of duplicate keys guarantees that the native extension executes the warning generation path. Second, the JSON payload must be cut off prematurely, ensuring that the stream remains incomplete and the application calls #partial_value to recover the partial state.
When the application reads the incomplete stream, it processes the initial chunk, reaches the end of the input segment, and frees the buffer while retaining pointers. The subsequent invocation of #partial_value forces the C extension to attempt pointer arithmetic on these freed segments to calculate line offsets. This attempt results in a read access violation and terminates the executing Ruby worker process.
The following script structure illustrates how a regression payload can be constructed to trigger the vulnerability when sent over a stream:
# Trigger sequence in Ruby test suite
parser = JSON::ResumableParser.new
parser << '{"a":1,"a":2,"pad":"' + ('x' * 4194304)
parser.parse
parser << '",'
parser.parse
parser.partial_value # Dereferences freed pointer in cursor_positionThe main outcome of successful exploitation is a persistent denial-of-service (DoS) condition. Because the memory dereference happens within a native C library, the Ruby VM cannot catch the resulting segmentation fault through standard exception handlers like rescue. The entire parent process or worker thread terminates immediately.
In multi-threaded web application servers (such as Puma or Passenger), a repeated execution of this payload against the server endpoints will deplete available workers. This leads to a denial of service for legitimate users. If the application environment lacks automatic worker recovery, manual intervention is required to restore the service.
The primary CVSS score for this vulnerability is assessed at 8.7 under the CVSS v4.0 standard. While the impact is primarily centered on system availability (VA:H), vulnerability researchers must recognize that heap-use-after-free vulnerabilities can occasionally be combined with other heap-grooming techniques to achieve remote code execution (RCE) in scenarios where memory allocators do not randomize allocations.
Remediation requires upgrading the json gem to version 2.21.2 or higher. The patch fully mitigates the vulnerability by clearing the dangling pointers inside cResumableParser_parse and short-circuiting the warning generation path to bypass the unsafe memory scanner when resumable parsing is active.
For legacy systems that cannot immediately update the gem, a structural workaround involves implementing input validation filters prior to passing stream data to the resumable parser. Specifically, a lightweight Ruby-based filter can check incoming payloads for duplicate key signatures or verify formatting before deep parsing.
Additionally, system administrators should configure containerized runtimes to automatically restart application processes that exit abnormally. Enforcing maximum memory limits per worker process can also prevent heap-grooming activities that facilitate memory exploitation.
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
json (gem) Ruby | >= 2.20.0, < 2.21.2 | 2.21.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-416 (Use After Free) |
| Attack Vector | Network |
| CVSS v4.0 | 8.7 (High) |
| Exploit Status | PoC (Proof of Concept) |
| Impact | Denial of Service (Process Crash) |
| Affected Gem Versions | >= 2.20.0, < 2.21.2 |
The product uses a pointer after it has been freed, which can lead to a crash, unexpected behavior, or execution of arbitrary code.
CVE-2026-128087 (GHSA-9264-jvgf-6w2f) is an incorrect behavior order (CWE-696) and improper authorization (CWE-863) vulnerability in the russh Rust SSH library, affecting versions 0.55.0 through 0.65.0. During the initial SSH key exchange (KEX) handshake, the client state machine invokes the client::Handler::kex_done() callback—providing the negotiated raw shared secret bytes—prior to validating the remote server's host key or certificate via check_server_key(). Consequently, an untrusted or rogue SSH server can complete cryptographic key agreement and force a connecting client to execute side-effecting application logic inside kex_done() before the server's identity is checked and rejected.
CVE-2026-128082 (GHSA-6vhm-j9h7-f9gh) is an uncontrolled recursion vulnerability (CWE-674) in AutoMapper, a convention-based object-object mapper for .NET. When mapping nested or self-referential types into destination models that rely on parameterized constructors, C# positional records, or recursive ConstructUsing expressions, AutoMapper evaluates child constructor arguments prior to incrementing the parent's recursion depth counter. This ordering flaw bypasses both explicit MaxDepth limits and the default depth limit of 64. An unauthenticated remote attacker can supply deeply nested input payloads to affected endpoints, exhausting the thread call stack and triggering an uncatchable System.StackOverflowException that terminates the hosting .NET process.
An information exposure vulnerability in the @opentelemetry/instrumentation-http package for OpenTelemetry JavaScript versions 0.204.0 prior to 0.222.0 causes incoming HTTP server spans to record raw, unredacted query string parameters under the url.query attribute. While outgoing client spans applied built-in and user-configured query parameter redaction, incoming server spans bypassed this sanitization completely, resulting in the transmission of sensitive credentials such as pre-signed cloud URLs and API tokens to external observability backends.
CVE-2026-128089 is an uncontrolled resource consumption vulnerability in the dnspython toolkit for Python, affecting all versions prior to 2.9.2. When parsing Well-Known Service (WKS) records from text via dns.rdtypes.IN.WKS.WKS.from_text(), the parser fails to validate that numeric service port identifiers fall within the RFC 1035 valid range of 0 through 65535. An attacker capable of supplying untrusted zone files or RDATA text strings containing an arbitrarily large service port integer can force the parser to allocate an unbounded bitmap array using an inefficient linear append loop, resulting in severe CPU exhaustion, application hangs, or Out-Of-Memory (OOM) process termination.
CVE-2026-126347 (GHSA-5cjw-f85j-cv6r) is an open redirect vulnerability (CWE-601) in Tornado, an asynchronous Python networking library and web framework. The flaw resides in tornado.web.RedirectHandler when dynamic URL redirection templates incorporate unvalidated user input. Due to a parser differential between RFC 3986 (used by Python's urllib.parse.urlsplit) and the WHATWG URL Standard (used by modern web browsers), inputs containing three or more leading slashes bypass Tornado 6.6.0's validation logic, allowing unauthenticated attackers to redirect users to arbitrary external domains.
A Denial of Service (DoS) vulnerability exists in @sveltejs/adapter-node versions 5.5.7 and 6.0.0-next.0 through 6.0.0. When a SvelteKit server route returns a direct response from a fetch() call with a Content-Type of text/event-stream, the Node.js adapter attempts to mutate the response headers to disable proxy buffering. Because headers returned directly by the WHATWG Fetch API carry an immutable guard, this mutation throws an unhandled TypeError, terminating the underlying Node.js process and denying service to all active and subsequent clients.