From Bug Report to Rails Patch

Shopify’s bug bounty program has a history of producing more than just vulnerability disclosures. During a 2021 event for the private Shopify Experiments program, participants were given temporary source access to a subset of projects. One report in particular highlighted an escalation path from a leaked development environment secret to Remote Code Execution (RCE). While the scope was limited to local development machines, the implication was clear: leaking a development secret should not, by default, result in RCE.

The root cause was a deserialization vulnerability in ActiveSupport::MessageEncryptor, a class in the Rails library. Up to version 7.0.X, this class defaults to the Marshal library for serialization, which converts Ruby objects into a byte stream. When an attacker can generate seemingly trusted data—using a leaked secret—and have it deserialized by MessageEncryptor, they can exploit that byte stream to execute arbitrary code or abuse application logic.

The fix on Shopify’s side was straightforward: configure the application to use JSON as the serializer instead of relying on the Marshal default. But the larger question was whether Rails itself should have a safer default.

Changing Rails Defaults

With several Rails contributors on staff, Shopify was well positioned to discuss the issue internally. The consensus was that there was no compelling reason to keep Marshal as the default serialization strategy for MessageEncryptor and MessageVerifier. Patches were prepared to switch both classes to JSON serialization in Rails 7.1.X, along with an upgrade path for existing applications to migrate their serialized data.

The change is significant because JSON is a text-based format with no built-in mechanism for object instantiation, whereas Marshal is designed to reconstruct Ruby object graphs from a byte stream—exactly what makes it dangerous with untrusted input. The relevant pull requests are available on the Rails GitHub repository:

Eliminating Marshal from Production

Changing the default in Rails was only the first step. Shopify also wanted to remove Marshal entirely from its own production environments, and built a patch to make that possible. The approach hooks the Marshal.load and Marshal.dump methods and verifies that calls originating from gems are expected. When a call is unexpected, the patch raises an exception and directs the developer to either use a safe alternative serializer or explicitly track the new call.

Shopify runs this patch in local development environments and in its continuous integration pipeline. The goal is to prevent new calls to Marshal from being introduced while maintaining a list of gems that still rely on it. That list serves a dual purpose: it flags libraries that could benefit from switching to a safer serializer, and it keeps pressure on the wider Ruby ecosystem to reduce reliance on Marshal.

For teams maintaining Ruby applications, adapting this patch to their own projects is a concrete way to make the same transition. It requires no changes to how gems are written—only to how calls to Marshal are vetted at runtime.

Extending Bug Bounty Impact

This incident is an example of how bug bounty programs can be leveraged for more than just fixing a single vulnerability. When a report points to a flaw in an underlying dependency, it is worth asking whether the fix belongs upstream. In this case, a report about a leaked development secret led to a change affecting the default serialization behavior of Rails, improving security for every application that adopts the new version.

For other program managers working with open source projects, the lesson is to look for these moments. A bug report often reveals a systemic issue that, when fixed at the source, multiplies the impact of the original disclosure. It is also a practical way to contribute back to the projects that form the foundation of modern software development.