VYPR
High severityNVD Advisory· Published Dec 14, 2022· Updated Nov 3, 2025

Uncontrolled Recursion in Loofah

CVE-2022-23516

Description

Loofah is a general library for manipulating and transforming HTML/XML documents and fragments, built on top of Nokogiri. Loofah >= 2.2.0, < 2.19.1 uses recursion for sanitizing CDATA sections, making it susceptible to stack exhaustion and raising a SystemStackError exception. This may lead to a denial of service through CPU resource consumption. This issue is patched in version 2.19.1. Users who are unable to upgrade may be able to mitigate this vulnerability by limiting the length of the strings that are sanitized.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Loofah < 2.19.1 uses recursion to sanitize CDATA sections, causing stack exhaustion and denial of service via CPU consumption.

Loofah is a Ruby library for manipulating and transforming HTML/XML documents, built on top of Nokogiri. In versions 2.2.0 through 2.19.0, the sanitization of CDATA sections is implemented recursively. This design flaw can lead to stack exhaustion when processing deeply nested or large CDATA sections, raising a SystemStackError exception and consuming excessive CPU resources [1][2].

An attacker can exploit this vulnerability by providing a crafted HTML/XML document containing a CDATA section that triggers deep recursion. No authentication is required if the application sanitizes user-supplied input. The attack surface includes any application that uses Loofah to sanitize untrusted HTML/XML, such as web applications that allow user-generated content [2].

Successful exploitation results in a denial of service due to CPU resource consumption, potentially causing the application to become unresponsive. The vulnerability does not lead to code execution or data leakage [2].

The issue is patched in Loofah version 2.19.1, which replaces the recursive approach with an escaping solution (see commit [3]). Users who are unable to upgrade can mitigate the vulnerability by limiting the length of strings that are sanitized [2][3].

AI Insight generated on May 20, 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
loofahRubyGems
>= 2.2.0, < 2.19.12.19.1

Affected products

8

Patches

1
86f7f6364491

fix: replace recursive approach to cdata with escaping solution

https://github.com/flavorjones/loofahMike DalessioAug 27, 2022via ghsa
4 files changed · +60 9
  • lib/loofah/html5/scrub.rb+40 0 modified
    @@ -182,6 +182,46 @@ def force_correct_attribute_escaping!(node)
                 end.force_encoding(encoding)
               end
             end
    +
    +        def cdata_needs_escaping?(node)
    +          # Nokogiri's HTML4 parser on JRuby doesn't flag the child of a `style` or `script` tag as cdata, but it acts that way
    +          node.cdata? || (Nokogiri.jruby? && node.text? && (node.parent.name == "style" || node.parent.name == "script"))
    +        end
    +
    +        def cdata_escape(node)
    +          escaped_text = escape_tags(node.text)
    +          if Nokogiri.jruby?
    +            node.document.create_text_node(escaped_text)
    +          else
    +            node.document.create_cdata(escaped_text)
    +          end
    +        end
    +
    +        TABLE_FOR_ESCAPE_HTML__ = {
    +          '<' => '&lt;',
    +          '>' => '&gt;',
    +          '&' => '&amp;',
    +        }
    +
    +        def escape_tags(string)
    +          # modified version of CGI.escapeHTML from ruby 3.1
    +          enc = string.encoding
    +          unless enc.ascii_compatible?
    +            if enc.dummy?
    +              origenc = enc
    +              enc = Encoding::Converter.asciicompat_encoding(enc)
    +              string = enc ? string.encode(enc) : string.b
    +            end
    +            table = Hash[TABLE_FOR_ESCAPE_HTML__.map {|pair|pair.map {|s|s.encode(enc)}}]
    +            string = string.gsub(/#{"[<>&]".encode(enc)}/, table)
    +            string.encode!(origenc) if origenc
    +            string
    +          else
    +            string = string.b
    +            string.gsub!(/[<>&]/, TABLE_FOR_ESCAPE_HTML__)
    +            string.force_encoding(enc)
    +          end
    +        end
           end
         end
       end
    
  • lib/loofah/scrubber.rb+4 0 modified
    @@ -108,6 +108,10 @@ def html5lib_sanitize(node)
               return Scrubber::CONTINUE
             end
           when Nokogiri::XML::Node::TEXT_NODE, Nokogiri::XML::Node::CDATA_SECTION_NODE
    +        if HTML5::Scrub.cdata_needs_escaping?(node)
    +          node.before(HTML5::Scrub.cdata_escape(node))
    +          return Scrubber::STOP
    +        end
             return Scrubber::CONTINUE
           end
           Scrubber::STOP
    
  • lib/loofah/scrubbers.rb+2 6 modified
    @@ -100,13 +100,9 @@ def initialize
     
           def scrub(node)
             return CONTINUE if html5lib_sanitize(node) == CONTINUE
    -        if node.children.length == 1 && node.children.first.cdata?
    -          sanitized_text = Loofah.fragment(node.children.first.to_html).scrub!(:strip).to_html
    -          node.before Nokogiri::XML::Text.new(sanitized_text, node.document)
    -        else
    -          node.before node.children
    -        end
    +        node.before(node.children)
             node.remove
    +        return STOP
           end
         end
     
    
  • test/integration/test_ad_hoc.rb+14 3 modified
    @@ -100,17 +100,28 @@ def test_return_empty_string_when_nothing_left
         end
     
         def test_nested_script_cdata_tags_should_be_scrubbed
    -      html = "<script><script src='malicious.js'></script>"
    +      html = "<script><script src=\"malicious.js\">this & that</script>"
           stripped = Loofah.fragment(html).scrub!(:strip)
    +
           assert_empty stripped.xpath("//script")
    -      refute_match("<script", stripped.to_html)
    +      assert_equal("&lt;script src=\"malicious.js\"&gt;this &amp; that", stripped.to_html)
         end
     
         def test_nested_script_cdata_tags_should_be_scrubbed_2
           html = "<script><script>alert('a');</script></script>"
           stripped = Loofah.fragment(html).scrub!(:strip)
    +
           assert_empty stripped.xpath("//script")
    -      refute_match("<script", stripped.to_html)
    +      assert_equal("&lt;script&gt;alert('a');", stripped.to_html)
    +    end
    +
    +    def test_nested_script_cdata_tags_should_be_scrubbed_max_recursion
    +      n = 40
    +      html = "<div>" + ("<script>" * n) + "alert(1);" + ("</script>" * n) + "</div>"
    +      expected = "<div>" + ("&lt;script&gt;" * (n-1)) + "alert(1);</div>"
    +      actual = Loofah.fragment(html).scrub!(:strip).to_html
    +
    +      assert_equal(expected, actual)
         end
     
         def test_removal_of_all_tags
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.