Skip to content

Commit c2e8b09

Browse files
committed
Security improvements. Use of tagid to prevent XPath injection. Disable DTD on fromstring defusedxml method
1 parent 64811f6 commit c2e8b09

File tree

6 files changed

+49
-37
lines changed

6 files changed

+49
-37
lines changed

src/onelogin/saml2/idp_metadata_parser.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def get_metadata(url, validate_cert=True):
5050

5151
if xml:
5252
try:
53-
dom = fromstring(xml)
53+
dom = fromstring(xml, forbid_dtd=True)
5454
idp_descriptor_nodes = OneLogin_Saml2_Utils.query(dom, '//md:IDPSSODescriptor')
5555
if idp_descriptor_nodes:
5656
valid = True
@@ -124,7 +124,7 @@ def parse(
124124
"""
125125
data = {}
126126

127-
dom = fromstring(idp_metadata)
127+
dom = fromstring(idp_metadata, forbid_dtd=True)
128128

129129
entity_desc_path = '//md:EntityDescriptor'
130130
if entity_id:

src/onelogin/saml2/logout_request.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ def get_id(request):
171171
else:
172172
if isinstance(request, Document):
173173
request = request.toxml()
174-
elem = fromstring(request)
174+
elem = fromstring(request, forbid_dtd=True)
175175
return elem.get('ID', None)
176176

177177
@staticmethod
@@ -190,7 +190,7 @@ def get_nameid_data(request, key=None):
190190
else:
191191
if isinstance(request, Document):
192192
request = request.toxml()
193-
elem = fromstring(request)
193+
elem = fromstring(request, forbid_dtd=True)
194194

195195
name_id = None
196196
encrypted_entries = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:EncryptedID')
@@ -271,7 +271,7 @@ def get_issuer(request):
271271
else:
272272
if isinstance(request, Document):
273273
request = request.toxml()
274-
elem = fromstring(request)
274+
elem = fromstring(request, forbid_dtd=True)
275275

276276
issuer = None
277277
issuer_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:Issuer')
@@ -293,7 +293,7 @@ def get_session_indexes(request):
293293
else:
294294
if isinstance(request, Document):
295295
request = request.toxml()
296-
elem = fromstring(request)
296+
elem = fromstring(request, forbid_dtd=True)
297297

298298
session_indexes = []
299299
session_index_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/samlp:SessionIndex')
@@ -314,7 +314,7 @@ def is_valid(self, request_data, raise_exceptions=False):
314314
self.__error = None
315315
lowercase_urlencoding = False
316316
try:
317-
dom = fromstring(self.__logout_request)
317+
dom = fromstring(self.__logout_request, forbid_dtd=True)
318318

319319
idp_data = self.__settings.get_idp_data()
320320
idp_entity_id = idp_data['entityId']

src/onelogin/saml2/logout_response.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def __init__(self, settings, response=None):
4444

4545
if response is not None:
4646
self.__logout_response = OneLogin_Saml2_Utils.decode_base64_and_inflate(response)
47-
self.document = parseString(self.__logout_response)
47+
self.document = parseString(self.__logout_response, forbid_dtd=True)
4848
self.id = self.document.documentElement.getAttribute('ID')
4949

5050
def get_issuer(self):
@@ -200,7 +200,7 @@ def __query(self, query):
200200
"""
201201
# Switch to lxml for querying
202202
xml = self.document.toxml()
203-
return OneLogin_Saml2_Utils.query(fromstring(xml), query)
203+
return OneLogin_Saml2_Utils.query(fromstring(xml, forbid_dtd=True), query)
204204

205205
def build(self, in_response_to):
206206
"""

src/onelogin/saml2/metadata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ def add_x509_key_descriptors(metadata, cert=None, add_encryption=True):
247247
if cert is None or cert == '':
248248
return metadata
249249
try:
250-
xml = parseString(metadata.encode('utf-8'))
250+
xml = parseString(metadata.encode('utf-8'), forbid_dtd=True)
251251
except Exception as e:
252252
raise Exception('Error parsing metadata. ' + e.message)
253253

src/onelogin/saml2/response.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def __init__(self, settings, response):
4040
self.__settings = settings
4141
self.__error = None
4242
self.response = b64decode(response)
43-
self.document = fromstring(self.response)
43+
self.document = fromstring(self.response, forbid_dtd=True)
4444
self.decrypted_document = None
4545
self.encrypted = None
4646
self.valid_scd_not_on_or_after = None
@@ -735,38 +735,44 @@ def __query_assertion(self, xpath_expr):
735735
signature_expr = '/ds:Signature/ds:SignedInfo/ds:Reference'
736736
signed_assertion_query = '/samlp:Response' + assertion_expr + signature_expr
737737
assertion_reference_nodes = self.__query(signed_assertion_query)
738+
tagid = None
738739

739740
if not assertion_reference_nodes:
740741
# Check if the message is signed
741742
signed_message_query = '/samlp:Response' + signature_expr
742743
message_reference_nodes = self.__query(signed_message_query)
743744
if message_reference_nodes:
744745
message_id = message_reference_nodes[0].get('URI')
745-
final_query = "/samlp:Response[@ID='%s']/" % message_id[1:]
746+
final_query = "/samlp:Response[@ID=$tagid]/"
747+
tagid = message_id[1:]
746748
else:
747749
final_query = "/samlp:Response"
748750
final_query += assertion_expr
749751
else:
750752
assertion_id = assertion_reference_nodes[0].get('URI')
751-
final_query = '/samlp:Response' + assertion_expr + "[@ID='%s']" % assertion_id[1:]
753+
final_query = '/samlp:Response' + assertion_expr + "[@ID=$tagid]"
754+
tagid = assertion_id[1:]
752755
final_query += xpath_expr
753-
return self.__query(final_query)
756+
return self.__query(final_query, tagid)
754757

755-
def __query(self, query):
758+
def __query(self, query, tagid=None):
756759
"""
757760
Extracts nodes that match the query from the Response
758761
759762
:param query: Xpath Expresion
760763
:type query: String
761764
765+
:param tagid: Tag ID
766+
:type query: String
767+
762768
:returns: The queried nodes
763769
:rtype: list
764770
"""
765771
if self.encrypted:
766772
document = self.decrypted_document
767773
else:
768774
document = self.document
769-
return OneLogin_Saml2_Utils.query(document, query)
775+
return OneLogin_Saml2_Utils.query(document, query, None, tagid)
770776

771777
def __decrypt_assertion(self, dom):
772778
"""
@@ -817,7 +823,7 @@ def __decrypt_assertion(self, dom):
817823
if not uri.startswith('#'):
818824
break
819825
uri = uri.split('#')[1]
820-
encrypted_key = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], './xenc:EncryptedKey[@Id="' + uri + '"]')
826+
encrypted_key = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], './xenc:EncryptedKey[@Id=$tagid]', None, uri)
821827
if encrypted_key:
822828
keyinfo.append(encrypted_key[0])
823829

