VYPR
Moderate severityNVD Advisory· Published May 26, 2022· Updated Aug 3, 2024

CVE-2022-27777

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.

PackageAffected versionsPatched versions
actionviewRubyGems
< 5.2.7.15.2.7.1
actionviewRubyGems
>= 6.0.0, < 6.0.4.86.0.4.8
actionviewRubyGems
>= 6.1.0, < 6.1.5.16.1.5.1
actionviewRubyGems
>= 7.0.0, < 7.0.2.47.0.2.4

Affected products

18

Patches

1
649516ce0feb

Fix and add protections for XSS in names.

https://github.com/rails/railsAlvaro Martin FraguasJan 5, 2022via ghsa
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('"', "&quot;") 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="[&quot;Chicago&quot;,&quot;IL&quot;]"></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 &amp; 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>&lt;script&gt;evil_js&lt;/script&gt;</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="&amp;"></a>', tag.a(href: "&amp;", escape_attributes: false)
    -    assert_equal '<a href="&amp;">cnt</a>', tag.a(href: "&amp;", escape_attributes: false) { "cnt" }
    -    assert_equal '<br data-hidden="&amp;">', tag.br("data-hidden": "&amp;", escape_attributes: false)
    -    assert_equal '<a href="&amp;">content</a>', tag.a("content", href: "&amp;", escape_attributes: false)
    -    assert_equal '<a href="&amp;">content</a>', tag.a(href: "&amp;", escape_attributes: false) { "content" }
    +    assert_equal '<a href="&amp;"></a>', tag.a(href: "&amp;", escape: false)
    +    assert_equal '<a href="&amp;">cnt</a>', tag.a(href: "&amp;", escape: false) { "cnt" }
    +    assert_equal '<br data-hidden="&amp;">', tag.br("data-hidden": "&amp;", escape: false)
    +    assert_equal '<a href="&amp;">content</a>', tag.a("content", href: "&amp;", escape: false)
    +    assert_equal '<a href="&amp;">content</a>', tag.a(href: "&amp;", 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 = "© &lt;"
         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

News mentions

0

No linked articles in our index yet.