CVE-2022-27777
Description
A XSS Vulnerability in Action View tag helpers >= 5.2.0 and < 5.2.0 which would allow an attacker to inject content if able to control input into specific attributes.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
XSS vulnerability in Action View tag helpers allows attacker to inject malicious content via attribute names in Rails 5.2.0 to before 5.2.7.1, 6.0.0 to before 6.0.4.8, 6.1.0 to before 6.1.5.1, and 7.0.0 to before 7.0.2.4.
Vulnerability
A cross-site scripting (XSS) vulnerability exists in Action View tag helpers in Ruby on Rails versions 5.2.0 through 5.2.7, 6.0.0 through 6.0.4.7, 6.1.0 through 6.1.5.0, and 7.0.0 through 7.0.2.3. The vulnerability occurs when an attacker can control input used as tag names or attribute names (e.g., aria-* or data-* attributes). The helpers fail to properly escape dangerous characters in these names, allowing injection of arbitrary HTML attributes or script payloads [1][3].
Exploitation
An attacker need only be able to supply input that is used as a tag name or an attribute name in any Action View tag helper (e.g., tag, tag.div, etc.). No authentication is required if the application renders user-controlled data directly into these fields. The attacker can craft a string containing malicious characters such as <, >, ", ', or = to break out of the intended attribute context and inject additional attributes or script elements [1].
Impact
Successful exploitation allows an attacker to execute arbitrary JavaScript in the context of the victim's browser, leading to potential session hijacking, defacement, or data exfiltration. The vulnerability is rated as medium severity (CVSS 6.1) [2].
Mitigation
Rails versions 5.2.7.1, 6.0.4.8, 6.1.5.1, and 7.0.2.4 include a fix that escapes dangerous characters in tag and attribute names [1][3]. Users should upgrade to these or later versions immediately. If upgrading is not possible, avoid rendering user input directly in tag names or attribute names, or manually sanitize input using an allowlist [2].
AI Insight generated on May 21, 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 |
|---|---|---|
actionviewRubyGems | < 5.2.7.1 | 5.2.7.1 |
actionviewRubyGems | >= 6.0.0, < 6.0.4.8 | 6.0.4.8 |
actionviewRubyGems | >= 6.1.0, < 6.1.5.1 | 6.1.5.1 |
actionviewRubyGems | >= 7.0.0, < 7.0.2.4 | 7.0.2.4 |
Affected products
18- Action View/Action View tag helpersdescription
- ghsa-coords17 versionspkg:gem/actionviewpkg:rpm/opensuse/rubygem-actionview-5_1&distro=openSUSE%20Leap%2015.4pkg:rpm/opensuse/rubygem-activesupport-5_1&distro=openSUSE%20Leap%2015.3pkg:rpm/opensuse/rubygem-activesupport-5_1&distro=openSUSE%20Leap%2015.4pkg:rpm/suse/rubygem-actionview-4_2&distro=SUSE%20OpenStack%20Cloud%20Crowbar%208pkg:rpm/suse/rubygem-actionview-4_2&distro=SUSE%20OpenStack%20Cloud%20Crowbar%209pkg:rpm/suse/rubygem-actionview-5_1&distro=SUSE%20Linux%20Enterprise%20High%20Availability%20Extension%2015%20SP1pkg:rpm/suse/rubygem-actionview-5_1&distro=SUSE%20Linux%20Enterprise%20High%20Availability%20Extension%2015%20SP2pkg:rpm/suse/rubygem-actionview-5_1&distro=SUSE%20Linux%20Enterprise%20High%20Availability%20Extension%2015%20SP3pkg:rpm/suse/rubygem-actionview-5_1&distro=SUSE%20Linux%20Enterprise%20High%20Availability%20Extension%2015%20SP4pkg:rpm/suse/rubygem-activesupport-4_2&distro=SUSE%20OpenStack%20Cloud%20Crowbar%208pkg:rpm/suse/rubygem-activesupport-4_2&distro=SUSE%20OpenStack%20Cloud%20Crowbar%209pkg:rpm/suse/rubygem-activesupport-5_1&distro=SUSE%20Linux%20Enterprise%20High%20Availability%20Extension%2015pkg:rpm/suse/rubygem-activesupport-5_1&distro=SUSE%20Linux%20Enterprise%20High%20Availability%20Extension%2015%20SP1pkg:rpm/suse/rubygem-activesupport-5_1&distro=SUSE%20Linux%20Enterprise%20High%20Availability%20Extension%2015%20SP2pkg:rpm/suse/rubygem-activesupport-5_1&distro=SUSE%20Linux%20Enterprise%20High%20Availability%20Extension%2015%20SP3pkg:rpm/suse/rubygem-activesupport-5_1&distro=SUSE%20Linux%20Enterprise%20High%20Availability%20Extension%2015%20SP4
< 5.2.7.1+ 16 more
- (no CPE)range: < 5.2.7.1
- (no CPE)range: < 5.1.4-150000.3.6.1
- (no CPE)range: < 5.1.4-150000.3.9.1
- (no CPE)range: < 5.1.4-150000.3.9.1
- (no CPE)range: < 4.2.9-9.15.1
- (no CPE)range: < 4.2.9-9.15.1
- (no CPE)range: < 5.1.4-150000.3.6.1
- (no CPE)range: < 5.1.4-150000.3.6.1
- (no CPE)range: < 5.1.4-150000.3.6.1
- (no CPE)range: < 5.1.4-150000.3.6.1
- (no CPE)range: < 4.2.9-7.12.1
- (no CPE)range: < 4.2.9-7.12.1
- (no CPE)range: < 5.1.4-150000.3.9.1
- (no CPE)range: < 5.1.4-150000.3.9.1
- (no CPE)range: < 5.1.4-150000.3.9.1
- (no CPE)range: < 5.1.4-150000.3.9.1
- (no CPE)range: < 5.1.4-150000.3.9.1
Patches
1649516ce0febFix and add protections for XSS in names.
6 files changed · +171 −19
actionview/CHANGELOG.md+9 −0 modified@@ -1,3 +1,12 @@ +* Fix and add protections for XSS in `ActionView::Helpers` and `ERB::Util`. + + Escape dangerous characters in names of tags and names of attributes in the + tag helpers, following the XML specification. Rename the option + `:escape_attributes` to `:escape`, to simplify by applying the option to the + whole tag. + + *Álvaro Martín Fraguas* + * Extend audio_tag and video_tag to accept Active Storage attachments. Now it's possible to write
actionview/lib/action_view/helpers/tag_helper.rb+17 −8 modified@@ -65,19 +65,24 @@ def p(*arguments, **options, &block) tag_string(:p, *arguments, **options, &block) end - def tag_string(name, content = nil, escape_attributes: true, **options, &block) + def tag_string(name, content = nil, escape: true, **options, &block) content = @view_context.capture(self, &block) if block_given? self_closing = SVG_SELF_CLOSING_ELEMENTS.include?(name) if (HTML_VOID_ELEMENTS.include?(name) || self_closing) && content.nil? - "<#{name.to_s.dasherize}#{tag_options(options, escape_attributes)}#{self_closing ? " />" : ">"}".html_safe + "<#{name.to_s.dasherize}#{tag_options(options, escape)}#{self_closing ? " />" : ">"}".html_safe else - content_tag_string(name.to_s.dasherize, content || "", options, escape_attributes) + content_tag_string(name.to_s.dasherize, content || "", options, escape) end end def content_tag_string(name, content, options, escape = true) tag_options = tag_options(options, escape) if options - content = ERB::Util.unwrapped_html_escape(content) if escape + + if escape + name = ERB::Util.xml_name_escape(name) + content = ERB::Util.unwrapped_html_escape(content) + end + "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}</#{name}>".html_safe end @@ -128,6 +133,8 @@ def boolean_tag_option(key) end def tag_option(key, value, escape) + key = ERB::Util.xml_name_escape(key) if escape + case value when Array, Hash value = TagHelper.build_tag_values(value) if key.to_s == "class" @@ -138,6 +145,7 @@ def tag_option(key, value, escape) value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s end value = value.gsub('"', """) if value.include?('"') + %(#{key}="#{value}") end @@ -217,13 +225,13 @@ def method_missing(called, *args, **options, &block) # tag.div data: { city_state: %w( Chicago IL ) } # # => <div data-city-state="["Chicago","IL"]"></div> # - # The generated attributes are escaped by default. This can be disabled using - # +escape_attributes+. + # The generated tag names and attributes are escaped by default. This can be disabled using + # +escape+. # # tag.img src: 'open & shut.png' # # => <img src="open & shut.png"> # - # tag.img src: 'open & shut.png', escape_attributes: false + # tag.img src: 'open & shut.png', escape: false # # => <img src="open & shut.png"> # # The tag builder respects @@ -301,6 +309,7 @@ def tag(name = nil, options = nil, open = false, escape = true) if name.nil? tag_builder else + name = ERB::Util.xml_name_escape(name) if escape "<#{name}#{tag_builder.tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe end end @@ -309,7 +318,7 @@ def tag(name = nil, options = nil, open = false, escape = true) # HTML attributes by passing an attributes hash to +options+. # Instead of passing the content as an argument, you can also use a block # in which case, you pass your +options+ as the second parameter. - # Set escape to false to disable attribute value escaping. + # Set escape to false to disable escaping. # Note: this is legacy syntax, see +tag+ method description for details. # # ==== Options
actionview/test/template/tag_helper_test.rb+84 −11 modified@@ -7,6 +7,8 @@ class TagHelperTest < ActionView::TestCase tests ActionView::Helpers::TagHelper + COMMON_DANGEROUS_CHARS = "&<>\"' %*+,/;=^|" + def test_tag assert_equal "<br />", tag("br") assert_equal "<br clear=\"left\" />", tag(:br, clear: "left") @@ -119,6 +121,77 @@ def test_tag_builder_do_not_modify_html_safe_options assert html_safe_str.html_safe? end + def test_tag_with_dangerous_name + assert_equal "<#{"_" * COMMON_DANGEROUS_CHARS.size} />", + tag(COMMON_DANGEROUS_CHARS) + + assert_equal "<#{COMMON_DANGEROUS_CHARS} />", + tag(COMMON_DANGEROUS_CHARS, nil, false, false) + end + + def test_tag_builder_with_dangerous_name + escaped_dangerous_chars = "_" * COMMON_DANGEROUS_CHARS.size + assert_equal "<#{escaped_dangerous_chars}></#{escaped_dangerous_chars}>", + tag.public_send(COMMON_DANGEROUS_CHARS.to_sym) + + assert_equal "<#{COMMON_DANGEROUS_CHARS}></#{COMMON_DANGEROUS_CHARS}>", + tag.public_send(COMMON_DANGEROUS_CHARS.to_sym, nil, escape: false) + end + + def test_tag_with_dangerous_aria_attribute_name + escaped_dangerous_chars = "_" * COMMON_DANGEROUS_CHARS.size + assert_equal "<the-name aria-#{escaped_dangerous_chars}=\"the value\" />", + tag("the-name", aria: { COMMON_DANGEROUS_CHARS => "the value" }) + + assert_equal "<the-name aria-#{COMMON_DANGEROUS_CHARS}=\"the value\" />", + tag("the-name", { aria: { COMMON_DANGEROUS_CHARS => "the value" } }, false, false) + end + + def test_tag_builder_with_dangerous_aria_attribute_name + escaped_dangerous_chars = "_" * COMMON_DANGEROUS_CHARS.size + assert_equal "<the-name aria-#{escaped_dangerous_chars}=\"the value\"></the-name>", + tag.public_send(:"the-name", aria: { COMMON_DANGEROUS_CHARS => "the value" }) + + assert_equal "<the-name aria-#{COMMON_DANGEROUS_CHARS}=\"the value\"></the-name>", + tag.public_send(:"the-name", aria: { COMMON_DANGEROUS_CHARS => "the value" }, escape: false) + end + + def test_tag_with_dangerous_data_attribute_name + escaped_dangerous_chars = "_" * COMMON_DANGEROUS_CHARS.size + assert_equal "<the-name data-#{escaped_dangerous_chars}=\"the value\" />", + tag("the-name", data: { COMMON_DANGEROUS_CHARS => "the value" }) + + assert_equal "<the-name data-#{COMMON_DANGEROUS_CHARS}=\"the value\" />", + tag("the-name", { data: { COMMON_DANGEROUS_CHARS => "the value" } }, false, false) + end + + def test_tag_builder_with_dangerous_data_attribute_name + escaped_dangerous_chars = "_" * COMMON_DANGEROUS_CHARS.size + assert_equal "<the-name data-#{escaped_dangerous_chars}=\"the value\"></the-name>", + tag.public_send(:"the-name", data: { COMMON_DANGEROUS_CHARS => "the value" }) + + assert_equal "<the-name data-#{COMMON_DANGEROUS_CHARS}=\"the value\"></the-name>", + tag.public_send(:"the-name", data: { COMMON_DANGEROUS_CHARS => "the value" }, escape: false) + end + + def test_tag_with_dangerous_unknown_attribute_name + escaped_dangerous_chars = "_" * COMMON_DANGEROUS_CHARS.size + assert_equal "<the-name #{escaped_dangerous_chars}=\"the value\" />", + tag("the-name", COMMON_DANGEROUS_CHARS => "the value") + + assert_equal "<the-name #{COMMON_DANGEROUS_CHARS}=\"the value\" />", + tag("the-name", { COMMON_DANGEROUS_CHARS => "the value" }, false, false) + 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>", + tag.public_send(:"the-name", COMMON_DANGEROUS_CHARS => "the value") + + assert_equal "<the-name #{COMMON_DANGEROUS_CHARS}=\"the value\"></the-name>", + tag.public_send(:"the-name", COMMON_DANGEROUS_CHARS => "the value", escape: false) + end + def test_content_tag assert_equal "<a href=\"create\">Create</a>", content_tag("a", "Create", "href" => "create") assert_predicate content_tag("a", "Create", "href" => "create"), :html_safe? @@ -138,7 +211,7 @@ def test_tag_builder_with_content assert_equal "<p><script>evil_js</script></p>", tag.p("<script>evil_js</script>") assert_equal "<p><script>evil_js</script></p>", - tag.p("<script>evil_js</script>", escape_attributes: false) + tag.p("<script>evil_js</script>", escape: false) assert_equal '<input pattern="\w+">', tag.input(pattern: /\w+/) end @@ -254,10 +327,10 @@ def test_content_tag_with_unescaped_array_class end def test_tag_builder_with_unescaped_array_class - str = tag.p "limelight", class: ["song", "play>"], escape_attributes: false + str = tag.p "limelight", class: ["song", "play>"], escape: false assert_equal "<p class=\"song play>\">limelight</p>", str - str = tag.p "limelight", class: ["song", ["play>"]], escape_attributes: false + str = tag.p "limelight", class: ["song", ["play>"]], escape: false assert_equal "<p class=\"song play>\">limelight</p>", str end @@ -276,7 +349,7 @@ def test_content_tag_with_unescaped_empty_array_class end def test_tag_builder_with_unescaped_empty_array_class - str = tag.p "limelight", class: [], escape_attributes: false + str = tag.p "limelight", class: [], escape: false assert_equal '<p class="">limelight</p>', str end @@ -347,10 +420,10 @@ def test_content_tag_with_unescaped_conditional_hash_classes end def test_tag_builder_with_unescaped_conditional_hash_classes - str = tag.p "limelight", class: { "song": true, "play>": true }, escape_attributes: false + str = tag.p "limelight", class: { "song": true, "play>": true }, escape: false assert_equal "<p class=\"song play>\">limelight</p>", str - str = tag.p "limelight", class: ["song", { "play>": true }], escape_attributes: false + str = tag.p "limelight", class: ["song", { "play>": true }], escape: false assert_equal "<p class=\"song play>\">limelight</p>", str end @@ -468,11 +541,11 @@ def test_disable_escaping end def test_tag_builder_disable_escaping - assert_equal '<a href="&"></a>', tag.a(href: "&", escape_attributes: false) - assert_equal '<a href="&">cnt</a>', tag.a(href: "&", escape_attributes: false) { "cnt" } - assert_equal '<br data-hidden="&">', tag.br("data-hidden": "&", escape_attributes: false) - assert_equal '<a href="&">content</a>', tag.a("content", href: "&", escape_attributes: false) - assert_equal '<a href="&">content</a>', tag.a(href: "&", escape_attributes: false) { "content" } + assert_equal '<a href="&"></a>', tag.a(href: "&", escape: false) + assert_equal '<a href="&">cnt</a>', tag.a(href: "&", escape: false) { "cnt" } + assert_equal '<br data-hidden="&">', tag.br("data-hidden": "&", escape: false) + assert_equal '<a href="&">content</a>', tag.a("content", href: "&", escape: false) + assert_equal '<a href="&">content</a>', tag.a(href: "&", escape: false) { "content" } end def test_data_attributes
activesupport/CHANGELOG.md+7 −0 modified@@ -1,3 +1,10 @@ +* Fix and add protections for XSS in `ActionView::Helpers` and `ERB::Util`. + + Add the method `ERB::Util.xml_name_escape` to escape dangerous characters + in names of tags and names of attributes, following the specification of XML. + + *Álvaro Martín Fraguas* + * Respect `ActiveSupport::Logger.new`'s `:formatter` keyword argument The stdlib `Logger::new` allows passing a `:formatter` keyword argument to
activesupport/lib/active_support/core_ext/string/output_safety.rb+28 −0 modified@@ -11,6 +11,14 @@ module Util HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+)|(#[xX][\dA-Fa-f]+));)/ JSON_ESCAPE_REGEXP = /[\u2028\u2029&><]/u + # Following XML requirements: https://www.w3.org/TR/REC-xml/#NT-Name + TAG_NAME_START_REGEXP_SET = ":A-Z_a-z\u{C0}-\u{D6}\u{D8}-\u{F6}\u{F8}-\u{2FF}\u{370}-\u{37D}\u{37F}-\u{1FFF}" \ + "\u{200C}-\u{200D}\u{2070}-\u{218F}\u{2C00}-\u{2FEF}\u{3001}-\u{D7FF}\u{F900}-\u{FDCF}" \ + "\u{FDF0}-\u{FFFD}\u{10000}-\u{EFFFF}" + TAG_NAME_START_REGEXP = /[^#{TAG_NAME_START_REGEXP_SET}]/ + TAG_NAME_FOLLOWING_REGEXP = /[^#{TAG_NAME_START_REGEXP_SET}\-.0-9\u{B7}\u{0300}-\u{036F}\u{203F}-\u{2040}]/ + TAG_NAME_REPLACEMENT_CHAR = "_" + # A utility method for escaping HTML tag characters. # This method is also aliased as <tt>h</tt>. # @@ -115,6 +123,26 @@ def json_escape(s) end module_function :json_escape + + # A utility method for escaping XML names of tags and names of attributes. + # + # xml_name_escape('1 < 2 & 3') + # # => "1___2___3" + # + # It follows the requirements of the specification: https://www.w3.org/TR/REC-xml/#NT-Name + def xml_name_escape(name) + name = name.to_s + return "" if name.blank? + + starting_char = name[0].gsub(TAG_NAME_START_REGEXP, TAG_NAME_REPLACEMENT_CHAR) + + return starting_char if name.size == 1 + + following_chars = name[1..-1].gsub(TAG_NAME_FOLLOWING_REGEXP, TAG_NAME_REPLACEMENT_CHAR) + + starting_char + following_chars + end + module_function :xml_name_escape end end
activesupport/test/core_ext/string_ext_test.rb+26 −0 modified@@ -1049,6 +1049,32 @@ def to_s expected = "© <" assert_equal expected, ERB::Util.html_escape_once(string) end + + test "ERB::Util.xml_name_escape should escape unsafe characters for XML names" do + unsafe_char = ">" + safe_char = "Á" + safe_char_after_start = "3" + + assert_equal "_", ERB::Util.xml_name_escape(unsafe_char) + assert_equal "_#{safe_char}", ERB::Util.xml_name_escape(unsafe_char + safe_char) + assert_equal "__", ERB::Util.xml_name_escape(unsafe_char * 2) + + assert_equal "__#{safe_char}_", + ERB::Util.xml_name_escape("#{unsafe_char * 2}#{safe_char}#{unsafe_char}") + + assert_equal safe_char + safe_char_after_start, + ERB::Util.xml_name_escape(safe_char + safe_char_after_start) + + assert_equal "_#{safe_char}", + ERB::Util.xml_name_escape(safe_char_after_start + safe_char) + + assert_equal "img_src_nonexistent_onerror_alert_1_", + ERB::Util.xml_name_escape("img src=nonexistent onerror=alert(1)") + + common_dangerous_chars = "&<>\"' %*+,/;=^|" + assert_equal "_" * common_dangerous_chars.size, + ERB::Util.xml_name_escape(common_dangerous_chars) + end end class StringExcludeTest < ActiveSupport::TestCase
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
9- github.com/advisories/GHSA-ch3h-j2vf-95pvghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-27777ghsaADVISORY
- www.debian.org/security/2023/dsa-5372ghsavendor-advisoryWEB
- discuss.rubyonrails.org/t/cve-2022-27777-possible-xss-vulnerability-in-action-view-tag-helpers/80534ghsaWEB
- github.com/rails/rails/commit/649516ce0feb699ae06a8c5e81df75d460cc9a85ghsaWEB
- github.com/rubysec/ruby-advisory-db/blob/master/gems/actionview/CVE-2022-27777.ymlghsaWEB
- groups.google.com/g/ruby-security-ann/c/9wJPEDv-iRwghsaWEB
- lists.debian.org/debian-lts-announce/2022/09/msg00002.htmlghsamailing-listWEB
- rubyonrails.org/2022/4/26/Rails-7-0-2-4-6-1-5-1-6-0-4-8-and-5-2-7-1-have-been-releasedghsaWEB
News mentions
0No linked articles in our index yet.