src/onelogin/saml2/utils.py

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def validate_xml(xml, schema, debug=False):
141141

142142
# Switch to lxml for schema validation
143143
try:
144-
dom = fromstring(xml.encode('utf-8'))
144+
dom = fromstring(xml.encode('utf-8'), forbid_dtd=True)
145145
except Exception:
146146
return 'unloaded_xml'
147147

@@ -160,7 +160,7 @@ def validate_xml(xml, schema, debug=False):
160160

161161
return 'invalid_xml'
162162

163-
return parseString(tostring(dom, encoding='unicode').encode('utf-8'))
163+
return parseString(tostring(dom, encoding='unicode').encode('utf-8'), forbid_dtd=True)
164164

165165
@staticmethod
166166
def element_text(node):
@@ -530,7 +530,7 @@ def get_expire_time(cache_duration=None, valid_until=None):
530530
return None
531531

532532
@staticmethod
533-
def query(dom, query, context=None):
533+
def query(dom, query, context=None, tagid=None):
534534
"""
535535
Extracts nodes that match the query from the Element
536536
@@ -543,13 +543,21 @@ def query(dom, query, context=None):
543543
:param context: Context Node
544544
:type: DOMElement
545545
546+
:param tagid: Tag ID
547+
:type: string
548+
546549
:returns: The queried nodes
547550
:rtype: list
548551
"""
549552
if context is None:
550-
return dom.xpath(query, namespaces=OneLogin_Saml2_Constants.NSMAP)
553+
source = dom
554+
else:
555+
source = context
556+
557+
if tagid is None:
558+
return source.xpath(query, namespaces=OneLogin_Saml2_Constants.NSMAP)
551559
else:
552-
return context.xpath(query, namespaces=OneLogin_Saml2_Constants.NSMAP)
560+
return source.xpath(query, tagid=tagid, namespaces=OneLogin_Saml2_Constants.NSMAP)
553561

