CVE-2024-35176: Insufficient Information (NVD-CWE-noinfo)
value:  REXML is an XML toolkit for Ruby. The REXML gem before 3.2.6 has a denial of service vulnerability when it parses an XML that has many `<`s in an attribute value. Those who need to parse untrusted XMLs may be impacted to this vulnerability. The REXML gem 3.2.7 or later include the patch to fix this vulnerability. As a workaround, don't parse untrusted XMLs.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-05-16T16:15Z
Repository: https://github.com/ruby/rexml
Language: Ruby
Files changed: Gemfile, lib/rexml/parsers/baseparser.rb, lib/rexml/source.rb, test/test_document.rb
Commit date: 2024-05-16 11:26:51 +0900
Commit message: Read quoted attributes in chunks (#126)


Diff: diff --git a/Gemfile b/Gemfile
index 042ef8a..f78cc86 100644
--- a/Gemfile
+++ b/Gemfile
@@ -8,6 +8,7 @@ gemspec
 group :development do
   gem "benchmark_driver"
   gem "bundler"
   gem "rake"
   gem "test-unit"
+  gem "test-unit-ruby-core"
 end
diff --git a/lib/rexml/parsers/baseparser.rb b/lib/rexml/parsers/baseparser.rb
index 8d62391..d09237c 100644
--- a/lib/rexml/parsers/baseparser.rb
+++ b/lib/rexml/parsers/baseparser.rb
@@ -626,21 +626,21 @@ module REXML
 
             unless @source.match(/\s*=\s*/um, true)
               message = "Missing attribute equal: <#{name}>"
               raise REXML::ParseException.new(message, @source)
             end
-            unless match = @source.match(/(['"])(.*?)\1\s*/um, true)
-              if match = @source.match(/(['"])/, true)
-                message =
-                  "Missing attribute value end quote: <#{name}>: <#{match[1]}>"
-                raise REXML::ParseException.new(message, @source)
-              else
-                message = "Missing attribute value start quote: <#{name}>"
-                raise REXML::ParseException.new(message, @source)
-              end
+            unless match = @source.match(/(['"])/, true)
+              message = "Missing attribute value start quote: <#{name}>"
+              raise REXML::ParseException.new(message, @source)
+            end
+            quote = match[1]
+            value = @source.read_until(quote)
+            unless value.chomp!(quote)
+              message = "Missing attribute value end quote: <#{name}>: <#{quote}>"
+              raise REXML::ParseException.new(message, @source)
             end
-            value = match[2]
+            @source.match(/\s*/um, true)
             if prefix == "xmlns"
               if local_part == "xml"
                 if value != "http://www.w3.org/XML/1998/namespace"
                   msg = "The 'xml' prefix must not be bound to any other namespace "+
                     "(http://www.w3.org/TR/REC-xml-names/#ns-decl)"
diff --git a/lib/rexml/source.rb b/lib/rexml/source.rb
index 7f47c2b..999751b 100644
--- a/lib/rexml/source.rb
+++ b/lib/rexml/source.rb
@@ -63,11 +63,15 @@ module REXML
     def encoding=(enc)
       return unless super
       encoding_updated
     end
 
-    def read
+    def read(term = nil)
+    end
+
+    def read_until(term)
+      @scanner.scan_until(Regexp.union(term)) or @scanner.rest
     end
 
     def ensure_buffer
     end
 
@@ -156,20 +160,35 @@ module REXML
       else
         @force_utf8 = false
       end
     end
 
-    def read
+    def read(term = nil)
       begin
-        @scanner << readline
+        @scanner << readline(term)
         true
       rescue Exception, NameError
         @source = nil
         false
       end
     end
 
+    def read_until(term)
+      pattern = Regexp.union(term)
+      data = []
+      begin
+        until str = @scanner.scan_until(pattern)
+          @scanner << readline(term)
+        end
+      rescue EOFError
+        @scanner.rest
+      else
+        read if @scanner.eos? and !@source.eof?
+        str
+      end
+    end
+
     def ensure_buffer
       read if @scanner.eos? && @source
     end
 
     # Note: When specifying a string for 'pattern', it must not include '>' except in the following formats:
@@ -216,12 +235,12 @@ module REXML
       end
       [pos, lineno, line]
     end
 
     private
-    def readline
-      str = @source.readline(@line_break)
+    def readline(term = nil)
+      str = @source.readline(term || @line_break)
       if @pending_buffer
         if str.nil?
           str = @pending_buffer
         else
           str = @pending_buffer + str
diff --git a/test/test_document.rb b/test/test_document.rb
index 953656f..f96bfd5 100644
--- a/test/test_document.rb
+++ b/test/test_document.rb
@@ -1,10 +1,14 @@
 # -*- coding: utf-8 -*-
 # frozen_string_literal: false
 
+require 'core_assertions'
+
 module REXMLTests
   class TestDocument < Test::Unit::TestCase
+    include Test::Unit::CoreAssertions
+
     def test_version_attributes_to_s
       doc = REXML::Document.new(<<~eoxml)
         <?xml version="1.0" encoding="UTF-8" standalone="no"?>
         <svg  id="svg2"
               xmlns:sodipodi="foo"
@@ -196,10 +200,17 @@ EOX
       assert_equal('no', doc.stand_alone?, bug2539)
       doc = REXML::Document.new('<?xml version="1.0" standalone=  "no" ?>')
       assert_equal('no', doc.stand_alone?, bug2539)
     end
 
+    def test_gt_linear_performance
+      seq = [10000, 50000, 100000, 150000, 200000]
+      assert_linear_performance(seq) do |n|
+        REXML::Document.new('<test testing="' + ">" * n + '"></test>')
+      end
+    end
+
     class WriteTest < Test::Unit::TestCase
       def setup
         @document = REXML::Document.new(<<-EOX)
 <?xml version="1.0" encoding="UTF-8"?>
 <message>Hello world!</message>

Vulnerable code:             unless match = @source.match(/(['"])(.*?)\1\s*/um, true)
              if match = @source.match(/(['"])/, true)
                message =
                  "Missing attribute value end quote: <#{name}>: <#{match[1]}>"
                raise REXML::ParseException.new(message, @source)
              else
                message = "Missing attribute value start quote: <#{name}>"
                raise REXML::ParseException.new(message, @source)
              end
            value = match[2]
    def read
    def read
        @scanner << readline
    def readline
      str = @source.readline(@line_break)
Fixed code:   gem "test-unit-ruby-core"
            unless match = @source.match(/(['"])/, true)
              message = "Missing attribute value start quote: <#{name}>"
              raise REXML::ParseException.new(message, @source)
            end
            quote = match[1]
            value = @source.read_until(quote)
            unless value.chomp!(quote)
              message = "Missing attribute value end quote: <#{name}>: <#{quote}>"
              raise REXML::ParseException.new(message, @source)
            @source.match(/\s*/um, true)
    def read(term = nil)
    end

    def read_until(term)
      @scanner.scan_until(Regexp.union(term)) or @scanner.rest
    def read(term = nil)
        @scanner << readline(term)
    def read_until(term)
      pattern = Regexp.union(term)
      data = []
      begin
        until str = @scanner.scan_until(pattern)
          @scanner << readline(term)
        end
      rescue EOFError
        @scanner.rest
      else
        read if @scanner.eos? and !@source.eof?
        str
      end
    end

    def readline(term = nil)
      str = @source.readline(term || @line_break)
require 'core_assertions'

    include Test::Unit::CoreAssertions

    def test_gt_linear_performance
      seq = [10000, 50000, 100000, 150000, 200000]
      assert_linear_performance(seq) do |n|
        REXML::Document.new('<test testing="' + ">" * n + '"></test>')
      end
    end