September 14, 2017·4 min read·engineeringrubysecurityjson

Ruby's JSON generator can expose heap bytes through formatting state

CVE-2017-14064 concerns malicious JSON generator state, not ordinary JSON parsing. The repair makes native copies respect Ruby string lengths, including embedded zero bytes.

A serializer writes more than the values supplied in the object being serialized. Its formatting state can also contribute bytes, including indentation and separators. Ruby's JSON extension had a mismatch between Ruby string lengths and C string copying in that state. Under affected conditions, generated output can contain heap contents unrelated to the intended document.

The September 14 advisory assigns CVE-2017-14064 to this problem. It specifically describes a malicious JSON::Ext::Generator::State instance passed to JSON.generate. That is a more precise condition than saying that parsing any untrusted JSON document exposes memory. The vulnerable operation is generation using malicious state.

I find Ruby's readable APIs useful, but a small method call can cross into native code with different string conventions. This disclosure matters because the result is output that might leave the process. A caller can receive a String that appears to be the serializer's completed work while it contains bytes the application never intended to publish.

Ruby strings and C strings have different boundaries

The Ruby backport of the JSON repair changes the copying of generator-state strings. The affected fields include indentation, spacing before and after separators, and newline formatting for objects and arrays. Each field has a pointer and a recorded length in the native state.

Previously several setters copied with strdup, which interprets a zero byte as the end of a C string. The setter nevertheless recorded the Ruby string's full length. A Ruby string can contain embedded zero bytes, so the recorded length could describe more data than the null-terminated copy actually contained.

The repair uses fstrndup with an explicit length for those fields. It also changes that helper from memccpy with a zero-byte stop condition to memcpy. The new implementation allocates the requested byte count and copies exactly that count. The copy and the recorded length therefore describe the same input extent.

This combination matters. Replacing a setter's function name alone would be insufficient if the replacement helper still stopped copying at a zero byte. The helper and its callers both need to use the length-based interpretation that Ruby strings require.

The arbitrary-heap-exposure entry in the JSON project's March 23 changelog credits Ahmad Sherif. It records the repair in JSON 2.0.4. The September Ruby advisory credits the same reporter and provides guidance for the JSON copies bundled with maintained Ruby branches.

Formatting state is part of the input

Generator state can look like an internal implementation detail, but the public generation operation can accept a state object supplied by a caller. The advisory's attack condition depends on that object being malicious. An application assessing its own use needs to consider who can influence state as well as who can influence the values being serialized.

This is different from a malformed JSON input string passed to a parser. Serialization starts with Ruby values and produces text. The relevant trust boundary lies in the configuration consumed by the native generator. Treating all JSON vulnerabilities as parser problems would direct attention away from the operation identified by this notice.

The mechanism also explains why validating the resulting document's ordinary field names would not repair the native copy. Formatting strings contribute outside the model's scalar values. The pointer-length mismatch occurs before any downstream consumer decides what the output means.

The public advisory does not claim that every call to JSON.generate is remotely exploitable. It names malicious generator state and possible heap content in the result. Whether a particular service allows an attacker to reach that condition depends on the service's data and configuration paths. The fixed extension removes the underlying mismatch instead of requiring that condition to be proved separately for each caller.

The disclosure also does not identify the contents of every exposed allocation. Heap exposure means output can contain process memory, but the notice supplies no guarantee about which application value appears. That limits any claim about a particular secret while leaving the information-disclosure defect clear.

The bundled library has its own version

The advisory lists Ruby 2.2.7 and earlier in the 2.2 series, Ruby 2.3.4 and earlier in 2.3, and Ruby 2.4.1 and earlier in 2.4 as affected. It also identifies trunk revisions before 58323. These are statements about the JSON library bundled with those interpreter versions.

JSON is also distributed as a gem, so the interpreter's version is not the only relevant version in an application environment. The advisory tells users unable to upgrade Ruby itself to install a JSON gem newer than 2.0.4. That is the published workaround wording and should be retained when following this notice.

There is a visible difference between that recommendation and the upstream changelog, which records the heap-exposure fix in 2.0.4 itself. The two records can be reported without silently rewriting the advisory. The changelog establishes when the upstream repair was recorded, while the advisory provides a stricter version recommendation for affected users.

I value this distinction because bundled libraries and separately selected gems can make a runtime inventory ambiguous. A release label on the interpreter does not by itself establish which JSON extension an application's dependency environment activates. The version and source of the actual library matter to identifying the native implementation in use.

The repair preserves the length-based representation

The implementation lesson here is concrete. A Ruby string's byte count cannot be replaced by the location of its first zero byte when native state will later use the full count. Allocating, copying and consuming the same extent prevents the output operation from reading beyond the copied data.

The change applies that rule to all the formatting fields identified in the patch instead of treating indentation as a unique special case. It preserves the generator's formatting model while changing the native copying operation beneath it. The published upgrade guidance addresses the affected bundled versions and the separately distributed gem, so the application can obtain that corrected implementation without changing its intended JSON schema.