Skip to content

Commit 3b70772

Browse files
committed
Fix CVE-2021-21238 - SAML XML Signature wrapping
All users of pysaml2 that use the default `CryptoBackendXmlSec1` backend and need to verify signed SAML documents are impacted. `pysaml2 <= 6.4.1` does not validate the SAML document against an XML schema. This allows invalid XML documents to trick the verification process, by presenting elements with a valid signature inside elements whose content has been malformed. The verification is offloaded to `xmlsec1` and `xmlsec1` will not validate every signature in the given document, but only the first it finds in the given scope. Credits for the report: - Victor Schönfelder Garcia (isits AG International School of IT Security) - Juraj Somorovsky (Paderborn University) - Vladislav Mladenov (Ruhr University Bochum) Signed-off-by: Ivan Kanakarakis <[email protected]>
1 parent b76ea40 commit 3b70772

7 files changed

+318
-0
lines changed

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ install_requires =
5454
requests >= 1.0.0
5555
six
5656
importlib_resources
57+
xmlschema
5758

5859

5960
[options.packages.find]

src/saml2/sigver.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
from saml2.xmlenc import CipherData
5252
from saml2.xmlenc import CipherValue
5353
from saml2.xmlenc import EncryptedData
54+
from saml2.xml.schema import node_to_schema
55+
from saml2.xml.schema import XMLSchemaError
5456

5557

