Skip to content

Commit 7854c08

Browse files
committed
Fix security vulnerability. Prevent signature wrapping
1 parent 57eb202 commit 7854c08

File tree

4 files changed

+165
-43
lines changed

4 files changed

+165
-43
lines changed

src/onelogin/saml2/response.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def is_valid(self, request_data, request_id=None):
119119
if security.get('wantAttributeStatement', True) and not attribute_statement_nodes:
120120
raise Exception('There is no AttributeStatement on the Response')
121121

122-
# Validates Asserion timestamps
122+
# Validates Assertion timestamps
123123
if not self.validate_timestamps():
124124
raise Exception('Timing issues (please check your clock settings)')
125125

@@ -194,13 +194,16 @@ def is_valid(self, request_data, request_id=None):
194194
raise Exception('The Message of the Response is not signed and the SP require it')
195195

196196
if len(signed_elements) > 0:
197+
if len(signed_elements) > 2:
198+
raise Exception('Too many Signatures found. SAML Response rejected')
197199
cert = idp_data.get('x509cert', None)
198200
fingerprint = idp_data.get('certFingerprint', None)
199201
fingerprintalg = idp_data.get('certFingerprintAlgorithm', None)
200202

201-
# Only validates the first sign found
203+
# If find a Signature on the Response, validates it checking the original response
202204
if '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP in signed_elements:
203205
document_to_validate = self.document
206+
# Otherwise validates the assertion (decrypted assertion if was encrypted)
204207
else:
205208
if self.encrypted:
206209
document_to_validate = self.decrypted_document
@@ -374,8 +377,8 @@ def validate_num_assertions(self):
374377
:returns: True if only 1 assertion encrypted or not
375378
:rtype: bool
376379
"""
377-
encrypted_assertion_nodes = self.__query('/samlp:Response/saml:EncryptedAssertion')
378-
assertion_nodes = self.__query('/samlp:Response/saml:Assertion')
380+
encrypted_assertion_nodes = OneLogin_Saml2_Utils.query(self.document, '//saml:EncryptedAssertion')
381+
assertion_nodes = OneLogin_Saml2_Utils.query(self.document, '//saml:Assertion')
379382
return (len(encrypted_assertion_nodes) + len(assertion_nodes)) == 1
380383

381384
def validate_timestamps(self):
@@ -461,7 +464,7 @@ def __decrypt_assertion(self, dom):
461464
if not key:
462465
raise Exception('No private key available, check settings')
463466

464-
encrypted_assertion_nodes = OneLogin_Saml2_Utils.query(dom, '//saml:EncryptedAssertion')
467+
encrypted_assertion_nodes = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/saml:EncryptedAssertion')
465468
if encrypted_assertion_nodes:
466469
encrypted_data_nodes = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], '//saml:EncryptedAssertion/xenc:EncryptedData')
467470
if encrypted_data_nodes:

src/onelogin/saml2/utils.py

Lines changed: 141 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -918,51 +918,163 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
918918

919919
xmlsec.addIDs(elem, ["ID"])
920920

921-
signature_nodes = OneLogin_Saml2_Utils.query(elem, '//ds:Signature')
921+
signature_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:Response/ds:Signature')
922922

923-
if len(signature_nodes) > 0:
923+
if not len(signature_nodes) > 0:
924+
signature_nodes += OneLogin_Saml2_Utils.query(elem, '/samlp:Response/saml:EncryptedAssertion/saml:Assertion/ds:Signature')
925+
signature_nodes += OneLogin_Saml2_Utils.query(elem, '/samlp:Response/saml:Assertion/ds:Signature')
926+
927+
if len(signature_nodes) == 1:
924928
signature_node = signature_nodes[0]
925929

926-
if (cert is None or cert == '') and fingerprint:
927-
x509_certificate_nodes = OneLogin_Saml2_Utils.query(signature_node, '//ds:Signature/ds:KeyInfo/ds:X509Data/ds:X509Certificate')
928-
if len(x509_certificate_nodes) > 0:
929-
x509_certificate_node = x509_certificate_nodes[0]
930-
x509_cert_value = x509_certificate_node.text
931-
x509_fingerprint_value = OneLogin_Saml2_Utils.calculate_x509_fingerprint(x509_cert_value, fingerprintalg)
932-
if fingerprint == x509_fingerprint_value:
933-
cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value)
930+
return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug)
931+
else:
932+
return False
933+
except Exception:
934+
return False
934935

935-
if cert is None or cert == '':
936-
return False
936+
@staticmethod
937+
def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False):
938+
"""
939+
Validates a signature of a EntityDescriptor.
937940
938-
# Check if Reference URI is empty
939-
reference_elem = OneLogin_Saml2_Utils.query(signature_node, '//ds:Reference')
940-
if len(reference_elem) > 0:
941-
if reference_elem[0].get('URI') == '':
942-
reference_elem[0].set('URI', '#%s' % signature_node.getparent().get('ID'))
941+
:param xml: The element we should validate
942+
:type: string | Document
943943
944-
dsig_ctx = xmlsec.DSigCtx()
944+
:param cert: The pubic cert
945+
:type: string
945946
946-
file_cert = OneLogin_Saml2_Utils.write_temp_file(cert)
947+
:param fingerprint: The fingerprint of the public cert
948+
:type: string
947949
948-
if validatecert:
949-
mngr = xmlsec.KeysMngr()
950-
mngr.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem, xmlsec.KeyDataTypeTrusted)
951-
dsig_ctx = xmlsec.DSigCtx(mngr)
952-
else:
953-
dsig_ctx = xmlsec.DSigCtx()
954-
dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None)
950+
:param fingerprintalg: The algorithm used to build the fingerprint
951+
:type: string
952+
953+
:param validatecert: If true, will verify the signature and if the cert is valid.
954+
:type: bool
955+
956+
:param debug: Activate the xmlsec debug
957+
:type: bool
958+
"""
959+
try:
960+
if xml is None or xml == '':
961+
raise Exception('Empty string supplied as input')
962+
elif isinstance(xml, etree._Element):
963+
elem = xml
964+
elif isinstance(xml, Document):
965+
xml = xml.toxml()
966+
elem = fromstring(str(xml))
967+
elif isinstance(xml, Element):
968+
xml.setAttributeNS(
969+
unicode(OneLogin_Saml2_Constants.NS_MD),
970+
'xmlns:md',
971+
unicode(OneLogin_Saml2_Constants.NS_MD)
972+
)
973+
xml = xml.toxml()
974+
elem = fromstring(str(xml))
975+
elif isinstance(xml, basestring):
976+
elem = fromstring(str(xml))
977+
else:
978+
raise Exception('Error parsing xml string')
979+
980+
xmlsec.initialize()
981+
982+
if debug:
983+
xmlsec.set_error_callback(print_xmlsec_errors)
984+
985+
xmlsec.addIDs(elem, ["ID"])
986+
987+
signature_nodes = OneLogin_Saml2_Utils.query(elem, '/md:EntitiesDescriptor/ds:Signature')
955988

956-
file_cert.close()
989+
if len(signature_nodes) == 0:
990+
signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/ds:Signature')
957991

958-
dsig_ctx.setEnabledKeyData([xmlsec.KeyDataX509])
959-
dsig_ctx.verify(signature_node)
992+
if len(signature_nodes) == 0:
993+
signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/md:SPSSODescriptor/ds:Signature')
994+
signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/md:IDPSSODescriptor/ds:Signature')
995+
996+
if len(signature_nodes) > 0:
997+
for signature_node in signature_nodes:
998+
if not OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug):
999+
return False
9601000
return True
9611001
else:
9621002
return False
9631003
except Exception:
9641004
return False
9651005

1006+
@staticmethod
1007+
def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False):
1008+
"""
1009+
Validates a signature node.
1010+
1011+
:param signature_node: The signature node
1012+
:type: Node
1013+
1014+
:param xml: The element we should validate
1015+
:type: Document
1016+
1017+
:param cert: The pubic cert
1018+
:type: string
1019+
1020+
:param fingerprint: The fingerprint of the public cert
1021+
:type: string
1022+
1023+
:param fingerprintalg: The algorithm used to build the fingerprint
1024+
:type: string
1025+
1026+
:param validatecert: If true, will verify the signature and if the cert is valid.
1027+
:type: bool
1028+
1029+
:param debug: Activate the xmlsec debug
1030+
:type: bool
1031+
"""
1032+
try:
1033+
xmlsec.initialize()
1034+
1035+
if debug:
1036+
xmlsec.set_error_callback(print_xmlsec_errors)
1037+
1038+
xmlsec.addIDs(elem, ["ID"])
1039+
1040+
if (cert is None or cert == '') and fingerprint:
1041+
x509_certificate_nodes = OneLogin_Saml2_Utils.query(signature_node, '//ds:Signature/ds:KeyInfo/ds:X509Data/ds:X509Certificate')
1042+
if len(x509_certificate_nodes) > 0:
1043+
x509_certificate_node = x509_certificate_nodes[0]
1044+
x509_cert_value = x509_certificate_node.text
1045+
x509_fingerprint_value = OneLogin_Saml2_Utils.calculate_x509_fingerprint(x509_cert_value, fingerprintalg)
1046+
if fingerprint == x509_fingerprint_value:
1047+
cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value)
1048+
1049+
if cert is None or cert == '':
1050+
return False
1051+
1052+
# Check if Reference URI is empty
1053+
reference_elem = OneLogin_Saml2_Utils.query(signature_node, '//ds:Reference')
1054+
if len(reference_elem) > 0:
1055+
if reference_elem[0].get('URI') == '':
1056+
reference_elem[0].set('URI', '#%s' % signature_node.getparent().get('ID'))
1057+
1058+
dsig_ctx = xmlsec.DSigCtx()
1059+
1060+
file_cert = OneLogin_Saml2_Utils.write_temp_file(cert)
1061+
1062+
if validatecert:
1063+
mngr = xmlsec.KeysMngr()
1064+
mngr.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem, xmlsec.KeyDataTypeTrusted)
1065+
dsig_ctx = xmlsec.DSigCtx(mngr)
1066+
else:
1067+
dsig_ctx = xmlsec.DSigCtx()
1068+
dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None)
1069+
1070+
file_cert.close()
1071+
1072+
dsig_ctx.setEnabledKeyData([xmlsec.KeyDataX509])
1073+
dsig_ctx.verify(signature_node)
1074+
return True
1075+
except Exception:
1076+
return False
1077+
9661078
@staticmethod
9671079
def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_Saml2_Constants.RSA_SHA1, debug=False):
9681080
"""

0 commit comments

Comments
 (0)