CVE-2026-33168
Description
Action View provides conventions and helpers for building web pages with the Rails framework. Prior to versions 8.1.2.1, 8.0.4.1, and 7.2.3.1, when a blank string is used as an HTML attribute name in Action View tag helpers, the attribute escaping is bypassed, producing malformed HTML. A carefully crafted attribute value could then be misinterpreted by the browser as a separate attribute name, possibly leading to XSS. Applications that allow users to specify custom HTML attributes are affected. Versions 8.1.2.1, 8.0.4.1, and 7.2.3.1 contain a patch.
Affected products
1Patches
363f5ad83edaaSkip blank attribute names in Action View tag helpers
3 files changed · +39 −2
actionview/CHANGELOG.md+5 −0 modified@@ -1,3 +1,8 @@ +* Skip blank attribute names in tag helpers to avoid generating invalid HTML. + + *Mike Dalessio* + + ## Rails 8.1.2 (January 08, 2026) ## * Fix `file_field` to join mime types with a comma when provided as Array
actionview/lib/action_view/helpers/tag_helper.rb+5 −2 modified@@ -237,16 +237,19 @@ def tag_options(options, escape = true) # :nodoc: output = +"" sep = " " options.each_pair do |key, value| + next if key.blank? + type = TAG_TYPES[key] if type == :data && value.is_a?(Hash) value.each_pair do |k, v| - next if v.nil? + next if k.blank? || v.nil? + output << sep output << prefix_tag_option(key, k, v, escape) end elsif type == :aria && value.is_a?(Hash) value.each_pair do |k, v| - next if v.nil? + next if k.blank? || v.nil? case v when Array, Hash
actionview/test/template/tag_helper_test.rb+29 −0 modified@@ -107,6 +107,27 @@ def test_tag_options_accepts_blank_option assert_equal "<p included=\"\" />", tag("p", included: "") end + def test_tag_options_rejects_blank_key + assert_equal "<p />", tag("p", "" => "value") + assert_equal "<p />", tag("p", nil => "value") + assert_equal '<p class="a" />', tag("p", "" => "value", "class" => "a") + assert_equal '<p class="a" />', tag("p", nil => "value", "class" => "a") + end + + def test_tag_options_rejects_blank_data_key + assert_equal "<p />", tag("p", data: { "" => "value" }) + assert_equal "<p />", tag("p", data: { nil => "value" }) + assert_equal '<p data-x="y" />', tag("p", data: { "" => "value", "x" => "y" }) + assert_equal '<p data-x="y" />', tag("p", data: { nil => "value", "x" => "y" }) + end + + def test_tag_options_rejects_blank_aria_key + assert_equal "<p />", tag("p", aria: { "" => "value" }) + assert_equal "<p />", tag("p", aria: { nil => "value" }) + assert_equal '<p aria-x="y" />', tag("p", aria: { "" => "value", "x" => "y" }) + assert_equal '<p aria-x="y" />', tag("p", aria: { nil => "value", "x" => "y" }) + end + def test_tag_builder_options_accepts_blank_option assert_equal "<p included=\"\"></p>", tag.p(included: "") end @@ -205,6 +226,14 @@ def test_tag_with_dangerous_unknown_attribute_name tag("the-name", { COMMON_DANGEROUS_CHARS => "the value" }, false, false) end + def test_tag_with_blank_attribute_name_generates_valid_markup + # https://hackerone.com/reports/3078929 + html = tag("img", "src" => "/nonexistent.png", "" => "/onerror=alert(1)") + fragment = Nokogiri::HTML5::DocumentFragment.parse(html) + attrs = fragment.at_css("img").attribute_nodes.map(&:name) + assert_equal [ "src" ], attrs + end + def test_tag_builder_with_dangerous_unknown_attribute_name escaped_dangerous_chars = "_" * COMMON_DANGEROUS_CHARS.size assert_equal "<the-name #{escaped_dangerous_chars}=\"the value\"></the-name>",
0b6f8002b52bSkip blank attribute names in Action View tag helpers
3 files changed · +39 −2
actionview/CHANGELOG.md+5 −0 modified@@ -1,3 +1,8 @@ +* Skip blank attribute names in tag helpers to avoid generating invalid HTML. + + *Mike Dalessio* + + ## Rails 7.2.3 (October 28, 2025) ## * Fix `javascript_include_tag` `type` option to accept either strings and symbols.
actionview/lib/action_view/helpers/tag_helper.rb+5 −2 modified@@ -263,16 +263,19 @@ def tag_options(options, escape = true) output = +"" sep = " " options.each_pair do |key, value| + next if key.blank? + type = TAG_TYPES[key] if type == :data && value.is_a?(Hash) value.each_pair do |k, v| - next if v.nil? + next if k.blank? || v.nil? + output << sep output << prefix_tag_option(key, k, v, escape) end elsif type == :aria && value.is_a?(Hash) value.each_pair do |k, v| - next if v.nil? + next if k.blank? || v.nil? case v when Array, Hash
actionview/test/template/tag_helper_test.rb+29 −0 modified@@ -107,6 +107,27 @@ def test_tag_options_accepts_blank_option assert_equal "<p included=\"\" />", tag("p", included: "") end + def test_tag_options_rejects_blank_key + assert_equal "<p />", tag("p", "" => "value") + assert_equal "<p />", tag("p", nil => "value") + assert_equal '<p class="a" />', tag("p", "" => "value", "class" => "a") + assert_equal '<p class="a" />', tag("p", nil => "value", "class" => "a") + end + + def test_tag_options_rejects_blank_data_key + assert_equal "<p />", tag("p", data: { "" => "value" }) + assert_equal "<p />", tag("p", data: { nil => "value" }) + assert_equal '<p data-x="y" />', tag("p", data: { "" => "value", "x" => "y" }) + assert_equal '<p data-x="y" />', tag("p", data: { nil => "value", "x" => "y" }) + end + + def test_tag_options_rejects_blank_aria_key + assert_equal "<p />", tag("p", aria: { "" => "value" }) + assert_equal "<p />", tag("p", aria: { nil => "value" }) + assert_equal '<p aria-x="y" />', tag("p", aria: { "" => "value", "x" => "y" }) + assert_equal '<p aria-x="y" />', tag("p", aria: { nil => "value", "x" => "y" }) + end + def test_tag_builder_options_accepts_blank_option assert_equal "<p included=\"\"></p>", tag.p(included: "") end @@ -205,6 +226,14 @@ def test_tag_with_dangerous_unknown_attribute_name tag("the-name", { COMMON_DANGEROUS_CHARS => "the value" }, false, false) end + def test_tag_with_blank_attribute_name_generates_valid_markup + # https://hackerone.com/reports/3078929 + html = tag("img", "src" => "/nonexistent.png", "" => "/onerror=alert(1)") + fragment = Nokogiri::HTML5::DocumentFragment.parse(html) + attrs = fragment.at_css("img").attribute_nodes.map(&:name) + assert_equal [ "src" ], attrs + end + def test_tag_builder_with_dangerous_unknown_attribute_name escaped_dangerous_chars = "_" * COMMON_DANGEROUS_CHARS.size assert_equal "<the-name #{escaped_dangerous_chars}=\"the value\"></the-name>",
c79a07df1e88Skip blank attribute names in Action View tag helpers
3 files changed · +39 −2
actionview/CHANGELOG.md+5 −0 modified@@ -1,3 +1,8 @@ +* Skip blank attribute names in tag helpers to avoid generating invalid HTML. + + *Mike Dalessio* + + ## Rails 8.0.4 (October 28, 2025) ## * Restore `add_default_name_and_id` method.
actionview/lib/action_view/helpers/tag_helper.rb+5 −2 modified@@ -250,16 +250,19 @@ def tag_options(options, escape = true) output = +"" sep = " " options.each_pair do |key, value| + next if key.blank? + type = TAG_TYPES[key] if type == :data && value.is_a?(Hash) value.each_pair do |k, v| - next if v.nil? + next if k.blank? || v.nil? + output << sep output << prefix_tag_option(key, k, v, escape) end elsif type == :aria && value.is_a?(Hash) value.each_pair do |k, v| - next if v.nil? + next if k.blank? || v.nil? case v when Array, Hash
actionview/test/template/tag_helper_test.rb+29 −0 modified@@ -107,6 +107,27 @@ def test_tag_options_accepts_blank_option assert_equal "<p included=\"\" />", tag("p", included: "") end + def test_tag_options_rejects_blank_key + assert_equal "<p />", tag("p", "" => "value") + assert_equal "<p />", tag("p", nil => "value") + assert_equal '<p class="a" />', tag("p", "" => "value", "class" => "a") + assert_equal '<p class="a" />', tag("p", nil => "value", "class" => "a") + end + + def test_tag_options_rejects_blank_data_key + assert_equal "<p />", tag("p", data: { "" => "value" }) + assert_equal "<p />", tag("p", data: { nil => "value" }) + assert_equal '<p data-x="y" />', tag("p", data: { "" => "value", "x" => "y" }) + assert_equal '<p data-x="y" />', tag("p", data: { nil => "value", "x" => "y" }) + end + + def test_tag_options_rejects_blank_aria_key + assert_equal "<p />", tag("p", aria: { "" => "value" }) + assert_equal "<p />", tag("p", aria: { nil => "value" }) + assert_equal '<p aria-x="y" />', tag("p", aria: { "" => "value", "x" => "y" }) + assert_equal '<p aria-x="y" />', tag("p", aria: { nil => "value", "x" => "y" }) + end + def test_tag_builder_options_accepts_blank_option assert_equal "<p included=\"\"></p>", tag.p(included: "") end @@ -205,6 +226,14 @@ def test_tag_with_dangerous_unknown_attribute_name tag("the-name", { COMMON_DANGEROUS_CHARS => "the value" }, false, false) end + def test_tag_with_blank_attribute_name_generates_valid_markup + # https://hackerone.com/reports/3078929 + html = tag("img", "src" => "/nonexistent.png", "" => "/onerror=alert(1)") + fragment = Nokogiri::HTML5::DocumentFragment.parse(html) + attrs = fragment.at_css("img").attribute_nodes.map(&:name) + assert_equal [ "src" ], attrs + end + def test_tag_builder_with_dangerous_unknown_attribute_name escaped_dangerous_chars = "_" * COMMON_DANGEROUS_CHARS.size assert_equal "<the-name #{escaped_dangerous_chars}=\"the value\"></the-name>",
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-v55j-83pf-r9cqghsaADVISORY
- github.com/rails/rails/commit/0b6f8002b52b9c606fd6be9e7915d9f944cf539cnvd
- github.com/rails/rails/commit/63f5ad83edaa0b976f82d46988d745426aa4a42dnvd
- github.com/rails/rails/commit/c79a07df1e88738df8f68cb0ee759ad6128ca924nvd
- github.com/rails/rails/releases/tag/v7.2.3.1nvd
- github.com/rails/rails/releases/tag/v8.0.4.1nvd
- github.com/rails/rails/releases/tag/v8.1.2.1nvd
- github.com/rails/rails/security/advisories/GHSA-v55j-83pf-r9cqnvd
- github.com/rubysec/ruby-advisory-db/blob/master/gems/actionview/CVE-2026-33168.ymlghsa
- nvd.nist.gov/vuln/detail/CVE-2026-33168ghsa
News mentions
0No linked articles in our index yet.