From eb4f1d6a02e9557b97cdbed76157dc5a625cdb82 Mon Sep 17 00:00:00 2001
From: Aaron Patterson <aaron.patterson () gmail com>
Date: Tue, 9 Jun 2015 11:24:25 -0700
Subject: [PATCH] enforce a depth limit on XML documents

XML documents that are too deep can cause an stack overflow, which in
turn will cause a potential DoS attack.

CVE-2015-3227
---
 activesupport/lib/active_support/xml_mini.rb       |  3 +++
 activesupport/lib/active_support/xml_mini/jdom.rb  | 11 ++++++-----
 activesupport/lib/active_support/xml_mini/rexml.rb | 11 ++++++-----
 3 files changed, 15 insertions(+), 10 deletions(-)

diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb
index 009ee4d..df7b081 100644
--- a/activesupport/lib/active_support/xml_mini.rb
+++ b/activesupport/lib/active_support/xml_mini.rb
@@ -78,6 +78,9 @@ module ActiveSupport
       )
     end
 
+    attr_accessor :depth
+    self.depth = 100
+
     delegate :parse, :to => :backend
 
     def backend
diff --git a/activesupport/lib/active_support/xml_mini/jdom.rb b/activesupport/lib/active_support/xml_mini/jdom.rb
index 27c64c4..cdc5490 100644
--- a/activesupport/lib/active_support/xml_mini/jdom.rb
+++ b/activesupport/lib/active_support/xml_mini/jdom.rb
@@ -46,7 +46,7 @@ module ActiveSupport
         xml_string_reader = StringReader.new(data)
         xml_input_source = InputSource.new(xml_string_reader)
         doc = @dbf.new_document_builder.parse(xml_input_source)
-        merge_element!({CONTENT_KEY => ''}, doc.document_element)
+        merge_element!({CONTENT_KEY => ''}, doc.document_element, XmlMini.depth)
       end
     end
 
@@ -58,9 +58,10 @@ module ActiveSupport
     #   Hash to merge the converted element into.
     # element::
     #   XML element to merge into hash
-    def merge_element!(hash, element)
+    def merge_element!(hash, element, depth)
+      raise 'Document too deep!' if depth == 0
       delete_empty(hash)
-      merge!(hash, element.tag_name, collapse(element))
+      merge!(hash, element.tag_name, collapse(element, depth))
     end
 
     def delete_empty(hash)
@@ -71,14 +72,14 @@ module ActiveSupport
     #
     # element::
     #   The document element to be collapsed.
-    def collapse(element)
+    def collapse(element, depth)
       hash = get_attributes(element)
 
       child_nodes = element.child_nodes
       if child_nodes.length > 0
         (0...child_nodes.length).each do |i|
           child = child_nodes.item(i)
-          merge_element!(hash, child) unless child.node_type == Node.TEXT_NODE
+          merge_element!(hash, child, depth - 1) unless child.node_type == Node.TEXT_NODE
         end
         merge_texts!(hash, element) unless empty_content?(element)
         hash
diff --git a/activesupport/lib/active_support/xml_mini/rexml.rb b/activesupport/lib/active_support/xml_mini/rexml.rb
index 5c7c78b..924ed72 100644
--- a/activesupport/lib/active_support/xml_mini/rexml.rb
+++ b/activesupport/lib/active_support/xml_mini/rexml.rb
@@ -29,7 +29,7 @@ module ActiveSupport
         doc = REXML::Document.new(data)
 
         if doc.root
-          merge_element!({}, doc.root)
+          merge_element!({}, doc.root, XmlMini.depth)
         else
           raise REXML::ParseException,
             "The document #{doc.to_s.inspect} does not have a valid root"
@@ -44,19 +44,20 @@ module ActiveSupport
       #   Hash to merge the converted element into.
       # element::
       #   XML element to merge into hash
-      def merge_element!(hash, element)
-        merge!(hash, element.name, collapse(element))
+      def merge_element!(hash, element, depth)
+        raise REXML::ParseException, "The document is too deep" if depth == 0
+        merge!(hash, element.name, collapse(element, depth))
       end
 
       # Actually converts an XML document element into a data structure.
       #
       # element::
       #   The document element to be collapsed.
-      def collapse(element)
+      def collapse(element, depth)
         hash = get_attributes(element)
 
         if element.has_elements?
-          element.each_element {|child| merge_element!(hash, child) }
+          element.each_element {|child| merge_element!(hash, child, depth - 1) }
           merge_texts!(hash, element) unless empty_content?(element)
           hash
         else
-- 
2.2.1

