Skip to content

Commit 8c2b052

Browse files
authored
Merge pull request #379 from fruechel/master
Fix XXE in XML parsing (related to #366)
2 parents 78261b9 + 6e09a25 commit 8c2b052

File tree

7 files changed

+95
-6
lines changed

7 files changed

+95
-6
lines changed

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
'pytz',
1919
'pyOpenSSL',
2020
'python-dateutil',
21+
'defusedxml',
2122
'six'
2223
]
2324

src/saml2/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import cElementTree as ElementTree
3737
except ImportError:
3838
from elementtree import ElementTree
39+
import defusedxml.ElementTree
3940

4041
root_logger = logging.getLogger(__name__)
4142
root_logger.level = logging.NOTSET
@@ -87,7 +88,7 @@ def create_class_from_xml_string(target_class, xml_string):
8788
"""
8889
if not isinstance(xml_string, six.binary_type):
8990
xml_string = xml_string.encode('utf-8')
90-
tree = ElementTree.fromstring(xml_string)
91+
tree = defusedxml.ElementTree.fromstring(xml_string)
9192
return create_class_from_element_tree(target_class, tree)
9293

9394

@@ -269,7 +270,7 @@ def loadd(self, ava):
269270

270271

271272
def extension_element_from_string(xml_string):
272-
element_tree = ElementTree.fromstring(xml_string)
273+
element_tree = defusedxml.ElementTree.fromstring(xml_string)
273274
return _extension_element_from_element_tree(element_tree)
274275

275276

src/saml2/pack.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import cElementTree as ElementTree
3838
except ImportError:
3939
from elementtree import ElementTree
40+
import defusedxml.ElementTree
4041

4142
NAMESPACE = "http://schemas.xmlsoap.org/soap/envelope/"
4243
FORM_SPEC = """<form method="post" action="%s">
@@ -235,7 +236,7 @@ def parse_soap_enveloped_saml(text, body_class, header_class=None):
235236
:param text: The SOAP object as XML
236237
:return: header parts and body as saml.samlbase instances
237238
"""
238-
envelope = ElementTree.fromstring(text)
239+
envelope = defusedxml.ElementTree.fromstring(text)
239240
assert envelope.tag == '{%s}Envelope' % NAMESPACE
240241

241242
# print(len(envelope))

src/saml2/soap.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
except ImportError:
2020
#noinspection PyUnresolvedReferences
2121
from elementtree import ElementTree
22+
import defusedxml.ElementTree
2223

2324

2425
logger = logging.getLogger(__name__)
@@ -133,7 +134,7 @@ def parse_soap_enveloped_saml_thingy(text, expected_tags):
133134
:param expected_tags: What the tag of the SAML thingy is expected to be.
134135
:return: SAML thingy as a string
135136
"""
136-
envelope = ElementTree.fromstring(text)
137+
envelope = defusedxml.ElementTree.fromstring(text)
137138

138139
# Make sure it's a SOAP message
139140
assert envelope.tag == '{%s}Envelope' % soapenv.NAMESPACE
@@ -183,7 +184,7 @@ def class_instances_from_soap_enveloped_saml_thingies(text, modules):
183184
:return: The body and headers as class instances
184185
"""
185186
try:
186-
envelope = ElementTree.fromstring(text)
187+
envelope = defusedxml.ElementTree.fromstring(text)
187188
except Exception as exc:
188189
raise XmlParseError("%s" % exc)
189190

@@ -209,7 +210,7 @@ def open_soap_envelope(text):
209210
:return: dictionary with two keys "body"/"header"
210211
"""
211212
try:
212-
envelope = ElementTree.fromstring(text)
213+
envelope = defusedxml.ElementTree.fromstring(text)
213214
except Exception as exc:
214215
raise XmlParseError("%s" % exc)
215216

tests/test_03_saml2.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import cElementTree as ElementTree
1818
except ImportError:
1919
from elementtree import ElementTree
20+
from defusedxml.common import EntitiesForbidden
2021

