Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
330 changes: 330 additions & 0 deletions SPECS/ruby/CVE-2025-58767.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,330 @@
From 5859bdeac792687eaf93d8e8f0b7e3c1e2ed5c23 Mon Sep 17 00:00:00 2001
From: NAITOH Jun <[email protected]>
Date: Sat, 23 Aug 2025 08:11:58 +0900
Subject: [PATCH] Added XML declaration check & `Source#skip_spaces` method
(#282)

## Why?

### Added XML declaration check

- The version attribute is required in XML declaration.
- Only version attribute, encoding attribute, and standalone attribute
are allowed in XML declaration.
- XML declaration is only allowed once.

See: https://www.w3.org/TR/xml/#NT-XMLDecl

### Added `Source#skip_spaces` method

In the case of `@source.match?(/\s+/um, true)`, if there are no spaces
at the beginning, I want to stop reading immediately.
However, it continues to read the buffer until it finds a match, but it
never finds a match.
As a result, it continues reading until the end of the file.

In the case of large XML files, drop_parsed_content occur frequently
until the buffer is cleared, which may affect performance.

Upstream Patch Reference: https://github.com/ruby/rexml/commit/5859bdeac792687eaf93d8e8f0b7e3c1e2ed5c23.patch
---
.../lib/rexml/parsers/baseparser.rb | 161 ++++++++++++------
.bundle/gems/rexml-3.3.9/lib/rexml/source.rb | 11 +-
2 files changed, 120 insertions(+), 52 deletions(-)

diff --git a/.bundle/gems/rexml-3.3.9/lib/rexml/parsers/baseparser.rb b/.bundle/gems/rexml-3.3.9/lib/rexml/parsers/baseparser.rb
index b4547ba..012ccc7 100644
--- a/.bundle/gems/rexml-3.3.9/lib/rexml/parsers/baseparser.rb
+++ b/.bundle/gems/rexml-3.3.9/lib/rexml/parsers/baseparser.rb
@@ -144,6 +144,7 @@ module REXML
PEREFERENCE_PATTERN = /#{PEREFERENCE}/um
TAG_PATTERN = /((?>#{QNAME_STR}))\s*/um
CLOSE_PATTERN = /(#{QNAME_STR})\s*>/um
+ EQUAL_PATTERN = /\s*=\s*/um
ATTLISTDECL_END = /\s+#{NAME}(?:#{ATTDEF})*\s*>/um
NAME_PATTERN = /#{NAME}/um
GEDECL_PATTERN = "\\s+#{NAME}\\s+#{ENTITYDEF}\\s*>"
@@ -168,6 +169,7 @@ module REXML
@entity_expansion_limit = Security.entity_expansion_limit
@entity_expansion_text_limit = Security.entity_expansion_text_limit
@source.ensure_buffer
+ @version = nil
end

def add_listener( listener )
@@ -283,7 +285,7 @@ module REXML
return [ :comment, md[1] ]
elsif @source.match("DOCTYPE", true)
base_error_message = "Malformed DOCTYPE"
- unless @source.match(/\s+/um, true)
+ unless @source.skip_spaces
if @source.match(">")
message = "#{base_error_message}: name is missing"
else
@@ -293,7 +295,8 @@ module REXML
raise REXML::ParseException.new(message, @source)
end
name = parse_name(base_error_message)
- if @source.match(/\s*\[/um, true)
+ @source.skip_spaces
+ if @source.match(/\s*\[/um, true)
id = [nil, nil, nil]
@document_status = :in_doctype
elsif @source.match(/\s*>/um, true)
@@ -308,6 +311,7 @@ module REXML
# For backward compatibility
id[1], id[2] = id[2], nil
end
+ @source.skip_spaces
if @source.match(/\s*\[/um, true)
@document_status = :in_doctype
elsif @source.match(/\s*>/um, true)
@@ -320,7 +324,7 @@ module REXML
end
args = [:start_doctype, name, *id]
if @document_status == :after_doctype
- @source.match(/\s*/um, true)
+ @source.skip_spaces
@stack << [ :end_doctype ]
end
return args
@@ -331,7 +335,7 @@ module REXML
end
end
if @document_status == :in_doctype
- @source.match(/\s*/um, true) # skip spaces
+ @source.skip_spaces
start_position = @source.position
if @source.match("<!", true)
if @source.match("ELEMENT", true)
@@ -392,7 +396,7 @@ module REXML
return [ :attlistdecl, element, pairs, contents ]
elsif @source.match("NOTATION", true)
base_error_message = "Malformed notation declaration"
- unless @source.match(/\s+/um, true)
+ unless @source.skip_spaces
if @source.match(">")
message = "#{base_error_message}: name is missing"
else
@@ -405,7 +409,8 @@ module REXML
id = parse_id(base_error_message,
accept_external_id: true,
accept_public_id: true)
- unless @source.match(/\s*>/um, true)
+ @source.skip_spaces
+ unless @source.match(/\s*>/um, true)
message = "#{base_error_message}: garbage before end >"
raise REXML::ParseException.new(message, @source)
end
@@ -428,7 +433,7 @@ module REXML
end
end
if @document_status == :after_doctype
- @source.match(/\s*/um, true)
+ @source.skip_spaces
end
begin
start_position = @source.position
@@ -648,6 +653,10 @@ module REXML
true
end

+ def normalize_xml_declaration_encoding(xml_declaration_encoding)
+ /\AUTF-16(?:BE|LE)\z/i.match?(xml_declaration_encoding) ? "UTF-16" : nil
+ end
+
def parse_name(base_error_message)
md = @source.match(Private::NAME_PATTERN, true)
unless md
@@ -729,37 +738,103 @@ module REXML

def process_instruction
name = parse_name("Malformed XML: Invalid processing instruction node")
- if @source.match(/\s+/um, true)
- match_data = @source.match(/(.*?)\?>/um, true)
- unless match_data
- raise ParseException.new("Malformed XML: Unclosed processing instruction", @source)
- end
- content = match_data[1]
- else
- content = nil
- unless @source.match("?>", true)
- raise ParseException.new("Malformed XML: Unclosed processing instruction", @source)
+ if name == "xml"
+ xml_declaration
+ else # PITarget
+ if @source.skip_spaces # e.g. <?name content?>
+ start_position = @source.position
+ content = @source.read_until("?>")
+ unless content.chomp!("?>")
+ @source.position = start_position
+ raise ParseException.new("Malformed XML: Unclosed processing instruction: <#{name}>", @source)
+ end
+ else # e.g. <?name?>
+ content = nil
+ unless @source.match?("?>", true)
+ raise ParseException.new("Malformed XML: Unclosed processing instruction: <#{name}>", @source)
+ end
end
+ [:processing_instruction, name, content]
end
- if name == "xml"
- if @document_status
- raise ParseException.new("Malformed XML: XML declaration is not at the start", @source)
+ end
+
+ def xml_declaration
+ unless @version.nil?
+ raise ParseException.new("Malformed XML: XML declaration is duplicated", @source)
+ end
+ if @document_status
+ raise ParseException.new("Malformed XML: XML declaration is not at the start", @source)
+ end
+ unless @source.skip_spaces
+ raise ParseException.new("Malformed XML: XML declaration misses spaces before version", @source)
+ end
+ unless @source.match?("version", true)
+ raise ParseException.new("Malformed XML: XML declaration misses version", @source)
+ end
+ @version = parse_attribute_value_with_equal("xml")
+ unless @source.skip_spaces
+ unless @source.match("?>", true)
+ raise ParseException.new("Malformed XML: Unclosed XML declaration", @source)
end
- version = VERSION.match(content)
- version = version[1] unless version.nil?
- encoding = ENCODING.match(content)
- encoding = encoding[1] unless encoding.nil?
- if need_source_encoding_update?(encoding)
- @source.encoding = encoding
+ encoding = normalize_xml_declaration_encoding(@source.encoding)
+ return [ :xmldecl, @version, encoding, nil ] # e.g. <?xml version="1.0"?>
+ end
+
+ if @source.match?("encoding", true)
+ encoding = parse_attribute_value_with_equal("xml")
+ unless @source.skip_spaces
+ unless @source.match?("?>", true)
+ raise ParseException.new("Malformed XML: Unclosed XML declaration", @source)
+ end
+ if need_source_encoding_update?(encoding)
+ @source.encoding = encoding
+ end
+ encoding ||= normalize_xml_declaration_encoding(@source.encoding)
+ return [ :xmldecl, @version, encoding, nil ] # e.g. <?xml version="1.1" encoding="UTF-8"?>
end
- if encoding.nil? and /\AUTF-16(?:BE|LE)\z/i =~ @source.encoding
- encoding = "UTF-16"
+ end
+
+ if @source.match?("standalone", true)
+ standalone = parse_attribute_value_with_equal("xml")
+ case standalone
+ when "yes", "no"
+ else
+ raise ParseException.new("Malformed XML: XML declaration standalone is not yes or no : <#{standalone}>", @source)
end
- standalone = STANDALONE.match(content)
- standalone = standalone[1] unless standalone.nil?
- return [ :xmldecl, version, encoding, standalone ]
end
- [:processing_instruction, name, content]
+ @source.skip_spaces
+ unless @source.match?("?>", true)
+ raise ParseException.new("Malformed XML: Unclosed XML declaration", @source)
+ end
+
+ if need_source_encoding_update?(encoding)
+ @source.encoding = encoding
+ end
+ encoding ||= normalize_xml_declaration_encoding(@source.encoding)
+
+ # e.g. <?xml version="1.0" ?>
+ # <?xml version="1.1" encoding="UTF-8" ?>
+ # <?xml version="1.1" standalone="yes"?>
+ # <?xml version="1.1" encoding="UTF-8" standalone="yes" ?>
+ [ :xmldecl, @version, encoding, standalone ]
+ end
+ def parse_attribute_value_with_equal(name)
+ unless @source.match?(Private::EQUAL_PATTERN, true)
+ message = "Missing attribute equal: <#{name}>"
+ raise REXML::ParseException.new(message, @source)
+ end
+ unless quote = scan_quote
+ message = "Missing attribute value start quote: <#{name}>"
+ raise REXML::ParseException.new(message, @source)
+ end
+ start_position = @source.position
+ value = @source.read_until(quote)
+ unless value.chomp!(quote)
+ @source.position = start_position
+ message = "Missing attribute value end quote: <#{name}>: <#{quote}>"
+ raise REXML::ParseException.new(message, @source)
+ end
+ value
end

def parse_attributes(prefixes)
@@ -776,25 +851,9 @@ module REXML
name = match[1]
prefix = match[2]
local_part = match[3]
-
- unless @source.match(/\s*=\s*/um, true)
- message = "Missing attribute equal: <#{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]
- start_position = @source.position
- value = @source.read_until(quote)
- unless value.chomp!(quote)
- @source.position = start_position
- message = "Missing attribute value end quote: <#{name}>: <#{quote}>"
- raise REXML::ParseException.new(message, @source)
- end
- @source.match(/\s*/um, true)
- if prefix == "xmlns"
+ value = parse_attribute_value_with_equal(name)
+ @source.skip_spaces
+ if prefix == "xmlns"
if local_part == "xml"
if value != Private::XML_PREFIXED_NAMESPACE
msg = "The 'xml' prefix must not be bound to any other namespace "+
diff --git a/.bundle/gems/rexml-3.3.9/lib/rexml/source.rb b/.bundle/gems/rexml-3.3.9/lib/rexml/source.rb
index dc0b532..f896668 100644
--- a/.bundle/gems/rexml-3.3.9/lib/rexml/source.rb
+++ b/.bundle/gems/rexml-3.3.9/lib/rexml/source.rb
@@ -55,9 +55,10 @@ module REXML
attr_reader :encoding

module Private
+ SPACES_PATTERN = /\s+/um
SCANNER_RESET_SIZE = 100000
PRE_DEFINED_TERM_PATTERNS = {}
- pre_defined_terms = ["'", '"', "<"]
+ pre_defined_terms = ["'", '"', "<", "]]>", "?>"]
pre_defined_terms.each do |term|
PRE_DEFINED_TERM_PATTERNS[term] = /#{Regexp.escape(term)}/
end
@@ -126,6 +127,14 @@ module REXML
end
end

+ def skip_spaces
+ @scanner.skip(Private::SPACES_PATTERN) ? true : false
+ end
+
+ def skip_spaces
+ @scanner.skip(Private::SPACES_PATTERN) ? true : false
+ end
+
def position
@scanner.pos
end
--
2.45.4

6 changes: 5 additions & 1 deletion SPECS/ruby/ruby.spec
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ Name: ruby
# provides should be versioned according to the ruby version.
# More info: https://stdgems.org/
Version: 3.1.7
Release: 3%{?dist}
Release: 4%{?dist}
License: (Ruby OR BSD) AND Public Domain AND MIT AND CC0 AND zlib AND UCD
Vendor: Microsoft Corporation
Distribution: Mariner
Expand All @@ -101,6 +101,7 @@ Source7: macros.rubygems
Patch0: CVE-2023-36617.patch
Patch1: CVE-2025-6442.patch
Patch2: CVE-2025-24294.patch
Patch3: CVE-2025-58767.patch
BuildRequires: openssl-devel
BuildRequires: readline
BuildRequires: readline-devel
Expand Down Expand Up @@ -403,6 +404,9 @@ sudo -u test make test TESTS="-v"
%{_rpmconfigdir}/rubygems.con

%changelog
* Fri Oct 03 2025 Ratiranjan Behera <[email protected]> - 3.1.7-4
- Patch CVE-2025-58767

* Tue Jul 15 2025 BinduSri Adabala <[email protected]> - 3.1.7-3
- Patch CVE-2025-24294

Expand Down
Loading