554562
@staticmethod
555563
def delete_local_session(callback=None):
@@ -668,7 +676,7 @@ def generate_name_id(value, sp_nq, sp_format=None, cert=None, debug=False, nq=No
668676

669677
if cert is not None:
670678
xml = name_id_container.toxml()
671-
elem = fromstring(xml)
679+
elem = fromstring(xml, forbid_dtd=True)
672680

673681
error_callback_method = None
674682
if debug:
@@ -697,7 +705,7 @@ def generate_name_id(value, sp_nq, sp_format=None, cert=None, debug=False, nq=No
697705

698706
edata = enc_ctx.encryptXml(enc_data, elem[0])
699707

700-
newdoc = parseString(tostring(edata, encoding='unicode').encode('utf-8'))
708+
newdoc = parseString(tostring(edata, encoding='unicode').encode('utf-8'), forbid_dtd=True)
701709

702710
if newdoc.hasChildNodes():
703711
child = newdoc.firstChild
@@ -783,9 +791,9 @@ def decrypt_element(encrypted_data, key, debug=False, inplace=False):
783791
:rtype: lxml.etree.Element
784792
"""
785793
if isinstance(encrypted_data, Element):
786-
encrypted_data = fromstring(str(encrypted_data.toxml()))
794+
encrypted_data = fromstring(str(encrypted_data.toxml()), forbid_dtd=True)
787795
elif isinstance(encrypted_data, basestring):
788-
encrypted_data = fromstring(str(encrypted_data))
796+
encrypted_data = fromstring(str(encrypted_data), forbid_dtd=True)
789797
elif not inplace and isinstance(encrypted_data, etree._Element):
790798
encrypted_data = deepcopy(encrypted_data)
791799

@@ -851,7 +859,7 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
851859
elem = xml
852860
elif isinstance(xml, Document):
853861
xml = xml.toxml()
854-
elem = fromstring(xml.encode('utf-8'))
862+
elem = fromstring(xml.encode('utf-8'), forbid_dtd=True)
855863
elif isinstance(xml, Element):
856864
xml.setAttributeNS(
857865
unicode(OneLogin_Saml2_Constants.NS_SAMLP),
@@ -864,9 +872,9 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
864872
unicode(OneLogin_Saml2_Constants.NS_SAML)
865873
)
866874
xml = xml.toxml()
867-
elem = fromstring(xml.encode('utf-8'))
875+
elem = fromstring(xml.encode('utf-8'), forbid_dtd=True)
868876
elif isinstance(xml, basestring):
869-
elem = fromstring(xml.encode('utf-8'))
877+
elem = fromstring(xml.encode('utf-8'), forbid_dtd=True)
870878
else:
871879
raise Exception('Error parsing xml string')
872880

@@ -939,8 +947,6 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
939947
dsig_ctx.sign(signature)
940948

941949
return tostring(elem, encoding='unicode').encode('utf-8')
942-
newdoc = parseString(tostring(elem, encoding='unicode').encode('utf-8'))
943-
return newdoc.saveXML(newdoc.firstChild)
944950

945951
@staticmethod
946952
@return_false_on_exception
@@ -981,7 +987,7 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
981987
elem = xml
982988
elif isinstance(xml, Document):
983989
xml = xml.toxml()
984-
elem = fromstring(str(xml))
990+
elem = fromstring(str(xml), forbid_dtd=True)
985991
elif isinstance(xml, Element):
986992
xml.setAttributeNS(
987993
unicode(OneLogin_Saml2_Constants.NS_SAMLP),
@@ -994,9 +1000,9 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
9941000
unicode(OneLogin_Saml2_Constants.NS_SAML)
9951001
)
9961002
xml = xml.toxml()
997-
elem = fromstring(str(xml))
1003+
elem = fromstring(str(xml), forbid_dtd=True)
9981004
elif isinstance(xml, basestring):
999-
elem = fromstring(str(xml))
1005+
elem = fromstring(str(xml), forbid_dtd=True)
10001006
else:
10011007
raise Exception('Error parsing xml string')
10021008

@@ -1065,17 +1071,17 @@ def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha
10651071
elem = xml
10661072
elif isinstance(xml, Document):
10671073
xml = xml.toxml()
1068-
elem = fromstring(str(xml))
1074+
elem = fromstring(str(xml), forbid_dtd=True)
10691075
elif isinstance(xml, Element):
10701076
xml.setAttributeNS(
10711077
unicode(OneLogin_Saml2_Constants.NS_MD),
10721078
'xmlns:md',
10731079
unicode(OneLogin_Saml2_Constants.NS_MD)
10741080
)
10751081
xml = xml.toxml()
1076-
elem = fromstring(str(xml))
1082+
elem = fromstring(str(xml), forbid_dtd=True)
10771083
elif isinstance(xml, basestring):
1078-
elem = fromstring(str(xml))
1084+
elem = fromstring(str(xml), forbid_dtd=True)
10791085
else:
10801086
raise Exception('Error parsing xml string')
10811087

0 commit comments

Comments
 (0)