Description: CVE-2013-1664 & CVE-2013-1665: Add a safe_minidom_parse_string function.
 Jonathan Murray from NCC Group, Joshua Harlow from Yahoo! and Stuart Stent
 independently reported a vulnerability in the parsing of XML requests in
 Keystone, Nova and Cinder. By using entities in XML requests, an
 unauthenticated attacker may consume excessive resources on the Keystone, Nova
 or Cinder API servers, resulting in a denial of service and potentially a
 crash. Authenticated attackers may also leverage XML entities to read the
 content of a local file on the Keystone API server. This only affects servers
 with XML support enabled.
 .
 Adds a new utils.safe_minidom_parse_string function and updates external API
 facing Nova modules to use it. This ensures we have safe defaults on our
 incoming API XML parsing.
 .
 Internally safe_minidom_parse_string uses a ProtectedExpatParser class to
 disable DTDs and entities from being parsed when using minidom.
Author: Dan Prince <dprince@redhat.com>
Origin: upstream
Date: Sat, 2 Feb 2013 14:32:12 -0500
Bug-Debian: http://bug.debian.org/700949
Bug-Ubuntu: https://bugs.launchpad.net/keystone/+bug/1100282

--- nova-2012.1.1.orig/nova/utils.py
+++ nova-2012.1.1/nova/utils.py
@@ -41,6 +41,10 @@ import time
 import types
 import uuid
 import warnings
+from xml.dom import minidom
+from xml.parsers import expat
+from xml import sax
+from xml.sax import expatreader
 from xml.sax import saxutils
 
 from eventlet import corolocal
@@ -722,6 +726,46 @@ class LoopingCall(object):
         return self.done.wait()
 
 
