VYPR
Moderate severityNVD Advisory· Published Aug 1, 2024· Updated Nov 3, 2025

REXML DoS vulnerability

CVE-2024-41946

Description

REXML is an XML toolkit for Ruby. The REXML gem 3.3.2 has a DoS vulnerability when it parses an XML that has many entity expansions with SAX2 or pull parser API. The REXML gem 3.3.3 or later include the patch to fix the vulnerability.

AI Insight

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

REXML gem 3.3.2 has a denial-of-service vulnerability due to uncontrolled entity expansion when parsing XML with SAX2 or pull parser APIs.

Vulnerability

Description

CVE-2024-41946 is a denial-of-service (DoS) vulnerability in the REXML gem for Ruby, version 3.3.2. The root cause is a missing check on entity expansion count and total expansion text size when using the SAX2 or pull parser APIs. Without these limits, an attacker can craft an XML document with deeply nested or exponentially expanding entity references, causing the parser to consume excessive CPU and memory resources. The fix, introduced in commit 033d1909a8f259d5a7c53681bcaf14f13bcf0368 [1], adds counters (@entity_expansion_count and a byte-size sum) and raises an exception when the limits are exceeded.

Exploitation and

Attack Surface

The vulnerability is triggered by supplying a maliciously crafted XML document to any application that uses REXML's SAX2 or pull parser to parse user-controlled XML input. No authentication is required; the attack can be performed remotely by sending a single HTTP request (e.g., POST body) that includes the malformed XML. The classic "XML entity explosion" technique uses recursively nested entities to amplify a small input into an enormous expansion, as demonstrated in Ruby's 2008 security advisory [3]. The absence of expansion limits in the SAX2 and pull parser code paths leads to unbounded processing.

Impact

A successful attack causes a denial of service by exhausting the application's CPU and memory, potentially crashing the process or making it unresponsive. This is a high-severity issue (CVSS 7.5) [2] because it can disable services that rely on REXML for XML parsing, such as Ruby on Rails applications.

Mitigation

The vulnerability is patched in REXML gem version 3.3.3 and later [1]. Users should upgrade immediately. For applications that cannot upgrade, no direct workaround is provided; limiting XML input size or preprocessing requests may reduce risk but not eliminate it. The REXML project maintains a history of similar entity expansion protections [3], and this patch completes coverage for the SAX2 and pull parser APIs.

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
rexmlRubyGems
< 3.3.33.3.3

Affected products

126

Patches

1
033d1909a8f2

