Safeguard
Industry Analysis

Ruby deserialization vulnerabilities: Marshal.load, YAML....

A decade of Ruby CVEs — from CVE-2013-0156 to CVE-2022-32224 — shows how Marshal.load and YAML.load turn untrusted input into remote code execution.

Aman Khan
AppSec Engineer
8 min read

In January 2013, a single YAML parsing flaw in Ruby on Rails — CVE-2013-0156 — earned a maximum CVSS score of 10.0 and put every unpatched Rails application on the internet one crafted HTTP request away from full remote code execution. Within 48 hours of disclosure, public exploit code was circulating, and mass scanning of exposed Rails servers began almost immediately. More than a decade later, the same underlying pattern — a ruby deserialization vulnerability hiding inside Marshal.load, YAML.load, or a custom object-loading routine — still shows up in dependency audits and bug bounty reports. Ruby's object model makes deserialization uniquely dangerous: loading a serialized string doesn't just restore data, it can reconstruct arbitrary objects and trigger their methods. This post breaks down how these flaws work, the real incidents that proved the risk, and what engineering teams can do about it.

What Is a Ruby Deserialization Vulnerability?

A ruby deserialization vulnerability occurs when an application converts untrusted, attacker-controlled data back into live Ruby objects without restricting which classes or methods can be instantiated in the process. Ruby ships with several built-in mechanisms for this — Marshal, the YAML/Psych library, and to a lesser extent JSON with custom create_id handling — and all of them were, by default, designed for trusted, internal data rather than user input. The core danger is that Ruby's object system lets a crafted payload instantiate almost any class available in the process, including ones with initialize, method_missing, or finalizer methods that execute code as a side effect of simply being built. An attacker doesn't need to find a bug in your application logic; they need to find any class loaded into memory whose construction or destruction path can be chained into a shell command. Security researchers call these reusable chains of otherwise-harmless classes "gadget chains," and Ruby's standard library and popular gems (ActiveSupport, Gem::Requirement, Psych itself) have historically supplied plenty of usable gadgets.

How Does Marshal.load Lead to Remote Code Execution?

Marshal.load RCE happens because Marshal.load reconstructs objects by directly calling their internal allocation and initialization routines, with zero validation of what class is being built. Ruby's Marshal format is a binary serialization designed for speed and full fidelity — it can encode instance variables, custom classes, and even _load/marshal_load hooks that developers define to customize deserialization. That last part is the trap: if any class reachable in your application defines a _load or init_with method that performs file operations, shell calls, or dynamic method dispatch based on instance data, an attacker who controls the marshaled bytes controls the arguments to that method. In 2018, researchers at the security firm Elttam published a widely cited "universal" gadget chain for Ruby on Rails applications that combined otherwise benign classes — including ones from Gem::Requirement and Net::WriteAdapter — into a chain that achieved remote code execution purely through Marshal.load on attacker-supplied session or cookie data. Because Marshal.load documentation itself states plainly that it "should never be used to deserialize untrusted data," any code path that feeds request parameters, cookies, or cache values into it is a critical finding, not a theoretical one.

Why Is YAML.load Considered Unsafe in Ruby?

YAML.load unsafe behavior comes from the same root cause as Marshal: by default, Ruby's Psych YAML parser translated custom YAML tags (like !ruby/object:SomeClass) directly into instantiated Ruby objects. That meant a YAML document — a format most developers treat as "just structured text" for config files — could smuggle in a full object graph. CVE-2013-0156 exploited exactly this: Rails' XML parameter parser transparently converted XML request bodies into YAML internally, and YAML.load happily built out attacker-specified objects, resulting in unauthenticated RCE across Rails 2.3, 3.0, 3.1, and 3.2 before their patched versions. Nearly a decade later, CVE-2022-32224 showed the pattern hadn't disappeared: Rails' Action Dispatch could deserialize database-backed session data with YAML.load instead of a safe loader, giving anyone with database or session write access a path to RCE, and it scored 9.8 on the CVSS scale when disclosed in July 2022. The Ruby core team's real fix was structural rather than a single patch: Psych 4, bundled with Ruby 3.1 in December 2021, flipped the default behavior of YAML.load to act like the previously separate YAML.safe_load — refusing to instantiate arbitrary classes unless a caller explicitly opts in via permitted_classes. Applications still pinned to older Psych versions, or that explicitly pass aliases: true, permitted_classes: [...] broadly, remain exposed.