+class ProtectedExpatParser(expatreader.ExpatParser):
+    """An expat parser which disables DTD's and entities by default."""
+
+    def __init__(self, forbid_dtd=True, forbid_entities=True,
+                 *args, **kwargs):
+        # Python 2.x old style class
+        expatreader.ExpatParser.__init__(self, *args, **kwargs)
+        self.forbid_dtd = forbid_dtd
+        self.forbid_entities = forbid_entities
+
+    def start_doctype_decl(self, name, sysid, pubid, has_internal_subset):
+        raise ValueError("Inline DTD forbidden")
+
+    def entity_decl(self, entityName, is_parameter_entity, value, base,
+                    systemId, publicId, notationName):
+        raise ValueError("<!ENTITY> forbidden")
+
+    def unparsed_entity_decl(self, name, base, sysid, pubid, notation_name):
+        # expat 1.2
+        raise ValueError("<!ENTITY> forbidden")
+
+    def reset(self):
+        expatreader.ExpatParser.reset(self)
+        if self.forbid_dtd:
+            self._parser.StartDoctypeDeclHandler = self.start_doctype_decl
+        if self.forbid_entities:
+            self._parser.EntityDeclHandler = self.entity_decl
+            self._parser.UnparsedEntityDeclHandler = self.unparsed_entity_decl
+
+
+def safe_minidom_parse_string(xml_string):
+    """Parse an XML string using minidom safely.
+
+    """
+    try:
+        return minidom.parseString(xml_string, parser=ProtectedExpatParser())
+    except sax.SAXParseException as se:
+        raise expat.ExpatError()
+
+
 def xhtml_escape(value):
     """Escapes a string so it is valid within XML or XHTML.
 
--- nova-2012.1.1.orig/nova/tests/test_utils.py
+++ nova-2012.1.1/nova/tests/test_utils.py
@@ -718,6 +718,41 @@ class DeprecationTest(test.TestCase):
         result = utils.service_is_up(service)
         self.assertFalse(result)
 
+
+    def test_safe_parse_xml(self):
+
+        normal_body = ("""
+                 <?xml version="1.0" ?><foo>
+                    <bar>
+                        <v1>hey</v1>
+                        <v2>there</v2>
+                    </bar>
+                </foo>""").strip()
+
+        def killer_body():
+            return (("""<!DOCTYPE x [
+                    <!ENTITY a "%(a)s">
+                    <!ENTITY b "%(b)s">
+                    <!ENTITY c "%(c)s">]>
+                <foo>
+                    <bar>
+                        <v1>%(d)s</v1>
+                    </bar>
+                </foo>""") % {
+                'a': 'A' * 10,
+                'b': '&a;' * 10,
+                'c': '&b;' * 10,
+                'd': '&c;' * 9999,
+            }).strip()
+
+        dom = utils.safe_minidom_parse_string(normal_body)
+        self.assertEqual(normal_body, str(dom.toxml()))
+
+        self.assertRaises(ValueError,
+                          utils.safe_minidom_parse_string,
+                          killer_body())
+
+
     def test_xhtml_escape(self):
         self.assertEqual('&quot;foo&quot;', utils.xhtml_escape('"foo"'))
         self.assertEqual('&apos;foo&apos;', utils.xhtml_escape("'foo'"))
--- nova-2012.1.1.orig/nova/api/openstack/wsgi.py
+++ nova-2012.1.1/nova/api/openstack/wsgi.py
@@ -153,7 +153,7 @@ class XMLDeserializer(TextDeserializer):
         plurals = set(self.metadata.get('plurals', {}))
 
         try:
-            node = minidom.parseString(datastring).childNodes[0]
+            node = utils.safe_minidom_parse_string(datastring).childNodes[0]
             return {node.nodeName: self._from_xml_node(node, plurals)}
         except expat.ExpatError:
             msg = _("cannot understand XML")
@@ -195,11 +195,11 @@ class XMLDeserializer(TextDeserializer):
 
     def extract_text(self, node):
         """Get the text field contained by the given node"""
-        if len(node.childNodes) == 1:
-            child = node.childNodes[0]
+        ret_val = ""
+        for child in node.childNodes:
             if child.nodeType == child.TEXT_NODE:
-                return child.nodeValue
-        return ""
+                ret_val += child.nodeValue
+        return ret_val
 
     def find_attribute_or_element(self, parent, name):
         """Get an attribute value; fallback to an element if not found"""
@@ -550,7 +550,7 @@ def action_peek_json(body):
 def action_peek_xml(body):
     """Determine action to invoke."""
 
-    dom = minidom.parseString(body)
+    dom = utils.safe_minidom_parse_string(body)
     action_node = dom.childNodes[0]
 
     return action_node.tagName
--- nova-2012.1.1.orig/nova/api/openstack/common.py
+++ nova-2012.1.1/nova/api/openstack/common.py
@@ -21,7 +21,6 @@ import re
 import urlparse
 
 import webob
-from xml.dom import minidom
 
 from nova.api.openstack import wsgi
 from nova.api.openstack import xmlutil
@@ -31,6 +30,7 @@ from nova.compute import utils as comput
 from nova import flags
 from nova import log as logging
 from nova import quota
+from nova import utils
 
 
 LOG = logging.getLogger(__name__)
@@ -334,7 +334,7 @@ def raise_http_conflict_for_instance_inv
 
 class MetadataDeserializer(wsgi.MetadataXMLDeserializer):
     def deserialize(self, text):
-        dom = minidom.parseString(text)
+        dom = utils.safe_minidom_parse_string(text)
         metadata_node = self.find_first_child_named(dom, "metadata")
         metadata = self.extract_metadata(metadata_node)
         return {'body': {'metadata': metadata}}
@@ -342,7 +342,7 @@ class MetadataDeserializer(wsgi.Metadata
 
 class MetaItemDeserializer(wsgi.MetadataXMLDeserializer):
     def deserialize(self, text):
-        dom = minidom.parseString(text)
+        dom = utils.safe_minidom_parse_string(text)
         metadata_item = self.extract_metadata(dom)
         return {'body': {'meta': metadata_item}}
 
@@ -360,7 +360,7 @@ class MetadataXMLDeserializer(wsgi.XMLDe
         return metadata
 
     def _extract_metadata_container(self, datastring):
-        dom = minidom.parseString(datastring)
+        dom = utils.safe_minidom_parse_string(datastring)
         metadata_node = self.find_first_child_named(dom, "metadata")
         metadata = self.extract_metadata(metadata_node)
         return {'body': {'metadata': metadata}}
@@ -372,7 +372,7 @@ class MetadataXMLDeserializer(wsgi.XMLDe
         return self._extract_metadata_container(datastring)
 
     def update(self, datastring):
-        dom = minidom.parseString(datastring)
+        dom = utils.safe_minidom_parse_string(datastring)
         metadata_item = self.extract_metadata(dom)
         return {'body': {'meta': metadata_item}}
 
--- nova-2012.1.1.orig/nova/api/openstack/compute/servers.py
+++ nova-2012.1.1/nova/api/openstack/compute/servers.py
@@ -17,7 +17,6 @@
 import base64
 import os
 import socket
-from xml.dom import minidom
 
 from webob import exc
 import webob
@@ -240,7 +239,7 @@ class ActionDeserializer(CommonDeseriali
     """
 
     def default(self, string):
