Rails Active Support has a possible XSS vulnerability in SafeBuffer#%
Description
Active Support is a toolkit of support libraries and Ruby core extensions extracted from the Rails framework. Prior to versions 8.1.2.1, 8.0.4.1, and 7.2.3.1, SafeBuffer#% does not propagate the @html_unsafe flag to the newly created buffer. If a SafeBuffer is mutated in place (e.g. via gsub!) and then formatted with % using untrusted arguments, the result incorrectly reports html_safe? == true, bypassing ERB auto-escaping and possibly leading to XSS. Versions 8.1.2.1, 8.0.4.1, and 7.2.3.1 contain a patch.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Active Support's SafeBuffer#% fails to propagate the @html_unsafe flag, allowing crafted unsafe buffers to be treated as safe, potentially enabling XSS via ERB auto-escaping bypass.
Vulnerability
Active Support, a Ruby on Rails toolkit, contains a vulnerability in the SafeBuffer#% method. The method creates a new buffer from the result of super(escaped_args) but does not propagate the @html_unsafe flag from the original buffer. This means that if a SafeBuffer has been marked as unsafe (e.g., after a mutation like gsub!), the formatted result incorrectly reports html_safe? == true. [2]
Exploitation
An attacker must be able to mutate a SafeBuffer in place using methods like gsub! with crafted input, then call % with untrusted arguments. This could occur in applications that format user-controlled strings via ERB templates that use the % operator on a previously mutated buffer. No special privileges are required beyond the ability to provide input that triggers the mutation and formatting. [2]
Impact
Successful exploitation bypasses ERB's automatic HTML escaping, leading to cross-site scripting (XSS). An attacker could inject arbitrary HTML or JavaScript, potentially compromising user sessions or data within the browser context of the application.
Mitigation
The issue is fixed in Active Support versions 8.1.2.1, 8.0.4.1, and 7.2.3.1. The patch introduces a mark_unsafe! method to properly propagate the unsafe flag. [3] Users should upgrade to the patched versions as soon as possible.
AI Insight generated on May 18, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
activesupportRubyGems | >= 8.1.0.beta1, < 8.1.2.1 | 8.1.2.1 |
activesupportRubyGems | >= 8.0.0.beta1, < 8.0.4.1 | 8.0.4.1 |
activesupportRubyGems | < 7.2.3.1 | 7.2.3.1 |
Affected products
2- Range: < 7.2.3.1, < 8.0.4.1, < 8.1.2.1
- rails/activesupportv5Range: >= 8.1.0.beta1, < 8.1.2.1
Patches
3c1ad0e8e1972Fix `SafeBuffer#%` to preserve unsafe status
2 files changed · +18 −2
activesupport/lib/active_support/core_ext/string/output_safety.rb+3 −1 modified@@ -128,7 +128,9 @@ def %(args) escaped_args = Array(args).map { |arg| explicit_html_escape_interpolated_argument(arg) } end - self.class.new(super(escaped_args)) + new_safe_buffer = self.class.new(super(escaped_args)) + new_safe_buffer.instance_variable_set(:@html_safe, @html_safe) + new_safe_buffer end attr_reader :html_safe
activesupport/test/safe_buffer_test.rb+15 −1 modified@@ -155,10 +155,24 @@ def test_titleize multiplied_safe_buffer = "<br />".html_safe * 2 assert_predicate multiplied_safe_buffer, :html_safe? - multiplied_unsafe_buffer = @buffer.gsub("", "<>") * 2 + @buffer.gsub!("", "<>") + assert_not_predicate @buffer, :html_safe? + multiplied_unsafe_buffer = @buffer * 2 assert_not_predicate multiplied_unsafe_buffer, :html_safe? end + test "Should preserve html_safe? status on format" do + safe_buffer = "<br />%{name}".html_safe + assert_predicate safe_buffer, :html_safe? + safe_buffer = safe_buffer % { name: "George" } + assert_predicate safe_buffer, :html_safe? + + unsafe_buffer = @buffer.gsub!("", "<%{name}>") + assert_not_predicate unsafe_buffer, :html_safe? + unsafe_buffer = unsafe_buffer % { name: "George" } + assert_not_predicate unsafe_buffer, :html_safe? + end + test "Should concat as a normal string when safe" do clean = "hello".html_safe @buffer.gsub!("", "<>")
50d732af3b7cFix `SafeBuffer#%` to preserve unsafe status
2 files changed · +26 −3
activesupport/lib/active_support/core_ext/string/output_safety.rb+11 −2 modified@@ -116,7 +116,7 @@ def *(_) new_string = super new_safe_buffer = new_string.is_a?(SafeBuffer) ? new_string : SafeBuffer.new(new_string) if @html_unsafe - new_safe_buffer.instance_variable_set(:@html_unsafe, true) + new_safe_buffer.mark_unsafe! end new_safe_buffer end @@ -129,7 +129,11 @@ def %(args) escaped_args = Array(args).map { |arg| explicit_html_escape_interpolated_argument(arg) } end - self.class.new(super(escaped_args)) + new_safe_buffer = self.class.new(super(escaped_args)) + if @html_unsafe + new_safe_buffer.mark_unsafe! + end + new_safe_buffer end def html_safe? @@ -194,6 +198,11 @@ def #{unsafe_method}!(*args, &block) # def gsub!(*args, &block) EOT end + protected + def mark_unsafe! + @html_unsafe = true + end + private def explicit_html_escape_interpolated_argument(arg) (!html_safe? || arg.html_safe?) ? arg : ERB::Util.unwrapped_html_escape(arg)
activesupport/test/safe_buffer_test.rb+15 −1 modified@@ -155,10 +155,24 @@ def test_titleize multiplied_safe_buffer = "<br />".html_safe * 2 assert_predicate multiplied_safe_buffer, :html_safe? - multiplied_unsafe_buffer = @buffer.gsub("", "<>") * 2 + @buffer.gsub!("", "<>") + assert_not_predicate @buffer, :html_safe? + multiplied_unsafe_buffer = @buffer * 2 assert_not_predicate multiplied_unsafe_buffer, :html_safe? end + test "Should preserve html_safe? status on format" do + safe_buffer = "<br />%{name}".html_safe + assert_predicate safe_buffer, :html_safe? + safe_buffer = safe_buffer % { name: "George" } + assert_predicate safe_buffer, :html_safe? + + unsafe_buffer = @buffer.gsub!("", "<%{name}>") + assert_not_predicate unsafe_buffer, :html_safe? + unsafe_buffer = unsafe_buffer % { name: "George" } + assert_not_predicate unsafe_buffer, :html_safe? + end + test "Should concat as a normal string when safe" do clean = "hello".html_safe @buffer.gsub!("", "<>")
6e8a81108001Fix `SafeBuffer#%` to preserve unsafe status
2 files changed · +18 −2
activesupport/lib/active_support/core_ext/string/output_safety.rb+3 −1 modified@@ -128,7 +128,9 @@ def %(args) escaped_args = Array(args).map { |arg| explicit_html_escape_interpolated_argument(arg) } end - self.class.new(super(escaped_args)) + new_safe_buffer = self.class.new(super(escaped_args)) + new_safe_buffer.instance_variable_set(:@html_safe, @html_safe) + new_safe_buffer end attr_reader :html_safe
activesupport/test/safe_buffer_test.rb+15 −1 modified@@ -155,10 +155,24 @@ def test_titleize multiplied_safe_buffer = "<br />".html_safe * 2 assert_predicate multiplied_safe_buffer, :html_safe? - multiplied_unsafe_buffer = @buffer.gsub("", "<>") * 2 + @buffer.gsub!("", "<>") + assert_not_predicate @buffer, :html_safe? + multiplied_unsafe_buffer = @buffer * 2 assert_not_predicate multiplied_unsafe_buffer, :html_safe? end + test "Should preserve html_safe? status on format" do + safe_buffer = "<br />%{name}".html_safe + assert_predicate safe_buffer, :html_safe? + safe_buffer = safe_buffer % { name: "George" } + assert_predicate safe_buffer, :html_safe? + + unsafe_buffer = @buffer.gsub!("", "<%{name}>") + assert_not_predicate unsafe_buffer, :html_safe? + unsafe_buffer = unsafe_buffer % { name: "George" } + assert_not_predicate unsafe_buffer, :html_safe? + end + test "Should concat as a normal string when safe" do clean = "hello".html_safe @buffer.gsub!("", "<>")
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
10- github.com/advisories/GHSA-89vf-4333-qx8vghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-33170ghsaADVISORY
- github.com/rails/rails/commit/50d732af3b7c8aaf63cbcca0becbc00279b215b7ghsax_refsource_MISCWEB
- github.com/rails/rails/commit/6e8a81108001d58043de9e54a06fca58962fc2dbghsax_refsource_MISCWEB
- github.com/rails/rails/commit/c1ad0e8e1972032f3395853a5e99cea035035bebghsax_refsource_MISCWEB
- github.com/rails/rails/releases/tag/v7.2.3.1ghsax_refsource_MISCWEB
- github.com/rails/rails/releases/tag/v8.0.4.1ghsax_refsource_MISCWEB
- github.com/rails/rails/releases/tag/v8.1.2.1ghsax_refsource_MISCWEB
- github.com/rails/rails/security/advisories/GHSA-89vf-4333-qx8vghsax_refsource_CONFIRMWEB
- github.com/rubysec/ruby-advisory-db/blob/master/gems/activesupport/CVE-2026-33170.ymlghsaWEB
News mentions
3- Object First Fleet Manager simplifies distributed backup storageHelp Net Security · May 8, 2026
- The EOL Blind Spot in Your CVE Feed: What SCA Tools Don't Check.BleepingComputer · May 5, 2026
- The EOL Blind Spot in Your CVE Feed: What SCA Tools MissBleepingComputer · May 5, 2026