Skip to content

Commit 7ec8ca0

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 1868c8b commit 7ec8ca0

3 files changed

Lines changed: 62 additions & 13 deletions

File tree

src/onelogin/saml2/response.py

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -82,16 +82,19 @@ def is_valid(self, request_data, request_id=None):
8282
sp_data = self.__settings.get_sp_data()
8383
sp_entity_id = sp_data['entityId']
8484

85-
sign_nodes = self.__query('//ds:Signature')
86-
87-
signed_elements = []
88-
for sign_node in sign_nodes:
89-
signed_elements.append(sign_node.getparent().tag)
85+
signed_elements = self.process_signed_elements()
9086

9187
if self.__settings.is_strict():
88+
no_valid_xml_msg = 'Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd'
9289
res = OneLogin_Saml2_XML.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active())
9390
if isinstance(res, str):
94-
raise Exception('Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd')
91+
raise Exception(no_valid_xml_msg)
92+
93+
# If encrypted, check also the decrypted document
94+
if self.encrypted:
95+
res = OneLogin_Saml2_XML.validate_xml(self.decrypted_document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active())
96+
if isinstance(res, str):
97+
raise Exception(no_valid_xml_msg)
9598

9699
security = self.__settings.get_security_data()
97100
current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data)
@@ -365,6 +368,53 @@ def validate_num_assertions(self):
365368
assertion_nodes = OneLogin_Saml2_XML.query(self.document, '//saml:Assertion')
366369
return (len(encrypted_assertion_nodes) + len(assertion_nodes)) == 1
367370

371+
def process_signed_elements(self):
372+
"""
373+
Verifies the signature nodes:
374+
- Checks that are Response or Assertion
375+
- Check that IDs and reference URI are unique and consistent.
376+
377+
:returns: The signed elements tag names
378+
:rtype: list
379+
"""
380+
sign_nodes = self.__query('//ds:Signature')
381+
382+
signed_elements = []
383+
verified_seis = []
384+
verified_ids = []
385+
response_tag = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP
386+
assertion_tag = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML
387+
388+
for sign_node in sign_nodes:
389+
signed_element = sign_node.getparent().tag
390+
if signed_element != response_tag and signed_element != assertion_tag:
391+
raise Exception('Invalid Signature Element %s SAML Response rejected' % signed_element)
392+
393+
if not sign_node.getparent().get('ID'):
394+
raise Exception('Signed Element must contain an ID. SAML Response rejected')
395+
396+
id_value = sign_node.getparent().get('ID')
397+
if id_value in verified_ids:
398+
raise Exception('Duplicated ID. SAML Response rejected')
399+
verified_ids.append(id_value)
400+
401+
# Check that reference URI matches the parent ID and no duplicate References or IDs
402+
ref = OneLogin_Saml2_XML.query(sign_node, './/ds:Reference')
403+
if ref:
404+
ref = ref[0]
405+
if ref.get('URI'):
406+
sei = ref.get('URI')[1:]
407+
408+
if sei != id_value:
409+
raise Exception('Found an invalid Signed Element. SAML Response rejected')
410+
411+
if sei in verified_seis:
412+
raise Exception('Duplicated Reference URI. SAML Response rejected')
413+
verified_seis.append(sei)
414+
415+
signed_elements.append(signed_element)
416+
return signed_elements
417+
368418
def validate_timestamps(self):
369419
"""
370420
Verifies that the document is valid according to Conditions Element
@@ -393,10 +443,8 @@ def __query_assertion(self, xpath_expr):
393443
:returns: The queried nodes
394444
:rtype: list
395445
"""
396-
if self.encrypted:
397-
assertion_expr = '/saml:EncryptedAssertion/saml:Assertion'
398-
else:
399-
assertion_expr = '/saml:Assertion'
446+
447+
assertion_expr = '/saml:Assertion'
400448
signature_expr = '/ds:Signature/ds:SignedInfo/ds:Reference'
401449
signed_assertion_query = '/samlp:Response' + assertion_expr + signature_expr
402450
assertion_reference_nodes = self.__query(signed_assertion_query)
@@ -473,7 +521,8 @@ def __decrypt_assertion(self, xml):
473521
keyinfo.append(encrypted_key[0])
474522

475523
encrypted_data = encrypted_data_nodes[0]
476-
OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key, debug)
524+
decrypted = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key, debug)
525+
xml.replace(encrypted_assertion_nodes[0], decrypted)
477526
return xml
478527

479528
def get_error(self):

src/onelogin/saml2/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -749,7 +749,7 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
749749
signature_nodes = OneLogin_Saml2_XML.query(elem, '/samlp:Response/ds:Signature')
750750

751751
if not len(signature_nodes) > 0:
752-
signature_nodes += OneLogin_Saml2_XML.query(elem, '/samlp:Response/saml:EncryptedAssertion/saml:Assertion/ds:Signature')
752+
signature_nodes += OneLogin_Saml2_XML.query(elem, '/samlp:Response/ds:Signature')
753753
signature_nodes += OneLogin_Saml2_XML.query(elem, '/samlp:Response/saml:Assertion/ds:Signature')
754754

755755
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
@@ -329,7 +329,7 @@ def testOnlyRetrieveAssertionWithIDThatMatchesSignatureReference(self):
329329
nameid = response.get_nameid()
330330
self.assertNotEqual('root@example.com', nameid)
331331
except Exception:
332-
self.assertEqual('Signature validation failed. SAML Response rejected', response.get_error())
332+
self.assertEqual('Invalid Signature Element {urn:oasis:names:tc:SAML:2.0:metadata}EntityDescriptor SAML Response rejected', response.get_error())
333333

334334
def testDoesNotAllowSignatureWrappingAttack(self):
335335
"""

0 commit comments

Comments
 (0)