Skip to content

Commit 5e9d5ac

Browse files
committed
Fix XML Signature Wrapping (XSW) vulnerabilities
PySAML2 did not check that the signature in a SAML document is enveloped and thus XML signature wrapping (XSW) was effective. The signature information and the node/object that is signed can be in different places and thus the signature verification will succeed, but the wrong data will be used. This specifically affects the verification of assertions that have been signed. This was assigned CVE-2020-5390 Thanks to Alexey Sintsov and Yuri Goltsev from HERE Technologies to report this. + + + + + + + + In more detail: libxml2 follows the xmldsig-core specification. The xmldsig specification is way too general. saml-core reuses the xmldsig specification, but constrains it to use of specific facilities. The implementation of the SAML specification is responsible to enforce those constraints. libxml2/xmlsec1 are not aware of those constraints and thus process the document based on the full/general xmldsig rules. What is happening is the following: - xmldsig-core allows the signature-information and the data that was signed to be in different places. This works by setting the URI attribute of the Reference element. The URI attribute contains an optional identifier of the object being signed. (see "4.4.3 The Reference Element" -- https://www.w3.org/TR/xmldsig-core1/#sec-Reference) This identifier is actually a pointer that can be defined in many different ways; from XPath expressions that need to be executed(!), to a full URL that should be fetched(!) in order to recalculate the signature. - saml-core section "5.4 XML Signature Profile" defines constrains on the xmldsig-core facilities. It explicitly dictates that enveloped signatures are the only signatures allowed. This mean that: * Assertion/RequestType/ResponseType elements must have an ID attribute * signatures must have a single Reference element * the Reference element must have a URI attribute * the URI attribute contains an anchor * the anchor points to the enclosing element's ID attribute xmlsec1 does the right thing - it follows the reference URI pointer and validates the assertion. But, the pointer points to an assertion in another part of the document; not the assertion in which the signature is embedded/enveloped. SAML processing thinks that the signature is fine (that's what xmlsec1 said), and gets the assertion data from the assertion that contains the signature - but that assertion was never validated. The issue is that pysaml2 does not enforce the constrains on the signature validation facilities of xmldsig-core, that the saml-core spec defines. The solution is simple; all we need is to make sure that assertions with signatures (1) contain one reference element that (2) has a URI attribute (3) that is an anchor that (4) points to the assertion in which the signature is embedded. If those conditions are met then we're good, otherwise we should fail the verification. Signed-off-by: Ivan Kanakarakis <[email protected]>
1 parent 324656e commit 5e9d5ac

File tree

3 files changed

+99
-0
lines changed

3 files changed

+99
-0
lines changed

src/saml2/sigver.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1476,6 +1476,55 @@ def _check_signature(self, decoded_xml, item, node_name=NODE_NAME, origdoc=None,
14761476
if not certs:
14771477
raise MissingKey(_issuer)
14781478

1479+
# saml-core section "5.4 XML Signature Profile" defines constrains on the
1480+
# xmldsig-core facilities. It explicitly dictates that enveloped signatures
1481+
# are the only signatures allowed. This mean that:
1482+
# * Assertion/RequestType/ResponseType elements must have an ID attribute
1483+
# * signatures must have a single Reference element
1484+
# * the Reference element must have a URI attribute
1485+
# * the URI attribute contains an anchor
1486+
# * the anchor points to the enclosing element's ID attribute
1487+
references = item.signature.signed_info.reference
1488+
signatures_must_have_a_single_reference_element = len(references) == 1
1489+
the_Reference_element_must_have_a_URI_attribute = (
1490+
signatures_must_have_a_single_reference_element
1491+
and hasattr(references[0], "uri")
1492+
)
1493+
the_URI_attribute_contains_an_anchor = (
1494+
the_Reference_element_must_have_a_URI_attribute
1495+
and references[0].uri.startswith("#")
1496+
and len(references[0].uri) > 1
1497+
)
1498+
the_anchor_points_to_the_enclosing_element_ID_attribute = (
1499+
the_URI_attribute_contains_an_anchor
1500+
and references[0].uri == "#{id}".format(id=item.id)
1501+
)
1502+
validators = {
1503+
"signatures must have a single reference element": (
1504+
signatures_must_have_a_single_reference_element
1505+
),
1506+
"the Reference element must have a URI attribute": (
1507+
the_Reference_element_must_have_a_URI_attribute
1508+
),
1509+
"the URI attribute contains an anchor": (
1510+
the_URI_attribute_contains_an_anchor
1511+
),
1512+
"the anchor points to the enclosing element ID attribute": (
1513+
the_anchor_points_to_the_enclosing_element_ID_attribute
1514+
),
1515+
}
1516+
if not all(validators.values()):
1517+
error_context = {
1518+
"message": "Signature failed to meet constraints on xmldsig",
1519+
"validators": validators,
1520+
"item ID": item.id,
1521+
"reference URI": item.signature.signed_info.reference[0].uri,
1522+
"issuer": _issuer,
1523+
"node name": node_name,
1524+
"xml document": decoded_xml,
1525+
}
1526+
raise SignatureError(error_context)
1527+
14791528
verified = False
14801529
last_pem_file = None
14811530

tests/saml2_response_xsw.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ns0:Response xmlns:ns0="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:ns1="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ns2="http://www.w3.org/2000/09/xmldsig#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" Destination="http://lingon.catalogix.se:8087/" ID="id-vqOQ72JCppXaBWnBE" InResponseTo="id12" IssueInstant="2019-12-20T12:15:16Z" Version="2.0"><ns1:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">urn:mace:example.com:saml:roland:idp</ns1:Issuer><ns0:Status><ns0:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></ns0:Status><ns1:Assertion ID="id-SPOOFED_ASSERTION" IssueInstant="2019-12-20T12:15:16Z" Version="2.0"><ns1:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">urn:mace:example.com:saml:roland:idp</ns1:Issuer><ns2:Signature Id="Signature2"><ns2:SignedInfo><ns2:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ns2:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><ns2:Reference URI="#id-Aa9IWfDxJVIX6GQye"><ns2:Transforms><ns2:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ns2:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ns2:Transforms><ns2:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ns2:DigestValue>EWBvQUlrwQbtrAjuUXkSBAVsZ50=</ns2:DigestValue></ns2:Reference></ns2:SignedInfo><ns2:SignatureValue>m4zRgTWleMcx1dFboeiYlbiDigHWAVhHVa+GLN++ELNMFDutuzBxc3tu6okyaNQGW3leu32wzbfdpb5+3RlpGoKj2wPX570/EMJj4uw91XfXsZfpNP+5GlgNT8w/elDmBXhG/KwmSO477Imk0szKovTBMVHmo3QOd+ba//dVsJE=</ns2:SignatureValue><ns2:KeyInfo><ns2:X509Data><ns2:X509Certificate>MIICsDCCAhmgAwIBAgIJAJrzqSSwmDY9MA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMDkxMDA2MTk0OTQxWhcNMDkxMTA1MTk0OTQxWjBFMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJg2cms7MqjniT8Fi/XkNHZNPbNVQyMUMXE9tXOdqwYCA1cc8vQdzkihscQMXy3iPw2cMggBu6gjMTOSOxECkuvX5ZCclKr8pXAJM5cY6gVOaVO2PdTZcvDBKGbiaNefiEw5hnoZomqZGp8wHNLAUkwtH9vjqqvxyS/vclc6k2ewIDAQABo4GnMIGkMB0GA1UdDgQWBBRePsKHKYJsiojE78ZWXccK9K4aJTB1BgNVHSMEbjBsgBRePsKHKYJsiojE78ZWXccK9K4aJaFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAJrzqSSwmDY9MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAJSrKOEzHO7TL5cy6h3qh+3+JAk8HbGBW+cbX6KBCAw/mzU8flK25vnWwXS3dv2FF3Aod0/S7AWNfKib5U/SA9nJaz/mWeF9S0farz9AQFc8/NSzAzaVq7YbM4F6f6N2FRl7GikdXRCed45j6mrPzGzk3ECbupFnqyREH3+ZPSdk=</ns2:X509Certificate></ns2:X509Data></ns2:KeyInfo></ns2:Signature><ns1:Subject><ns1:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" NameQualifier="" SPNameQualifier="id12">ANOTHER_ID</ns1:NameID><ns1:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><ns1:SubjectConfirmationData InResponseTo="id12" NotOnOrAfter="2019-12-20T12:20:16Z" Recipient="http://lingon.catalogix.se:8087/"/></ns1:SubjectConfirmation></ns1:Subject><ns1:Conditions NotBefore="2019-12-20T12:15:16Z" NotOnOrAfter="2019-12-20T12:20:16Z"><ns1:AudienceRestriction><ns1:Audience>urn:mace:example.com:saml:roland:sp</ns1:Audience></ns1:AudienceRestriction></ns1:Conditions><ns1:AuthnStatement AuthnInstant="2019-12-20T12:15:16Z" SessionIndex="id-eEhNCc5BSiesVOl8B"><ns1:AuthnContext><ns1:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword</ns1:AuthnContextClassRef><ns1:AuthenticatingAuthority>http://www.example.com/login</ns1:AuthenticatingAuthority></ns1:AuthnContext></ns1:AuthnStatement><ns1:AttributeStatement><ns1:Attribute FriendlyName="eduPersonAffiliation" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.1" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">staff</ns1:AttributeValue><ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">ADMIN</ns1:AttributeValue></ns1:Attribute><ns1:Attribute FriendlyName="mail" Name="urn:oid:0.9.2342.19200300.100.1.3" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">[email protected]</ns1:AttributeValue></ns1:Attribute><ns1:Attribute FriendlyName="givenName" Name="urn:oid:2.5.4.42" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">Derek</ns1:AttributeValue></ns1:Attribute><ns1:Attribute FriendlyName="surName" Name="urn:oid:2.5.4.4" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">Jeter</ns1:AttributeValue></ns1:Attribute><ns1:Attribute FriendlyName="title" Name="urn:oid:2.5.4.12" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">shortstop</ns1:AttributeValue></ns1:Attribute></ns1:AttributeStatement></ns1:Assertion>
3+
<XSW_ATTACK>
4+
<ns1:Assertion ID="id-Aa9IWfDxJVIX6GQye" IssueInstant="2019-12-20T12:15:16Z" Version="2.0"><ns1:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">urn:mace:example.com:saml:roland:idp</ns1:Issuer><ns1:Subject><ns1:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" NameQualifier="" SPNameQualifier="id12">ac5b22bb8eac4a26ed07a55432a0fe0da243f6e911aa614cff402c44d7cdec36</ns1:NameID><ns1:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><ns1:SubjectConfirmationData InResponseTo="id12" NotOnOrAfter="2019-12-20T12:20:16Z" Recipient="http://lingon.catalogix.se:8087/"/></ns1:SubjectConfirmation></ns1:Subject><ns1:Conditions NotBefore="2019-12-20T12:15:16Z" NotOnOrAfter="2019-12-20T12:20:16Z"><ns1:AudienceRestriction><ns1:Audience>urn:mace:example.com:saml:roland:sp</ns1:Audience></ns1:AudienceRestriction></ns1:Conditions><ns1:AuthnStatement AuthnInstant="2019-12-20T12:15:16Z" SessionIndex="id-eEhNCc5BSiesVOl8B"><ns1:AuthnContext><ns1:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword</ns1:AuthnContextClassRef><ns1:AuthenticatingAuthority>http://www.example.com/login</ns1:AuthenticatingAuthority></ns1:AuthnContext></ns1:AuthnStatement><ns1:AttributeStatement><ns1:Attribute FriendlyName="eduPersonAffiliation" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.1" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">staff</ns1:AttributeValue><ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">member</ns1:AttributeValue></ns1:Attribute><ns1:Attribute FriendlyName="mail" Name="urn:oid:0.9.2342.19200300.100.1.3" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">[email protected]</ns1:AttributeValue></ns1:Attribute><ns1:Attribute FriendlyName="givenName" Name="urn:oid:2.5.4.42" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">Derek</ns1:AttributeValue></ns1:Attribute><ns1:Attribute FriendlyName="surName" Name="urn:oid:2.5.4.4" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">Jeter</ns1:AttributeValue></ns1:Attribute><ns1:Attribute FriendlyName="title" Name="urn:oid:2.5.4.12" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><ns1:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:type="xs:string">shortstop</ns1:AttributeValue></ns1:Attribute></ns1:AttributeStatement></ns1:Assertion>
5+
</XSW_ATTACK>
6+
</ns0:Response>

tests/test_xsw.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from datetime import datetime
2+
from unittest.mock import Mock
3+
from unittest.mock import patch
4+
5+
from saml2.config import config_factory
6+
from saml2.response import authn_response
7+
from saml2.sigver import SignatureError
8+
9+
from dateutil import parser
10+
11+
from pytest import raises
12+
13+
from pathutils import dotname
14+
from pathutils import full_path
15+
16+
17+
XML_RESPONSE_XSW = full_path("saml2_response_xsw.xml")
18+
19+
20+
class TestAuthnResponse:
21+
def setup_class(self):
22+
self.conf = config_factory("sp", dotname("server_conf"))
23+
self.ar = authn_response(self.conf, "http://lingon.catalogix.se:8087/")
24+
25+
@patch('saml2.response.validate_on_or_after', return_value=True)
26+
def test_verify_signed_xsw(self, mock_validate_on_or_after):
27+
self.ar.issue_instant_ok = Mock(return_value=True)
28+
29+
with open(XML_RESPONSE_XSW) as fp:
30+
xml_response = fp.read()
31+
32+
self.ar.outstanding_queries = {"id12": "http://localhost:8088/sso"}
33+
self.ar.timeslack = 10000
34+
self.ar.loads(xml_response, decode=False)
35+
36+
assert self.ar.came_from == 'http://localhost:8088/sso'
37+
assert self.ar.session_id() == "id12"
38+
assert self.ar.issuer() == 'urn:mace:example.com:saml:roland:idp'
39+
40+
with raises(SignatureError):
41+
self.ar.verify()
42+
43+
assert self.ar.ava is None
44+
assert self.ar.name_id is None

0 commit comments

Comments
 (0)