CVE-2016-10127 (published 2017-03-03T15:59Z)

PySAML2 allows remote attackers to conduct XML external entity (XXE) attacks via a crafted SAML XML request or response.

Severity: MEDIUM
CVSS v2 base score: 6.8 (AV:N/AC:M/Au:N/C:P/I:P/A:P)
CVSS v3 base score: 9.0, CRITICAL (CVSS:3.0/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-611
cwe_name: Improper Restriction of XML External Entity Reference
description: The product processes an XML document that can contain XML entities with URIs that resolve to documents outside of the intended sphere of control, causing the product to embed incorrect documents into its output.
extended_description: 
            XML documents optionally contain a Document Type Definition (DTD), which, among other features, enables the definition of XML entities. It is possible to define an entity by providing a substitution string in the form of a URI. The XML parser can access the contents of this URI and embed these contents back into the XML document for further processing.
            By submitting an XML file that defines an external entity with a file:// URI, an attacker can cause the processing application to read the contents of a local file. For example, a URI such as "file:///c:/winnt/win.ini" designates (in Windows) the file C:\Winnt\win.ini, or file:///etc/passwd designates the password file in Unix-based systems. Using URIs with other schemes such as http://, the attacker can force the application to make outgoing requests to servers that the attacker cannot reach directly, which can be used to bypass firewall restrictions or hide the source of attacks such as port scanning.
            Once the content of the URI is read, it is fed back into the application that is processing the XML. This application may echo back the data (e.g. in an error message), thereby exposing the file contents.
         
         

Fix information:
repo_url: https://github.com/rohe/pysaml2
commit_details: repo_url: https://github.com/rohe/pysaml2
author: Florian Rüchel
author_date: 2016-10-31 11:56:48+11:00
msg: Fix XXE in XML parsing (related to #366)

This fixes XXE issues on anything where pysaml2 parses XML directly as part of
issue #366. It doesn't address the xmlsec issues discussed on that ticket as
they are out of reach of a direct fix and need the underlying library to fix
this issue.
repository: repo_url: https://github.com/rohe/pysaml2
repo_name: IdentityPython/pysaml2
description: Python implementation of SAML2
repo_language: Python
owner: rohe
file_changes: filename: setup.py
change_type: ModificationType.MODIFY
diff: @@ -18,6 +18,7 @@ install_requires = [
     'pytz',
     'pyOpenSSL',
     'python-dateutil',
+    'defusedxml',
     'six'
 ]
 

programming_language: Python
filename: __init__.py
change_type: ModificationType.MODIFY
diff: @@ -36,6 +36,7 @@ except ImportError:
         import cElementTree as ElementTree
     except ImportError:
         from elementtree import ElementTree
+import defusedxml.ElementTree
 
 root_logger = logging.getLogger(__name__)
 root_logger.level = logging.NOTSET
@@ -87,7 +88,7 @@ def create_class_from_xml_string(target_class, xml_string):
     """
     if not isinstance(xml_string, six.binary_type):
         xml_string = xml_string.encode('utf-8')
-    tree = ElementTree.fromstring(xml_string)
+    tree = defusedxml.ElementTree.fromstring(xml_string)
     return create_class_from_element_tree(target_class, tree)
 
 
@@ -269,7 +270,7 @@ class ExtensionElement(object):
 
 
 def extension_element_from_string(xml_string):
-    element_tree = ElementTree.fromstring(xml_string)
+    element_tree = defusedxml.ElementTree.fromstring(xml_string)
     return _extension_element_from_element_tree(element_tree)
 
 

programming_language: Python
filename: pack.py
change_type: ModificationType.MODIFY
diff: @@ -37,6 +37,7 @@ except ImportError:
         import cElementTree as ElementTree
     except ImportError:
         from elementtree import ElementTree
+import defusedxml.ElementTree
 
 NAMESPACE = "http://schemas.xmlsoap.org/soap/envelope/"
 FORM_SPEC = """<form method="post" action="%s">
@@ -235,7 +236,7 @@ def parse_soap_enveloped_saml(text, body_class, header_class=None):
     :param text: The SOAP object as XML
     :return: header parts and body as saml.samlbase instances
     """
-    envelope = ElementTree.fromstring(text)
+    envelope = defusedxml.ElementTree.fromstring(text)
     assert envelope.tag == '{%s}Envelope' % NAMESPACE
 
     # print(len(envelope))

programming_language: Python
filename: soap.py
change_type: ModificationType.MODIFY
diff: @@ -19,6 +19,7 @@ except ImportError:
     except ImportError:
         #noinspection PyUnresolvedReferences
         from elementtree import ElementTree
+import defusedxml.ElementTree
 
 
 logger = logging.getLogger(__name__)
@@ -133,7 +134,7 @@ def parse_soap_enveloped_saml_thingy(text, expected_tags):
     :param expected_tags: What the tag of the SAML thingy is expected to be.
     :return: SAML thingy as a string
     """
-    envelope = ElementTree.fromstring(text)
+    envelope = defusedxml.ElementTree.fromstring(text)
 
     # Make sure it's a SOAP message
     assert envelope.tag == '{%s}Envelope' % soapenv.NAMESPACE
@@ -183,7 +184,7 @@ def class_instances_from_soap_enveloped_saml_thingies(text, modules):
     :return: The body and headers as class instances
     """
     try:
-        envelope = ElementTree.fromstring(text)
+        envelope = defusedxml.ElementTree.fromstring(text)
     except Exception as exc:
         raise XmlParseError("%s" % exc)
 
@@ -209,7 +210,7 @@ def open_soap_envelope(text):
     :return: dictionary with two keys "body"/"header"
     """
     try:
-        envelope = ElementTree.fromstring(text)
+        envelope = defusedxml.ElementTree.fromstring(text)
     except Exception as exc:
         raise XmlParseError("%s" % exc)
 

programming_language: Python
filename: test_03_saml2.py
change_type: ModificationType.MODIFY
diff: @@ -17,6 +17,7 @@ except ImportError:
         import cElementTree as ElementTree
     except ImportError:
         from elementtree import ElementTree
+from defusedxml.common import EntitiesForbidden
 
 ITEMS = {
     NameID: ["""<?xml version="1.0" encoding="utf-8"?>
@@ -166,6 +167,19 @@ def test_create_class_from_xml_string_wrong_class_spec():
     assert kl == None
 
 
+def test_create_class_from_xml_string_xxe():
+    xml = """<?xml version="1.0"?>
+    <!DOCTYPE lolz [
+    <!ENTITY lol "lol">
+    <!ELEMENT lolz (#PCDATA)>
+    <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
+    ]>
+    <lolz>&lol1;</lolz>
+    """
+    with raises(EntitiesForbidden) as err:
+        create_class_from_xml_string(NameID, xml)
+
+
 def test_ee_1():
     ee = saml2.extension_element_from_string(
         """<?xml version='1.0' encoding='UTF-8'?><foo>bar</foo>""")
@@ -454,6 +468,19 @@ def test_ee_7():
     assert nid.text.strip() == "http://federationX.org"
 
 
+def test_ee_xxe():
+    xml = """<?xml version="1.0"?>
+    <!DOCTYPE lolz [
+    <!ENTITY lol "lol">
+    <!ELEMENT lolz (#PCDATA)>
+    <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
+    ]>
+    <lolz>&lol1;</lolz>
+    """
+    with raises(EntitiesForbidden):
+        saml2.extension_element_from_string(xml)
+
+
 def test_extension_element_loadd():
     ava = {'attributes': {},
            'tag': 'ExternalEntityAttributeAuthority',

programming_language: Python
filename: test_43_soap.py
change_type: ModificationType.MODIFY
diff: @@ -12,9 +12,13 @@ except ImportError:
         import cElementTree as ElementTree
     except ImportError:
         from elementtree import ElementTree
+from defusedxml.common import EntitiesForbidden
+
+from pytest import raises
 
 import saml2.samlp as samlp
 from saml2.samlp import NAMESPACE as SAMLP_NAMESPACE
+from saml2 import soap
 
 NAMESPACE = "http://schemas.xmlsoap.org/soap/envelope/"
 
@@ -66,3 +70,42 @@ def test_make_soap_envelope():
     assert len(body) == 1
     saml_part = body[0]
     assert saml_part.tag == '{%s}AuthnRequest' % SAMLP_NAMESPACE
+
+
+def test_parse_soap_enveloped_saml_thingy_xxe():
+    xml = """<?xml version="1.0"?>
+    <!DOCTYPE lolz [
+    <!ENTITY lol "lol">
+    <!ELEMENT lolz (#PCDATA)>
+    <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
+    ]>
+    <lolz>&lol1;</lolz>
+    """
+    with raises(EntitiesForbidden):
+        soap.parse_soap_enveloped_saml_thingy(xml, None)
+
+
+def test_class_instances_from_soap_enveloped_saml_thingies_xxe():
+    xml = """<?xml version="1.0"?>
+    <!DOCTYPE lolz [
+    <!ENTITY lol "lol">
+    <!ELEMENT lolz (#PCDATA)>
+    <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
+    ]>
+    <lolz>&lol1;</lolz>
+    """
+    with raises(soap.XmlParseError):
+        soap.class_instances_from_soap_enveloped_saml_thingies(xml, None)
+
+
+def test_open_soap_envelope_xxe():
+    xml = """<?xml version="1.0"?>
+    <!DOCTYPE lolz [
+    <!ENTITY lol "lol">
+    <!ELEMENT lolz (#PCDATA)>
+    <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
+    ]>
+    <lolz>&lol1;</lolz>
+    """
+    with raises(soap.XmlParseError):
+        soap.open_soap_envelope(xml)

programming_language: Python
filename: test_51_client.py
change_type: ModificationType.MODIFY
diff: @@ -7,6 +7,7 @@ import six
 from future.backports.urllib.parse import parse_qs
 from future.backports.urllib.parse import urlencode
 from future.backports.urllib.parse import urlparse
+from pytest import raises
 
 from saml2.argtree import add_path
 from saml2.cert import OpenSSLWrapper
@@ -25,6 +26,7 @@ from saml2.assertion import Assertion
 from saml2.authn_context import INTERNETPROTOCOLPASSWORD
 from saml2.client import Saml2Client
 from saml2.config import SPConfig
+from saml2.pack import parse_soap_enveloped_saml
 from saml2.response import LogoutResponse
 from saml2.saml import NAMEID_FORMAT_PERSISTENT, EncryptedAssertion, Advice
 from saml2.saml import NAMEID_FORMAT_TRANSIENT
@@ -38,6 +40,8 @@ from saml2.s_utils import do_attribute_statement
 from saml2.s_utils import factory
 from saml2.time_util import in_a_while, a_while_ago
 
+from defusedxml.common import EntitiesForbidden
+
 from fakeIDP import FakeIDP
 from fakeIDP import unpack_form
 from pathutils import full_path
@@ -1552,6 +1556,17 @@ class TestClientWithDummy():
                'http://www.example.com/login'
         assert ac.authn_context_class_ref.text == INTERNETPROTOCOLPASSWORD
 
+def test_parse_soap_enveloped_saml_xxe():
+    xml = """<?xml version="1.0"?>
+    <!DOCTYPE lolz [
+    <!ENTITY lol "lol">
+    <!ELEMENT lolz (#PCDATA)>
+    <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
+    ]>
+    <lolz>&lol1;</lolz>
+    """
+    with raises(EntitiesForbidden):
+        parse_soap_enveloped_saml(xml, None)
 
 # if __name__ == "__main__":
 #     tc = TestClient()

programming_language: Python