5658
logger = logging.getLogger(__name__)
@@ -1460,6 +1462,30 @@ def _check_signature(self, decoded_xml, item, node_name=NODE_NAME, origdoc=None,
14601462
if not certs:
14611463
raise MissingKey(_issuer)
14621464

1465+
# validate XML with the appropriate schema
1466+
try:
1467+
_schema = node_to_schema[node_name]
1468+
except KeyError as e:
1469+
error_context = {
1470+
"message": "Signature verification failed. Unknown node type.",
1471+
"issuer": _issuer,
1472+
"type": node_name,
1473+
"document": decoded_xml,
1474+
}
1475+
raise SignatureError(error_context) from e
1476+
1477+
try:
1478+
_schema.validate(str(item))
1479+
except XMLSchemaError as e:
1480+
error_context = {
1481+
"message": "Signature verification failed. Invalid document format.",
1482+
"ID": item.id,
1483+
"issuer": _issuer,
1484+
"type": node_name,
1485+
"document": decoded_xml,
1486+
}
1487+
raise SignatureError(error_context) from e
1488+
14631489
# saml-core section "5.4 XML Signature Profile" defines constrains on the
14641490
# xmldsig-core facilities. It explicitly dictates that enveloped signatures
14651491
# are the only signatures allowed. This means that:

src/saml2/xml/__init__.py

Whitespace-only changes.

src/saml2/xml/schema/__init__.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from importlib_resources import path as _resource_path
2+
3+
from xmlschema import XMLSchema as _XMLSchema
4+
from xmlschema.exceptions import XMLSchemaException as XMLSchemaError
5+
6+
import saml2.data.schemas as _data_schemas
7+
8+
9+
def _create_xml_schema_validator(source, **kwargs):
10+
kwargs = {
11+
**kwargs,
12+
"validation": "strict",
13+
"locations": _locations,
14+
"base_url": source,
15+
"allow": "sandbox",
16+
"use_fallback": False,
17+
}
18+
return _XMLSchema(source, **kwargs)
19+
20+
21+
with _resource_path(_data_schemas, "xml.xsd") as fp:
22+
_path_schema_xml = str(fp)
23+
with _resource_path(_data_schemas, "envelope.xsd") as fp:
24+
_path_schema_envelope = str(fp)
25+
with _resource_path(_data_schemas, "xenc-schema.xsd") as fp:
26+
_path_schema_xenc = str(fp)
27+
with _resource_path(_data_schemas, "xmldsig-core-schema.xsd") as fp:
28+
_path_schema_xmldsig_core = str(fp)
29+
with _resource_path(_data_schemas, "saml-schema-assertion-2.0.xsd") as fp:
30+
_path_schema_saml_assertion = str(fp)
31+
with _resource_path(_data_schemas, "saml-schema-metadata-2.0.xsd") as fp:
32+
_path_schema_saml_metadata = str(fp)
33+
with _resource_path(_data_schemas, "saml-schema-protocol-2.0.xsd") as fp:
34+
_path_schema_saml_protocol = str(fp)
35+
36+
_locations = {
37+
"http://www.w3.org/XML/1998/namespace": _path_schema_xml,
38+
"http://schemas.xmlsoap.org/soap/envelope/": _path_schema_envelope,
39+
"http://www.w3.org/2001/04/xmlenc#": _path_schema_xenc,
40+
"http://www.w3.org/2000/09/xmldsig#": _path_schema_xmldsig_core,
41+
"urn:oasis:names:tc:SAML:2.0:assertion": _path_schema_saml_assertion,
42+
"urn:oasis:names:tc:SAML:2.0:protocol": _path_schema_saml_protocol,
43+
}
44+
45+
with _resource_path(_data_schemas, "saml-schema-assertion-2.0.xsd") as fp:
46+
schema_saml_assertion = _create_xml_schema_validator(str(fp))
47+
with _resource_path(_data_schemas, "saml-schema-metadata-2.0.xsd") as fp:
48+
schema_saml_metadata = _create_xml_schema_validator(str(fp))
49+
with _resource_path(_data_schemas, "saml-schema-protocol-2.0.xsd") as fp:
50+
schema_saml_protocol = _create_xml_schema_validator(str(fp))
51+
52+
53+
node_to_schema = {
54+
# AssertionType
55+
"urn:oasis:names:tc:SAML:2.0:assertion:Assertion": schema_saml_assertion,
56+
# EntitiesDescriptorType
57+
"urn:oasis:names:tc:SAML:2.0:metadata:EntitiesDescriptor": schema_saml_metadata,
58+
# EntityDescriptorType
59+
"urn:oasis:names:tc:SAML:2.0:metadata:EntityDescriptor": schema_saml_metadata,
60+
# RequestAbstractType
61+
"urn:oasis:names:tc:SAML:2.0:protocol:AssertionIDRequest": schema_saml_protocol,
62+
"urn:oasis:names:tc:SAML:2.0:protocol:SubjectQuery": schema_saml_protocol,
63+
"urn:oasis:names:tc:SAML:2.0:protocol:AuthnRequest": schema_saml_protocol,
64+
"urn:oasis:names:tc:SAML:2.0:protocol:ArtifactResolve": schema_saml_protocol,
65+
"urn:oasis:names:tc:SAML:2.0:protocol:ManageNameIDRequest": schema_saml_protocol,
66+
"urn:oasis:names:tc:SAML:2.0:protocol:LogoutRequest": schema_saml_protocol,
67+
"urn:oasis:names:tc:SAML:2.0:protocol:NameIDMappingRequest": schema_saml_protocol,
68+
# StatusResponseType
69+
"urn:oasis:names:tc:SAML:2.0:protocol:Response": schema_saml_protocol,
70+
"urn:oasis:names:tc:SAML:2.0:protocol:ArtifactResponse": schema_saml_protocol,
71+
"urn:oasis:names:tc:SAML:2.0:protocol:ManageNameIDResponse": schema_saml_protocol,
72+
"urn:oasis:names:tc:SAML:2.0:protocol:LogoutResponse": schema_saml_protocol,
73+
"urn:oasis:names:tc:SAML:2.0:protocol:NameIDMappingResponse": schema_saml_protocol,
74+
}

tests/test_xsw.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
SIGNED_XSW_ASSERTION_EXTENSIONS = full_path("xsw/signed-xsw-assertion-extensions.xml")
1919
SIGNED_XSW_ASSERTION_ASSERTION = full_path("xsw/signed-xsw-assertion-assertion.xml")
2020

21+
SIGNED_ASSERTION_FIRST_SIG = full_path("xsw/signed-xsw-assertion-in-assertion-first-sig.xml")
22+
SIGNED_REPONSE_FIRST_SIG = full_path("xsw/signed-xsw-response-in-response-first-sig.xml")
2123

2224

2325
class TestXSW:
@@ -87,3 +89,42 @@ def test_signed_xsw_assertion_assertion_should_fail(self, mock_validate_on_or_af
8789

8890
assert self.ar.ava is None
8991
assert self.ar.name_id is None
92+
93+
94+
class TestInvalidDepthFirstSig:
95+
def setup_class(self):
96+
self.conf = config_factory("sp", dotname("server_conf"))
97+
self.ar = authn_response(self.conf, return_addrs="https://example.org/acs/post")
98+
99+
@patch('saml2.response.validate_on_or_after', return_value=True)
100+
def test_signed_assertion_first_sig_should_fail(self, mock_validate_on_or_after):
101+
self.ar.issue_instant_ok = Mock(return_value=True)
102+
103+
with open(SIGNED_ASSERTION_FIRST_SIG) as fp:
104+
xml_response = fp.read()
105+
106+
self.ar.outstanding_queries = {"id-abc": "http://localhost:8088/sso"}
107+
self.ar.timeslack = 10000
108+
self.ar.loads(xml_response, decode=False)
109+
110+
assert self.ar.came_from == 'http://localhost:8088/sso'
111+
assert self.ar.session_id() == "id-abc"
112+
assert self.ar.issuer() == 'urn:mace:example.com:saml:roland:idp'
113+
114+
with raises(SignatureError):
115+
self.ar.verify()
116+
117+
assert self.ar.ava is None
118+
assert self.ar.name_id is None
119+
120+
@patch('saml2.response.validate_on_or_after', return_value=True)
121+
def test_signed_response_first_sig_should_fail(self, mock_validate_on_or_after):
122+
self.ar.issue_instant_ok = Mock(return_value=True)
123+
124+
with open(SIGNED_REPONSE_FIRST_SIG) as fp:
125+
xml_response = fp.read()
126+
127+
self.ar.outstanding_queries = {"id-abc": "http://localhost:8088/sso"}
128+
self.ar.timeslack = 10000
129+
with raises(SignatureError):
130+
self.ar.loads(xml_response, decode=False)
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?xml version="1.0"?>
2+
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="the-response-id" InResponseTo="id-abc" Version="2.0" IssueInstant="2020-09-14T22:37:32Z" Destination="https://example.org/acs/post">
3+
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">urn:mace:example.com:saml:roland:idp</saml:Issuer>
4+
<samlp:Status xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
5+
<samlp:StatusCode xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
6+
</samlp:Status>
7+
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ID="attack-assertion-id" IssueInstant="2020-09-14T22:37:32Z" Version="2.0">
8+
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ID="the-assertion-id" IssueInstant="2020-09-14T22:37:32Z" Version="2.0">
9+
<saml:Issuer>urn:mace:example.com:saml:roland:idp</saml:Issuer>
10+
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
11+
<ds:SignedInfo>
12+
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
13+
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
14+
<ds:Reference URI="#the-assertion-id">
15+
<ds:Transforms>
16+
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
17+
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
18+
</ds:Transforms>
19+
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
20+
<ds:DigestValue>iLDF5/5VJs4sb3TasVTvFCsIi0k=</ds:DigestValue>
21+
</ds:Reference>
22+
</ds:SignedInfo>
23+
<ds:SignatureValue>Ked5gvNcRhHCivVN9y9+5LDAZLqLhRg3Sw2xlRR4HP2am1mFoBDdUx4khEWdcC2dknbzfo2AC1AtcbHTogDLOSLzYX9sT/gj995qotu4fUFQPMiocbCZRpbXTI6iDRiytwYtAkw28yQ4FVCe99GUThbV9tpLIoqMPZYNJ3TmL/I=</ds:SignatureValue>
24+
<ds:KeyInfo>
25+
<ds:X509Data>
26+
<ds:X509Certificate>MIICsDCCAhmgAwIBAgIJAJrzqSSwmDY9MA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMDkxMDA2MTk0OTQxWhcNMDkxMTA1MTk0OTQxWjBFMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJg2cms7MqjniT8Fi/XkNHZNPbNVQyMUMXE9tXOdqwYCA1cc8vQdzkihscQMXy3iPw2cMggBu6gjMTOSOxECkuvX5ZCclKr8pXAJM5cY6gVOaVO2PdTZcvDBKGbiaNefiEw5hnoZomqZGp8wHNLAUkwtH9vjqqvxyS/vclc6k2ewIDAQABo4GnMIGkMB0GA1UdDgQWBBRePsKHKYJsiojE78ZWXccK9K4aJTB1BgNVHSMEbjBsgBRePsKHKYJsiojE78ZWXccK9K4aJaFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAJrzqSSwmDY9MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAJSrKOEzHO7TL5cy6h3qh+3+JAk8HbGBW+cbX6KBCAw/mzU8flK25vnWwXS3dv2FF3Aod0/S7AWNfKib5U/SA9nJaz/mWeF9S0farz9AQFc8/NSzAzaVq7YbM4F6f6N2FRl7GikdXRCed45j6mrPzGzk3ECbupFnqyREH3+ZPSdk=</ds:X509Certificate>
27+
</ds:X509Data>
28+
</ds:KeyInfo>
29+
</ds:Signature>
30+
<saml:Subject>
31+
<saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">the-name-id</saml:NameID>
32+
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
33+
<saml:SubjectConfirmationData InResponseTo="id-abc" NotOnOrAfter="2020-09-14T22:47:32Z" Recipient="https://example.org/acs/post"/>
34+
</saml:SubjectConfirmation>
35+
</saml:Subject>
36+
<saml:Conditions NotBefore="2020-09-14T22:27:32Z" NotOnOrAfter="2020-09-14T22:47:32Z">
37+
<saml:AudienceRestriction>
38+
<saml:Audience>urn:mace:example.com:saml:roland:sp</saml:Audience>
39+
</saml:AudienceRestriction>
40+
</saml:Conditions>
41+
<saml:AuthnStatement AuthnInstant="2020-09-14T22:37:32Z" SessionIndex="id-sessidx">
42+
<saml:AuthnContext>
43+
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
44+
</saml:AuthnContext>
45+
</saml:AuthnStatement>
46+
</saml:Assertion>
47+
<saml:Issuer>urn:mace:example.com:saml:roland:idp</saml:Issuer>
48+
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
49+
<ds:SignedInfo>
50+
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
51+
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
52+
<ds:Reference URI="#attack-assertion-id">
53+
<ds:Transforms>
54+
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
55+
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
56+
</ds:Transforms>
57+
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
58+
<ds:DigestValue>dGhpcyBpcyBza2lwcGVkOyBvbmx5IHRoZSBmaXJzdCBzaWduYXR1cmUgaXMgcHJvY2Vzc2VkCg==</ds:DigestValue>
59+
</ds:Reference>
60+
</ds:SignedInfo>
61+
<ds:SignatureValue>dGhpcyBpcyBza2lwcGVkOyBvbmx5IHRoZSBmaXJzdCBzaWduYXR1cmUgaXMgcHJvY2Vzc2VkCg==</ds:SignatureValue>
62+
<ds:KeyInfo>
63+
<ds:X509Data>
64+
<ds:X509Certificate>MIICsDCCAhmgAwIBAgIJAJrzqSSwmDY9MA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMDkxMDA2MTk0OTQxWhcNMDkxMTA1MTk0OTQxWjBFMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJg2cms7MqjniT8Fi/XkNHZNPbNVQyMUMXE9tXOdqwYCA1cc8vQdzkihscQMXy3iPw2cMggBu6gjMTOSOxECkuvX5ZCclKr8pXAJM5cY6gVOaVO2PdTZcvDBKGbiaNefiEw5hnoZomqZGp8wHNLAUkwtH9vjqqvxyS/vclc6k2ewIDAQABo4GnMIGkMB0GA1UdDgQWBBRePsKHKYJsiojE78ZWXccK9K4aJTB1BgNVHSMEbjBsgBRePsKHKYJsiojE78ZWXccK9K4aJaFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAJrzqSSwmDY9MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAJSrKOEzHO7TL5cy6h3qh+3+JAk8HbGBW+cbX6KBCAw/mzU8flK25vnWwXS3dv2FF3Aod0/S7AWNfKib5U/SA9nJaz/mWeF9S0farz9AQFc8/NSzAzaVq7YbM4F6f6N2FRl7GikdXRCed45j6mrPzGzk3ECbupFnqyREH3+ZPSdk=</ds:X509Certificate>
65+
</ds:X509Data>
66+
</ds:KeyInfo>
67+
</ds:Signature>
68+
<saml:Subject>
69+
<saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">attack-name-id</saml:NameID>
70+
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
71+
<saml:SubjectConfirmationData InResponseTo="id-abc" NotOnOrAfter="2020-09-14T22:47:32Z" Recipient="https://example.org/acs/post"/>
72+
</saml:SubjectConfirmation>
73+
</saml:Subject>
74+
<saml:Conditions NotBefore="2020-09-14T22:27:32Z" NotOnOrAfter="2020-09-14T22:47:32Z">
75+
<saml:AudienceRestriction>
76+
<saml:Audience>urn:mace:example.com:saml:roland:sp</saml:Audience>
77+
</saml:AudienceRestriction>
78+
</saml:Conditions>
79+
<saml:AuthnStatement AuthnInstant="2020-09-14T22:37:32Z" SessionIndex="id-sessidx">
80+
<saml:AuthnContext>
81+
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
82+
</saml:AuthnContext>
83+
</saml:AuthnStatement>
84+
</saml:Assertion>
85+
</samlp:Response>

0 commit comments

Comments
 (0)