May 18, 2020·4 min read·engineeringrubyrailssecurityserialization

Rails repairs trust boundaries in parameters and cached values

Rails 5.2.4.3 and 6.0.3.1 correct parameter iteration, cache deserialization, upload signatures and CSRF token handling.

A wrapper around untrusted input can enforce policy only while operations preserve that wrapper's contract. Returning its internal hash or guessing that raw bytes describe an object can cross the boundary accidentally. Rails 5.2.4.3 and 6.0.3.1, released on May 18, include fixes illustrating both problems. The release also corrects direct-upload signatures and CSRF handling in Ruby and JavaScript code.

The release announcement lists CVE-2020-8162 and CVE-2020-8164 through CVE-2020-8167. These are separate defects in different components. Understanding the implementation changes is more useful than treating the collection as one generic framework vulnerability, because the triggering application behavior differs between them.

I find Ruby's collection methods easy to read, particularly when a block expresses a small operation. Return values still deserve attention. A method can yield the intended values to a block and then return an object with a different security meaning from the receiver.

Iteration returned the underlying parameters hash

The strong-parameters advisory identifies the return values of each, each_pair and each_value. Those methods could return the internal untrusted hash. An application using the return value could then pass user input onward without retaining the protections associated with ActionController::Parameters.

Ruby returns the value of the last expression in a method. A helper ending in an iteration call can therefore return that iteration's result even when its author was thinking only about work performed inside the block. The advisory's example uses that pattern in a parameter-cleanup helper called by an update action.

The 6.0.3.1 changes add an explicit self after the loops. Their regression tests assert object identity for the return value of each affected method. Returning the parameters object preserves its permission state and avoids exposing the internal hash through an incidental iteration result.

That correction does not mean iteration grants permission to update every supplied attribute. It keeps the operation inside the wrapper's existing contract. Application code still needs to select permitted input through the intended API. The fix prevents a convenient iteration method from becoming an unintended route around that API.

Raw cache data must retain its interpretation

The cache advisory concerns MemCacheStore and RedisCacheStore when untrusted strings are stored using raw: true. Re-reading could interpret the bytes as a marshalled Ruby object. The advisory describes effects up to remote code execution, depending on the input and application context.

The significant boundary is between bytes and objects. A raw cache value is useful precisely because the caller can store a representation without the framework's normal serialization. Trying to detect a marshalled object from those bytes makes the read perform more work than the writer's declared format implies.

The MemCacheStore changes remove an extra unmarshalling path. Tests that formerly expected Marshal.dump([]) to come back as an array now expect the same serialized string. That is a concrete specification of the repaired interpretation. Raw data should remain raw through that path.

RedisCacheStore requires a more qualified explanation in this release. The code passes the raw option into deserialization and warns when the read option differs from how the value was written. The diff still contains Marshal.load in that method. Describing the change as removing all Redis unmarshalling would therefore overstate what shipped.

The advisory explicitly asks developers to use consistent values of raw for reading and writing. The changed cache tests do so for incremented values and multi-key reads as well. A value's interpretation is part of the cache key's contract, not an option that can vary independently at each call site without consequences.

Upload limits need to be signed

Active Storage's S3 service includes a declared content length when creating a direct-upload URL. The patch adds content-length to the headers included in the signature. A limit supplied as metadata cannot constrain the direct request unless the storage service can enforce its relationship to that request.

The new test constructs a signed upload for a length smaller than the supplied body. It expects the upload to fail and the object not to be available afterward. That verifies enforcement at the direct-upload boundary, where the application has delegated the byte transfer to storage.

This matters because the application server may never receive the file body. Checking one declared size while handing out an authorization for a different effective request would leave the intended limit unenforced. The signature change ties that request property to the authorization sent to storage.

Global and per-form CSRF tokens need separate derivation

The CSRF-token patch changes the value masked for a global token. It derives that value through an HMAC using the underlying session token and a separate identifier. Per-form tokens already derive from a form path and method. The change prevents a disclosed global token from directly exposing the root value used for the per-form derivation.

The tests establish that unmasking a newly generated global token does not yield the original session token. They also test compatibility with an older token. The implementation can change the material it exposes while still accepting the older form during verification.

Masking and derivation have different jobs here. A representation can obscure a value in transit without making it a separate cryptographic value. The HMAC adds a distinct derivation step before masking, which is the part relevant to separating global and per-form token material.

The JavaScript change controls where tokens are sent

The rails-ujs fix places CSRF-header installation inside the same-origin branch of XHR setup. Previously, the X-Requested-With header was conditional on the cross-domain option, but the CSRF-protection call followed outside that condition. The new structure keeps both inside the appropriate branch.

I prefer this kind of source-level explanation because it identifies the actual decision being corrected. Token verification on the server and token transmission by the browser are separate behaviors. A framework update spanning both sides has to reach the JavaScript an application serves as well as the Ruby dependencies it loads. The release's Action View changes contain the client-side correction alongside its Ruby-side fixes.