What Is Ruby Object Injection and How Do Attackers Exploit It?

Ruby object injection is the technique of crafting a serialized payload so that, once deserialized, it produces objects an application never intended to create — often chained together so that building or discarding one object triggers a method on the next. Attackers typically start by fingerprinting which gems and Rails version an application uses (from a Gemfile.lock leak, error page, or public repo), then select a known gadget chain compatible with that dependency set, since chains are version- and library-specific. From there, the injection point is usually one of three places: a session cookie signed but not encrypted (so tampering is detectable but the plaintext structure is still attacker-visible), a cache backend that stores serialized Ruby objects, or an API endpoint that accepts a YAML or Marshal-encoded body directly. Because the payload rides inside data the application already trusts implicitly — its own session format — object injection frequently bypasses authentication entirely rather than exploiting a logic flaw after login.

Which Real-World Incidents Show the Impact of These Flaws?

The clearest evidence is the scale and speed of exploitation after CVE-2013-0156: security researchers tracking internet-wide scanning at the time observed automated exploit attempts against Rails applications within days of disclosure, and the vulnerability was folded into commodity attack toolkits used to build botnets from compromised Rails servers — a rare case of a single language-level deserialization bug producing internet-scale, worm-like exploitation. The Ruby on Rails security team's own advisory for CVE-2013-0156 rated it a full 10.0 CVSS, the maximum possible severity, reflecting that it was unauthenticated, remote, and resulted in complete code execution. Nearly ten years later, CVE-2022-32224 demonstrated the pattern persists even in modern, actively maintained frameworks: because Action Dispatch used YAML.load for database-backed session deserialization by default, any Rails app using that session store and running an affected version carried a 9.8-severity RCE risk until upgrading. Bug bounty writeups from firms like Elttam, HackerOne researchers, and GitHub's own security lab have continued to surface fresh gadget chains through Marshal and YAML in Ruby gems well into the 2020s, confirming this isn't a solved problem — it's an ongoing category that resurfaces with every new gem added to a dependency tree.

How Can Developers Defend Against Ruby Deserialization Attacks?

The most reliable defense is avoiding untrusted deserialization entirely: use JSON.parse for data interchange wherever possible, since it produces plain hashes and arrays rather than arbitrary objects, and reserve Marshal and YAML for data your application itself generated and controls. Where YAML is unavoidable, always call YAML.safe_load (or rely on Psych 4's safe-by-default YAML.load on Ruby 3.1+) with an explicit, minimal permitted_classes allowlist rather than disabling that protection for convenience. Session and cache stores should be audited specifically for serialization format — Rails apps should confirm they're on ActiveSupport::MessageEncryptor with JSON serialization rather than legacy Marshal-based cookies, and any custom cache backend that calls Marshal.load on externally influenced keys or values needs the same scrutiny as a raw eval. Finally, dependency hygiene matters as much as code review: because gadget chains are built from classes shipped in gems you didn't write, keeping Rails, Psych, and other core libraries current — and knowing exactly which gems are loaded in production — determines whether a published gadget chain even has ingredients to work with in your environment.

How Safeguard Helps

Safeguard is built for exactly this class of risk: vulnerabilities that live not in your own logic but in how your dependency graph handles untrusted data. Our software supply chain security platform continuously maps every gem your Ruby applications pull in — direct and transitive — and flags known-vulnerable versions of Rails, Psych, and other libraries tied to deserialization CVEs like CVE-2013-0156 and CVE-2022-32224 the moment they're detected, not weeks later during a manual audit. Because gadget chains depend on the specific mix of classes present in a runtime, Safeguard's dependency inventory lets security teams answer the question that matters during an active disclosure — "do we actually have the gadgets this exploit needs?" — in minutes instead of days. Combined with policy checks that catch risky patterns like unrestricted YAML.load or Marshal.load calls on external input during code review, Safeguard helps teams close the gap between a CVE being published and an application actually being patched, before an unauthenticated request turns a parsing routine into remote code execution.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.