April 21, 2026·6 min read·engineeringrubysecurityserialization

ERB's deserialization guard missed its method-generation APIs

CVE-2026-41316 bypasses ERB's initialization guard through alternative template-evaluation methods. The fix closes those paths, while untrusted Marshal input remains unsafe by design.

ERB had a guard intended to stop a reconstructed template object from executing code, but several ways of evaluating its source did not check it. The April 21 advisory for CVE-2026-41316 identifies def_method, def_module and def_class as bypasses. An application that loads untrusted Marshal data with both ERB and ActiveSupport loaded can consequently execute attacker-controlled code.

The guard's existence is important context. This is a failure in a defensive check around a dangerous object reconstruction path. Updating ERB closes the disclosed bypass. It does not change Marshal into a format suitable for arbitrary data supplied by users. Ruby's own Marshal documentation already warns that its ability to reconstruct loaded classes makes untrusted input unsafe.

I value Ruby's uniform object model and the readability it offers over the Perl conventions I came from. Serialization exposes a consequence of that flexibility. Reconstructing an object can recover behavior as well as values, especially when the loaded process contains libraries designed to generate or execute code. That is a much stronger operation than reading a dictionary of strings.

ERB compiles a template into executable Ruby

The ERB 6.0.4 implementation shows initialization compiling a template into @src, alongside encoding information. The object records an initialization marker in @_init. Calling result checks that marker before evaluating the generated source in a binding. The marker links permission to evaluate with the object having passed through the expected initialization path.

That makes the problem different from escaping an interpolated HTML value. ERB's purpose includes executing Ruby embedded in a trusted template. Here the concern is an object reconstructed from untrusted serialized state reaching an execution method despite not having the expected initialized status. HTML escaping would address a different boundary and would not correct the missing guard.

A legitimately constructed template can be turned into an instance method through def_method. The implementation wraps the compiled source in a method definition and evaluates it in a module context. def_module creates a module and installs a generated method there. def_class creates a class and uses the same underlying method-generation facility. These are additional routes from template source to Ruby evaluation.

The advisory says those methods lacked the initialization check. Protecting result therefore left other behaviorally related entry points available. The method names describe different output forms, but all of them ultimately depend on the authority to evaluate the template's compiled source. The security condition has to follow that common responsibility.

Deserialization supplies objects outside ordinary construction

The Marshal documentation in Ruby 4.0 explains that Marshal.load can deserialize almost any class loaded into the process. It also documents customization hooks such as marshal_dump and marshal_load. This is an object reconstruction system, with substantially more expressive power than a simple data interchange format.

For a class using those hooks, reconstruction can allocate an object and call its loading method with stored information. The documentation explicitly describes the security consequences of loaded classes participating in deserialization. It warns that remote code execution can result when the source is untrusted. That warning applies independently of this particular ERB bypass.

The advisory's scope requires both ERB and ActiveSupport to be loaded. It identifies Rails applications importing untrusted serialized data, Ruby tools doing the same, and older Rails cookie-session arrangements using Marshal as affected contexts. These are conditional exposure descriptions. The presence of ERB in an application alone does not establish that untrusted data reaches Marshal.load.

The input's origin therefore matters more than the filename or the application's intended use for it. A cache entry, an import payload or an IPC message can carry untrusted bytes if another party controls its contents. Calling the operation internal does not prove that only trusted code can supply its input. The relevant property is who can influence the serialized data that the process reconstructs.

This is why reviewing only direct template-rendering calls would miss the disclosed path. The attack begins at deserialization, and the available classes supply behavior that can be connected during reconstruction. The advisory notes that def_module takes no arguments, which makes it straightforward to invoke within such a chain. An application need not have written a normal rendering call for an untrusted template.

Marshal also supports a second customization pair, _dump and _load, for classes that need to control allocation during restoration. Its documentation gives marshal_dump precedence when both serialization approaches are defined. These hooks illustrate how loading can involve class-specific behavior, rather than a fixed reconstruction of simple fields.

The set of classes loaded in the receiving process consequently forms part of the deserialization context. Adding a library can change the behavior available to reconstructed objects even when the loading call itself stays unchanged. For this advisory, the published scope identifies ERB and ActiveSupport together. That dependency context explains why auditing the input boundary is more durable than relying solely on one class rejecting an invalid state.

A common execution path carries the new check

In the patched implementation, def_method checks @_init before building and evaluating the method source. Its comparison uses the same expected class singleton object as the guard in result. A mismatch raises ArgumentError. The important ordering is that the check happens before source evaluation.

The higher-level class and module helpers call def_method, so the shared implementation enforces the condition for those routes as well. This is a useful structure for a security fix. It places the check where the related operations converge, while preserving the legitimate APIs for properly initialized template objects. The correction does not require removing method generation as a supported feature.

A benign use of that API can be illustrated without constructing serialized objects. The following code creates the template through the normal initializer and installs a method on a fresh module.

require "erb"

template = ERB.new("Hello <%= name %>")
renderer = Module.new
template.def_method(renderer, "render(name)")

The example's source is application-controlled. It demonstrates why the API exists and where normal initialization occurs. It is not a recommendation to evaluate templates provided by an untrusted sender, and it does not exercise a Marshal loading path. Keeping those distinctions explicit prevents a demonstration of ordinary ERB behavior from implying an unsupported security guarantee.

The patched version depends on the installed gem line

The advisory lists corrected erb versions 4.0.3.1, 4.0.4.1, 6.0.1.1 and 6.0.4 or later. These provide update options across supported package lines. The numbers matter because a fixed maintenance release on an older line can compare numerically below the newest release. Exposure decisions need the advisory's patched-version list, not a single simplistic major-version threshold.

Updating the gem that the application actually loads closes this disclosed ERB route. A system can contain several installed versions, and an application's dependency selection determines which one it uses. The interpreter's version is therefore insufficient evidence about the loaded ERB implementation. The selected erb version belongs in the assessment alongside the location of any untrusted Marshal input.

The advisory credits TristanInSec with discovering the issue. Its description is specific about the prerequisite libraries and the methods that bypassed the guard. Those details support a focused remediation. They do not support claiming that every Rails endpoint permits code execution or that patching this one library makes arbitrary Marshal payloads safe.

Object reconstruction remains a trust decision

Ruby's Marshal documentation recommends a format limited to simple values when the data is untrusted and says user input must not specify arbitrary types to reconstruct. JSON is its example of such a format. Choosing a simpler format constrains what the parser can create, although the application still needs to validate the meaning of the values it receives.

A data schema can express the fields needed for an import without granting the sender a choice among the process's loaded Ruby classes. That reduces the operation's authority. By contrast, Marshal's usefulness for restoring Ruby object graphs is precisely what makes it inappropriate as a general boundary for hostile input. The documentation's warning is part of understanding the API, not a temporary restriction introduced by this advisory.

My view of Ruby remains that its expressive object model is useful when the programmer controls how behavior enters the process. This event makes that boundary concrete. A restored ERB object reached an alternative evaluation method whose guard was incomplete. The patched releases extend the guard to method generation, and the application still has to remove untrusted object reconstruction from places that only require data interchange.