-        dom = minidom.parseString(string)
+        dom = utils.safe_minidom_parse_string(string)
         action_node = dom.childNodes[0]
         action_name = action_node.tagName
 
@@ -336,7 +335,7 @@ class CreateDeserializer(CommonDeseriali
 
     def default(self, string):
         """Deserialize an xml-formatted server create request"""
-        dom = minidom.parseString(string)
+        dom = utils.safe_minidom_parse_string(string)
         server = self._extract_server(dom)
         return {'body': {'server': server}}
 
--- nova-2012.1.1.orig/nova/api/openstack/compute/contrib/security_groups.py
+++ nova-2012.1.1/nova/api/openstack/compute/contrib/security_groups.py
@@ -17,7 +17,6 @@
 """The security groups extension."""
 
 import urllib
-from xml.dom import minidom
 
 from webob import exc
 import webob
@@ -112,7 +111,7 @@ class SecurityGroupXMLDeserializer(wsgi.
     """
     def default(self, string):
         """Deserialize an xml-formatted security group create request"""
-        dom = minidom.parseString(string)
+        dom = utils.safe_minidom_parse_string(string)
         security_group = {}
         sg_node = self.find_first_child_named(dom,
                                                'security_group')
@@ -133,7 +132,7 @@ class SecurityGroupRulesXMLDeserializer(
 
     def default(self, string):
         """Deserialize an xml-formatted security group create request"""
-        dom = minidom.parseString(string)
+        dom = utils.safe_minidom_parse_string(string)
         security_group_rule = self._extract_security_group_rule(dom)
         return {'body': {'security_group_rule': security_group_rule}}
 
--- nova-2012.1.1.orig/nova/api/openstack/compute/contrib/hosts.py
+++ nova-2012.1.1/nova/api/openstack/compute/contrib/hosts.py
@@ -16,7 +16,6 @@
 """The hosts admin extension."""
 
 import webob.exc
-from xml.dom import minidom
 from xml.parsers import expat
 
 from nova.api.openstack import wsgi
@@ -28,6 +27,7 @@ from nova import exception
 from nova import flags
 from nova import log as logging
 from nova.scheduler import api as scheduler_api
+from nova import utils
 
 
 LOG = logging.getLogger(__name__)
@@ -81,7 +81,7 @@ class HostShowTemplate(xmlutil.TemplateB
 class HostDeserializer(wsgi.XMLDeserializer):
     def default(self, string):
         try:
-            node = minidom.parseString(string)
+            node = utils.safe_minidom_parse_string(string)
         except expat.ExpatError:
             msg = _("cannot understand XML")
             raise exception.MalformedRequestBody(reason=msg)