2122
ITEMS = {
2223
NameID: ["""<?xml version="1.0" encoding="utf-8"?>
@@ -166,6 +167,19 @@ def test_create_class_from_xml_string_wrong_class_spec():
166167
assert kl == None
167168

168169

170+
def test_create_class_from_xml_string_xxe():
171+
xml = """<?xml version="1.0"?>
172+
<!DOCTYPE lolz [
173+
<!ENTITY lol "lol">
174+
<!ELEMENT lolz (#PCDATA)>
175+
<!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
176+
]>
177+
<lolz>&lol1;</lolz>
178+
"""
179+
with raises(EntitiesForbidden) as err:
180+
create_class_from_xml_string(NameID, xml)
181+
182+
169183
def test_ee_1():
170184
ee = saml2.extension_element_from_string(
171185
"""<?xml version='1.0' encoding='UTF-8'?><foo>bar</foo>""")
@@ -454,6 +468,19 @@ def test_ee_7():
454468
assert nid.text.strip() == "http://federationX.org"
455469

456470

471+
def test_ee_xxe():
472+
xml = """<?xml version="1.0"?>
473+
<!DOCTYPE lolz [
474+
<!ENTITY lol "lol">
475+
<!ELEMENT lolz (#PCDATA)>
476+
<!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
477+
]>
478+
<lolz>&lol1;</lolz>
479+
"""
480+
with raises(EntitiesForbidden):
481+
saml2.extension_element_from_string(xml)
482+
483+
457484
def test_extension_element_loadd():
458485
ava = {'attributes': {},
459486
'tag': 'ExternalEntityAttributeAuthority',

tests/test_43_soap.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@
1212
import cElementTree as ElementTree
1313
except ImportError:
1414
from elementtree import ElementTree
15+
from defusedxml.common import EntitiesForbidden
16+
17+
from pytest import raises
1518

1619
import saml2.samlp as samlp
1720
from saml2.samlp import NAMESPACE as SAMLP_NAMESPACE
21+
from saml2 import soap
1822

1923
NAMESPACE = "http://schemas.xmlsoap.org/soap/envelope/"
2024

@@ -66,3 +70,42 @@ def test_make_soap_envelope():
6670
assert len(body) == 1
6771
saml_part = body[0]
6872
assert saml_part.tag == '{%s}AuthnRequest' % SAMLP_NAMESPACE
73+
74+
75+
def test_parse_soap_enveloped_saml_thingy_xxe():
76+
xml = """<?xml version="1.0"?>
77+
<!DOCTYPE lolz [
78+
<!ENTITY lol "lol">
79+
<!ELEMENT lolz (#PCDATA)>
80+
<!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
81+
]>
82+
<lolz>&lol1;</lolz>
83+
"""
84+
with raises(EntitiesForbidden):
85+
soap.parse_soap_enveloped_saml_thingy(xml, None)
86+
87+
88+
def test_class_instances_from_soap_enveloped_saml_thingies_xxe():
89+
xml = """<?xml version="1.0"?>
90+
<!DOCTYPE lolz [
91+
<!ENTITY lol "lol">
92+
<!ELEMENT lolz (#PCDATA)>
93+
<!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
94+
]>
95+
<lolz>&lol1;</lolz>
96+
"""
97+
with raises(soap.XmlParseError):
98+
soap.class_instances_from_soap_enveloped_saml_thingies(xml, None)
99+
100+
101+
def test_open_soap_envelope_xxe():
102+
xml = """<?xml version="1.0"?>
103+
<!DOCTYPE lolz [
104+
<!ENTITY lol "lol">
105+
<!ELEMENT lolz (#PCDATA)>
106+
<!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
107+
]>
108+
<lolz>&lol1;</lolz>
109+
"""
110+
with raises(soap.XmlParseError):
111+
soap.open_soap_envelope(xml)

tests/test_51_client.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from future.backports.urllib.parse import parse_qs
88
from future.backports.urllib.parse import urlencode
99
from future.backports.urllib.parse import urlparse
10+
from pytest import raises
1011

1112
from saml2.argtree import add_path
1213
from saml2.cert import OpenSSLWrapper
@@ -25,6 +26,7 @@
2526
from saml2.authn_context import INTERNETPROTOCOLPASSWORD
2627
from saml2.client import Saml2Client
2728
from saml2.config import SPConfig
29+
from saml2.pack import parse_soap_enveloped_saml
2830
from saml2.response import LogoutResponse
2931
from saml2.saml import NAMEID_FORMAT_PERSISTENT, EncryptedAssertion, Advice
3032
from saml2.saml import NAMEID_FORMAT_TRANSIENT
@@ -38,6 +40,8 @@
3840
from saml2.s_utils import factory
3941
from saml2.time_util import in_a_while, a_while_ago
4042

43+
from defusedxml.common import EntitiesForbidden
44+
4145
from fakeIDP import FakeIDP
4246
from fakeIDP import unpack_form
4347
from pathutils import full_path
@@ -1552,6 +1556,17 @@ def test_negotiated_post_sso(self):
15521556
'http://www.example.com/login'
15531557
assert ac.authn_context_class_ref.text == INTERNETPROTOCOLPASSWORD
15541558

1559+
def test_parse_soap_enveloped_saml_xxe():
1560+
xml = """<?xml version="1.0"?>
1561+
<!DOCTYPE lolz [
1562+
<!ENTITY lol "lol">
1563+
<!ELEMENT lolz (#PCDATA)>
1564+
<!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
1565+
]>
1566+
<lolz>&lol1;</lolz>
1567+
"""
1568+
with raises(EntitiesForbidden):
1569+
parse_soap_enveloped_saml(xml, None)
15551570

15561571
# if __name__ == "__main__":
15571572
# tc = TestClient()

0 commit comments

Comments
 (0)