Skip to content

Commit b65749d

Browse files
committed
On this commit we changed the way we decrypt an Assertion and added 2 new
validations in order to avoid Signature wrapping attacks: - Extra validations at the validateSignedElements method to check that the ref URIs and IDs are unique and consistent. - Validate the document (encrypted and decrypted version) against the scheme.
1 parent 9d312a8 commit b65749d

File tree

3 files changed

+60
-13
lines changed

3 files changed

+60
-13
lines changed

src/onelogin/saml2/response.py

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,16 +86,19 @@ def is_valid(self, request_data, request_id=None):
8686
sp_data = self.__settings.get_sp_data()
8787
sp_entity_id = sp_data.get('entityId', '')
8888

89-
sign_nodes = self.__query('//ds:Signature')
90-
91-
signed_elements = []
92-
for sign_node in sign_nodes:
93-
signed_elements.append(sign_node.getparent().tag)
89+
signed_elements = self.process_signed_elements()
9490

9591
if self.__settings.is_strict():
92+
no_valid_xml_msg = 'Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd'
9693
res = OneLogin_Saml2_Utils.validate_xml(etree.tostring(self.document), 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active())
9794
if not isinstance(res, Document):
98-
raise Exception('Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd')
95+
raise Exception(no_valid_xml_msg)
96+
97+
# If encrypted, check also the decrypted document
98+
if self.encrypted:
99+
res = OneLogin_Saml2_Utils.validate_xml(etree.tostring(self.decrypted_document), 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active())
100+
if not isinstance(res, Document):
101+
raise Exception(no_valid_xml_msg)
99102

100103
security = self.__settings.get_security_data()
101104
current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data)
@@ -385,6 +388,53 @@ def validate_num_assertions(self):
385388
assertion_nodes = OneLogin_Saml2_Utils.query(self.document, '//saml:Assertion')
386389
return (len(encrypted_assertion_nodes) + len(assertion_nodes)) == 1
387390

391+
def process_signed_elements(self):
392+
"""
393+
Verifies the signature nodes:
394+
- Checks that are Response or Assertion
395+
- Check that IDs and reference URI are unique and consistent.
396+
397+
:returns: The signed elements tag names
398+
:rtype: list
399+
"""
400+
sign_nodes = self.__query('//ds:Signature')
401+
402+
signed_elements = []
403+
verified_seis = []
404+
verified_ids = []
405+
response_tag = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP
406+
assertion_tag = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML
407+
408+
for sign_node in sign_nodes:
409+
signed_element = sign_node.getparent().tag
410+
if signed_element != response_tag and signed_element != assertion_tag:
411+
raise Exception('Invalid Signature Element %s SAML Response rejected' % signed_element)
412+
413+
if not sign_node.getparent().get('ID'):
414+
raise Exception('Signed Element must contain an ID. SAML Response rejected')
415+
416+
id_value = sign_node.getparent().get('ID')
417+
if id_value in verified_ids:
418+
raise Exception('Duplicated ID. SAML Response rejected')
419+
verified_ids.append(id_value)
420+
421+
# Check that reference URI matches the parent ID and no duplicate References or IDs
422+
ref = OneLogin_Saml2_Utils.query(sign_node, './/ds:Reference')
423+
if ref:
424+
ref = ref[0]
425+
if ref.get('URI'):
426+
sei = ref.get('URI')[1:]
427+
428+
if sei != id_value:
429+
raise Exception('Found an invalid Signed Element. SAML Response rejected')
430+
431+
if sei in verified_seis:
432+
raise Exception('Duplicated Reference URI. SAML Response rejected')
433+
verified_seis.append(sei)
434+
435+
signed_elements.append(signed_element)
436+
return signed_elements
437+
388438
def validate_timestamps(self):
389439
"""
390440
Verifies that the document is valid according to Conditions Element
@@ -413,10 +463,7 @@ def __query_assertion(self, xpath_expr):
413463
:returns: The queried nodes
414464
:rtype: list
415465
"""
416-
if self.encrypted:
417-
assertion_expr = '/saml:EncryptedAssertion/saml:Assertion'
418-
else:
419-
assertion_expr = '/saml:Assertion'
466+
assertion_expr = '/saml:Assertion'
420467
signature_expr = '/ds:Signature/ds:SignedInfo/ds:Reference'
421468
signed_assertion_query = '/samlp:Response' + assertion_expr + signature_expr
422469
assertion_reference_nodes = self.__query(signed_assertion_query)
@@ -493,7 +540,8 @@ def __decrypt_assertion(self, dom):
493540
keyinfo.append(encrypted_key[0])
494541

495542
encrypted_data = encrypted_data_nodes[0]
496-
OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key, debug)
543+
decrypted = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key, debug)
544+
dom.replace(encrypted_assertion_nodes[0], decrypted)
497545
return dom
498546

499547
def get_error(self):

src/onelogin/saml2/utils.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -926,7 +926,6 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
926926
signature_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:Response/ds:Signature')
927927

928928
if not len(signature_nodes) > 0:
929-
signature_nodes += OneLogin_Saml2_Utils.query(elem, '/samlp:Response/saml:EncryptedAssertion/saml:Assertion/ds:Signature')
930929
signature_nodes += OneLogin_Saml2_Utils.query(elem, '/samlp:Response/saml:Assertion/ds:Signature')
931930

932931
if len(signature_nodes) == 1:

tests/src/OneLogin/saml2_tests/response_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ def testOnlyRetrieveAssertionWithIDThatMatchesSignatureReference(self):
352352
nameid = response.get_nameid()
353353
self.assertNotEqual('[email protected]', nameid)
354354
except:
355-
self.assertEqual('Signature validation failed. SAML Response rejected', response.get_error())
355+
self.assertEqual('Invalid Signature Element {urn:oasis:names:tc:SAML:2.0:metadata}EntityDescriptor SAML Response rejected', response.get_error())
356356

357357
def testDoesNotAllowSignatureWrappingAttack(self):
358358
"""

0 commit comments

Comments
 (0)