The Marshal Madness: A Decade of Futile Patches for a Ruby Vulnerability
Since the introduction of serialization through the Marshal module in the Ruby programming language, developers and security experts have been drawn into a protracted game of “bypass and patch.” The history of these vulnerabilities has become a vivid illustration of how cosmetic fixes to isolated flaws achieve little when the underlying architecture remains unchanged. The evolution of Marshal exploits is, in essence, a decade-long chronicle of the futility of temporary measures in the face of a systemic weakness.
The first documented warning about the risks of using Marshal.load
appeared in January 2013, when Charlie Somerville filed a ticket in Ruby’s bug tracker. From that point on, a steady stream of research, publications, and CVEs revealed ever new pathways to remote code execution through the deserialization of Marshal objects.
One of the most striking turning points came in December 2024. Ruby 3.4.0-rc1 still contained code untouched for sixteen years—within which a vulnerability lay hidden. At the same time, researcher Luke Janke published a novel method of exploiting Marshal. Though the bug was resolved before the final release, the incident underscored a sobering truth: even long-forgotten sections of code can serve as gateways for attack.
A classic example of vulnerable code is a Ruby on Rails controller that loads an incoming parameter using Marshal.load
. Such an approach is considered tantamount to remote code execution if the input is not rigorously sanitized. A carefully crafted binary object could trigger arbitrary commands on the server, from file reads to the execution of shell scripts.
As interest in the subject grew, researchers developed increasingly sophisticated methods of identifying and assembling “gadgets”—fragments of Ruby libraries that can be chained together within deserialized objects to achieve execution. In 2018, Luke Janke released a universal RCE gadget for Ruby 2.x. His work was soon followed by articles and reports from other researchers, including Etienne Stalmans, William Bowling, and the Zero Day Initiative.
By 2021–2022, updated exploits emerged targeting Ruby 3.x. Around the same time, a new version of the YAML library Psych made safe object loading the default behavior—an apparent step forward. Yet the true automation of exploit discovery began only later.
From 2024 onward, the “modern era” of Ruby deserialization research took shape. Experts such as Alex Leahu (Include Security) and Peter Stöckli (GitHub Security Lab) began applying program analysis and interprocedural CodeQL queries to uncover flaws. Their work expanded beyond Marshal to include XML, JSON, and YAML. Public PoCs, exploitation guides, analysis tools, and defensive recommendations proliferated. Within the community, the very use of Marshal came to be regarded as an unsafe and unacceptable practice.
At the time of writing, despite years of warnings and mitigation efforts, segments of code vulnerable to Marshal deserialization still linger in widely used infrastructure like RubyGems.org. These instances are often classified as “informational” vulnerabilities, yet history has shown that even the most inconspicuous flaw can evolve into a critical incident.
To improve the situation, both individual developers and the Ruby community at large are urged to act. In the short term: auditing code for Marshal usage, enforcing Semgrep rules, and migrating toward safer formats such as JSON (with manual object reconstruction) or MessagePack. In the long term: the gradual removal of Marshal from the language itself.
Further proposals include introducing a Marshal.safe_load
function with class restrictions, issuing warnings upon use of Marshal.load
, and eventually making safe behavior the default—before finally eliminating unsafe deserialization altogether.
The story of Ruby and Marshal demonstrates a harsh but enduring lesson: convenience is often the enemy of security. If a tool enables remote code execution with minimal effort, it will inevitably be exploited, time and again—until it is excised from the ecosystem entirely.