Add support for XML entity expansion limitation in SAX and pull parsers (#187)

https://github.com/ruby/rexmlNAITOH JunAug 1, 2024via ghsa
6 files changed · +222 12
  • lib/rexml/parsers/baseparser.rb+18 1 modified
    @@ -154,13 +154,15 @@ def initialize( source )
             self.stream = source
             @listeners = []
             @prefixes = Set.new
    +        @entity_expansion_count = 0
           end
     
           def add_listener( listener )
             @listeners << listener
           end
     
           attr_reader :source
    +      attr_reader :entity_expansion_count
     
           def stream=( source )
             @source = SourceFactory.create_from( source )
    @@ -513,7 +515,9 @@ def pull_event
           def entity( reference, entities )
             value = nil
             value = entities[ reference ] if entities
    -        if not value
    +        if value
    +          record_entity_expansion
    +        else
               value = DEFAULT_ENTITIES[ reference ]
               value = value[2] if value
             end
    @@ -552,12 +556,17 @@ def unnormalize( string, entities=nil, filter=nil )
             }
             matches.collect!{|x|x[0]}.compact!
             if matches.size > 0
    +          sum = 0
               matches.each do |entity_reference|
                 unless filter and filter.include?(entity_reference)
                   entity_value = entity( entity_reference, entities )
                   if entity_value
                     re = Private::DEFAULT_ENTITIES_PATTERNS[entity_reference] || /&#{entity_reference};/
                     rv.gsub!( re, entity_value )
    +                sum += rv.bytesize
    +                if sum > Security.entity_expansion_text_limit
    +                  raise "entity expansion has grown too large"
    +                end
                   else
                     er = DEFAULT_ENTITIES[entity_reference]
                     rv.gsub!( er[0], er[2] ) if er
    @@ -570,6 +579,14 @@ def unnormalize( string, entities=nil, filter=nil )
           end
     
           private
    +
    +      def record_entity_expansion
    +        @entity_expansion_count += 1
    +        if @entity_expansion_count > Security.entity_expansion_limit
    +          raise "number of entity expansions exceeded, processing aborted."
    +        end
    +      end
    +
           def need_source_encoding_update?(xml_declaration_encoding)
             return false if xml_declaration_encoding.nil?
             return false if /\AUTF-16\z/i =~ xml_declaration_encoding
    
  • lib/rexml/parsers/pullparser.rb+4 0 modified
    @@ -47,6 +47,10 @@ def add_listener( listener )
             @listeners << listener
           end
     
    +      def entity_expansion_count
    +        @parser.entity_expansion_count
    +      end
    +
           def each
             while has_next?
               yield self.pull
    
  • lib/rexml/parsers/sax2parser.rb+4 0 modified
    @@ -22,6 +22,10 @@ def source
             @parser.source
           end
     
    +      def entity_expansion_count
    +        @parser.entity_expansion_count
    +      end
    +
           def add_listener( listener )
             @parser.add_listener( listener )
           end
    
  • test/test_document.rb+14 11 modified
    @@ -41,7 +41,7 @@ def teardown
     
           class GeneralEntityTest < self
             def test_have_value
    -          xml = <<EOF
    +          xml = <<XML
     <?xml version="1.0" encoding="UTF-8"?>
     <!DOCTYPE member [
       <!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
    @@ -55,23 +55,24 @@ def test_have_value
     <member>
     &a;
     </member>
    -EOF
    +XML
     
               doc = REXML::Document.new(xml)
    -          assert_raise(RuntimeError) do
    +          assert_raise(RuntimeError.new("entity expansion has grown too large")) do
                 doc.root.children.first.value
               end
    +
               REXML::Security.entity_expansion_limit = 100
               assert_equal(100, REXML::Security.entity_expansion_limit)
               doc = REXML::Document.new(xml)
    -          assert_raise(RuntimeError) do
    +          assert_raise(RuntimeError.new("number of entity expansions exceeded, processing aborted.")) do
                 doc.root.children.first.value
               end
               assert_equal(101, doc.entity_expansion_count)
             end
     
             def test_empty_value
    -          xml = <<EOF
    +          xml = <<XML
     <?xml version="1.0" encoding="UTF-8"?>
     <!DOCTYPE member [
       <!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
    @@ -85,23 +86,24 @@ def test_empty_value
     <member>
     &a;
     </member>
    -EOF
    +XML
     
               doc = REXML::Document.new(xml)
    -          assert_raise(RuntimeError) do
    +          assert_raise(RuntimeError.new("number of entity expansions exceeded, processing aborted.")) do
                 doc.root.children.first.value
               end
    +
               REXML::Security.entity_expansion_limit = 100
               assert_equal(100, REXML::Security.entity_expansion_limit)
               doc = REXML::Document.new(xml)
    -          assert_raise(RuntimeError) do
    +          assert_raise(RuntimeError.new("number of entity expansions exceeded, processing aborted.")) do
                 doc.root.children.first.value
               end
               assert_equal(101, doc.entity_expansion_count)
             end
     
             def test_with_default_entity
    -          xml = <<EOF
    +          xml = <<XML
     <?xml version="1.0" encoding="UTF-8"?>
     <!DOCTYPE member [
       <!ENTITY a "a">
    @@ -112,14 +114,15 @@ def test_with_default_entity
     &a2;
     &lt;
     </member>
    -EOF
    +XML
     
               REXML::Security.entity_expansion_limit = 4
               doc = REXML::Document.new(xml)
               assert_equal("\na\na a\n<\n", doc.root.children.first.value)
    +
               REXML::Security.entity_expansion_limit = 3
               doc = REXML::Document.new(xml)
    -          assert_raise(RuntimeError) do
    +          assert_raise(RuntimeError.new("number of entity expansions exceeded, processing aborted.")) do
                 doc.root.children.first.value
               end
             end
    
  • test/test_pullparser.rb+96 0 modified
    @@ -155,5 +155,101 @@ def test_peek
           end
           assert_equal( 0, names.length )
         end
    +
    +    class EntityExpansionLimitTest < Test::Unit::TestCase
    +      def setup
    +        @default_entity_expansion_limit = REXML::Security.entity_expansion_limit
    +      end
    +
    +      def teardown
    +        REXML::Security.entity_expansion_limit = @default_entity_expansion_limit
    +      end
    +
    +      class GeneralEntityTest < self
    +        def test_have_value
    +          source = <<-XML
    +<?xml version="1.0" encoding="UTF-8"?>
    +<!DOCTYPE member [
    +  <!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
    +  <!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">
    +  <!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;">
    +  <!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;">
    +  <!ENTITY e "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
    +]>
    +<member>
    +&a;
    +</member>
    +          XML
    +
    +          parser = REXML::Parsers::PullParser.new(source)
    +          assert_raise(RuntimeError.new("entity expansion has grown too large")) do
    +            while parser.has_next?
    +              parser.pull
    +            end
    +          end
    +        end
    +
    +        def test_empty_value
    +          source = <<-XML
    +<?xml version="1.0" encoding="UTF-8"?>
    +<!DOCTYPE member [
    +  <!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
    +  <!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">
    +  <!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;">
    +  <!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;">
    +  <!ENTITY e "">
    +]>
    +<member>
    +&a;
    +</member>
    +          XML
    +
    +          parser = REXML::Parsers::PullParser.new(source)
    +          assert_raise(RuntimeError.new("number of entity expansions exceeded, processing aborted.")) do
    +            while parser.has_next?
    +              parser.pull
    +            end
    +          end
    +
    +          REXML::Security.entity_expansion_limit = 100
    +          parser = REXML::Parsers::PullParser.new(source)
    +          assert_raise(RuntimeError.new("number of entity expansions exceeded, processing aborted.")) do
    +            while parser.has_next?
    +              parser.pull
    +            end
    +          end
    +          assert_equal(101, parser.entity_expansion_count)
    +        end
    +
    +        def test_with_default_entity
    +          source = <<-XML
    +<?xml version="1.0" encoding="UTF-8"?>
    +<!DOCTYPE member [
    +  <!ENTITY a "a">
    +  <!ENTITY a2 "&a; &a;">
    +]>
    +<member>
    +&a;
    +&a2;
    +&lt;
    +</member>
    +          XML
    +
    +          REXML::Security.entity_expansion_limit = 4
    +          parser = REXML::Parsers::PullParser.new(source)
    +          while parser.has_next?
    +            parser.pull
    +          end
    +
    +          REXML::Security.entity_expansion_limit = 3
    +          parser = REXML::Parsers::PullParser.new(source)
    +          assert_raise(RuntimeError.new("number of entity expansions exceeded, processing aborted.")) do
    +            while parser.has_next?
    +              parser.pull
    +            end
    +          end
    +        end
    +      end
    +    end
       end
     end
    
  • test/test_sax.rb+86 0 modified
    @@ -99,6 +99,92 @@ def test_sax2
           end
         end
     
    +    class EntityExpansionLimitTest < Test::Unit::TestCase
    +      def setup
    +        @default_entity_expansion_limit = REXML::Security.entity_expansion_limit
    +      end
    +
    +      def teardown
    +        REXML::Security.entity_expansion_limit = @default_entity_expansion_limit
    +      end
    +
    +      class GeneralEntityTest < self
    +        def test_have_value
    +          source = <<-XML
    +<?xml version="1.0" encoding="UTF-8"?>
    +<!DOCTYPE member [
    +  <!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
    +  <!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">
    +  <!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;">
    +  <!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;">
    +  <!ENTITY e "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
    +]>
    +<member>
    +&a;
    +</member>
    +          XML
    +
    +          sax = REXML::Parsers::SAX2Parser.new(source)
    +          assert_raise(RuntimeError.new("entity expansion has grown too large")) do
    +            sax.parse
    +          end
    +        end
    +
    +        def test_empty_value
    +          source = <<-XML
    +<?xml version="1.0" encoding="UTF-8"?>
    +<!DOCTYPE member [
    +  <!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
    +  <!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">
    +  <!ENTITY c "&d;&d;&d;&d;&d;&d;&d;&d;&d;&d;">
    +  <!ENTITY d "&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;">
    +  <!ENTITY e "">
    +]>
    +<member>
    +&a;
    +</member>
    +          XML
    +
    +          sax = REXML::Parsers::SAX2Parser.new(source)
    +          assert_raise(RuntimeError.new("number of entity expansions exceeded, processing aborted.")) do
    +            sax.parse
    +          end
    +
    +          REXML::Security.entity_expansion_limit = 100
    +          sax = REXML::Parsers::SAX2Parser.new(source)
    +          assert_raise(RuntimeError.new("number of entity expansions exceeded, processing aborted.")) do
    +            sax.parse
    +          end
    +          assert_equal(101, sax.entity_expansion_count)
    +        end
    +
    +        def test_with_default_entity
    +          source = <<-XML
    +<?xml version="1.0" encoding="UTF-8"?>
    +<!DOCTYPE member [
    +  <!ENTITY a "a">
    +  <!ENTITY a2 "&a; &a;">
    +]>
    +<member>
    +&a;
    +&a2;
    +&lt;
    +</member>
    +          XML
    +
    +          REXML::Security.entity_expansion_limit = 4
    +          sax = REXML::Parsers::SAX2Parser.new(source)
    +          sax.parse
    +
    +          REXML::Security.entity_expansion_limit = 3
    +          sax = REXML::Parsers::SAX2Parser.new(source)
    +          assert_raise(RuntimeError.new("number of entity expansions exceeded, processing aborted.")) do
    +            sax.parse
    +          end
    +        end
    +      end
    +    end
    +
         # used by test_simple_doctype_listener
         # submitted by Jeff Barczewski
         class SimpleDoctypeListener
    

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.