diff --git a/authentik/sources/saml/processors/response.py b/authentik/sources/saml/processors/response.py index 5f215d1f80..04f47d7cc1 100644 --- a/authentik/sources/saml/processors/response.py +++ b/authentik/sources/saml/processors/response.py @@ -143,9 +143,21 @@ class ResponseProcessor: if datetime.fromisoformat(on_or_after).replace(tzinfo=UTC) < _now: raise SAMLException("Assertion is not valid yet or expired.") - def _verify_signature(self, signature_node: _Element): - """Verify a single signature node""" - xmlsec.tree.add_ids(self._root, ["ID"]) + def _verify_signature(self, signature_node: _Element, target: _Element): + """Verify a single signature node against the given target element.""" + target_id = target.attrib.get("ID") + if not target_id: + raise InvalidSignature("Signed element is missing an ID attribute.") + refs = signature_node.xpath("./ds:SignedInfo/ds:Reference", namespaces=NS_MAP) + if len(refs) != 1: + raise InvalidSignature("Signature must contain exactly one Reference.") + ref_uri = refs[0].get("URI", "") + if ref_uri not in ("", f"#{target_id}"): + raise InvalidSignature( + "Signature Reference URI does not match the signed element's ID." + ) + + xmlsec.tree.add_ids(target, ["ID"]) ctx = xmlsec.SignatureContext() key = xmlsec.Key.from_memory( @@ -168,24 +180,22 @@ class ResponseProcessor: signature_nodes = self._root.xpath("/samlp:Response/ds:Signature", namespaces=NS_MAP) if len(signature_nodes) != 1: - raise InvalidSignature("No Signature exists in the Response element.") + raise InvalidSignature("Expected exactly one Signature in the Response element.") - self._verify_signature(signature_nodes[0]) + self._verify_signature(signature_nodes[0], self._root) def _verify_assertion_signature(self): """Verify SAML Assertion's Signature (after decryption)""" signature_nodes = self._root.xpath( "/samlp:Response/saml:Assertion/ds:Signature", namespaces=NS_MAP ) - if len(signature_nodes) != 1: - raise InvalidSignature("No Signature exists in the Assertion element.") + raise InvalidSignature("Expected exactly one signed Assertion in the Response.") + signature_node = signature_nodes[0] + assertion = signature_node.getparent() - self._verify_signature(signature_nodes[0]) - parent = signature_nodes[0].getparent() - if parent is None or parent.tag != f"{{{NS_SAML_ASSERTION}}}Assertion": - raise InvalidSignature("No Signature exists in the Assertion element.") - self._assertion = parent + self._verify_signature(signature_node, assertion) + self._assertion = assertion def _verify_request_id(self): if self._source.allow_idp_initiated: diff --git a/authentik/sources/saml/tests/fixtures/response_signed_assertion_uri_empty.xml b/authentik/sources/saml/tests/fixtures/response_signed_assertion_uri_empty.xml new file mode 100644 index 0000000000..eac54aa74a --- /dev/null +++ b/authentik/sources/saml/tests/fixtures/response_signed_assertion_uri_empty.xml @@ -0,0 +1,41 @@ +http://idp.example.com/metadata.phphttp://idp.example.com/metadata.php + + + + + + + + + +6AZlgEwsPEWwPPioj5HiNRStaPAm70zIRSBGnYaRc+E= + + +fei+JUaTHlQuy6/icjyZKUh5kx9qvL5xEqUfwOa1kiLtlfCbMCcEFHMEjePb1uPW +fAePa/v9mIl8pV7TrALI1xicdwRPvvM6xgiWe5hQDU+MKd88bHuU/O/0DUktku+e +ANR4kYQUAgkmmMCSPSruD3zIgVTAI8AMEpTtDNuHr8C12phsDkqaRQ1OP/ptC//2 +s6eeJ0DizMWkv/UHrqN8PSoTSgIl30Ffq30t/9TY644lBjgcQZD4h0uvpaRo/M0/ +yxcgfP6U3ec9ucePFJKprNXvkmNSh/DGbA0BPx1zoB7xf1nhyIYZI76GqC1NP0rh +YQ1BinW++XE19PvY66MnIA== + + +MIIC0jCCAbqgAwIBAgIUXHr2/LJAqtsJ4CcXkFjMwJo8HmQwDQYJKoZIhvcNAQEL +BQAwIzEhMB8GA1UEAwwYVVJJLWVtcHR5IGFzc2VydGlvbiB0ZXN0MB4XDTE0MDEw +MTAwMDAwMFoXDTMwMDEwMTAwMDAwMFowIzEhMB8GA1UEAwwYVVJJLWVtcHR5IGFz +c2VydGlvbiB0ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArzIB +E4WYhfFNJk2VcvxTX9+cdyJ+v+GCAU4BxSJ1/97BPf3UN4yob23qohnnnMlGAO2x +QBK2VBQKjNZo3qwy2t5xhsa0YLjjWGxAEL1s5K8cQlkYwkciTy+RFpMXmM80kk8p +ZdZFgrjf5ltincH4QuhJcN3/fsEibHQibymWeb/0I3Mba6Uh+gssMN82NYETl677 +I+5HV4wgJWMh1vaZFbid+YFBeWWJoIAIdbTQEAwIJriTA43lgbcK0Lo9A8/RCH1O +RVsQSkpA3kfs9yJ9AvKglBIThapR1iRgtVsC9LdiauHmiNU+8POSHXWByXaWOK0o +Izfg/lI+xKSWJerhUwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQA+/3kRE6jYFfgy +vHZdOX2cFOA5Y0N/RZmdt34tsfCiqP2vHtfQUje8gQAhjmV6dFk4wptPF1FsP601 +bMp+9LsRsb4y6pxy5m7xKVK9P/EI33N2zZZL9tlJ7CPIA81DPi53lYOvX54UIi2I +GpF6QyYMX2HTs/KVxo4gYnOnkyqPw6QrKaWpJLQndQd1rTn4/ybWW/9XU46RYf7/ +7Z8H8t4n4lhPYm5WGer4eG+k+F3R04yhwSm3Wi91gkQwQdGPgFSPe1z8TusDw/Q/ +1ax1a/mNoN9NCcZgg3L0xZgbtDnzBr/Gd/MWBQdDgRM7DpcWaVVcXq5GRLLeAvwM +uF73araE + + + +test_userhttp://sp.example.com/demo1/metadata.phpurn:oasis:names:tc:SAML:2.0:ac:classes:Password \ No newline at end of file diff --git a/authentik/sources/saml/tests/fixtures/response_signed_assertion_xsw3.xml b/authentik/sources/saml/tests/fixtures/response_signed_assertion_xsw3.xml new file mode 100644 index 0000000000..3dd436d159 --- /dev/null +++ b/authentik/sources/saml/tests/fixtures/response_signed_assertion_xsw3.xml @@ -0,0 +1,72 @@ + + http://idp.example.com/metadata.php + + + + + http://idp.example.com/metadata.php + + + zNDuGxwP4gVkv/Dzt7kiKo/4gzk=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 + + ATTACKER_FORGED_NAMEID + + + + + + + http://sp.example.com/demo1/metadata.php + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:Password + + + + + ATTACKER_FORGED + + + ATTACKER_FORGED + + + ATTACKER_FORGED + ATTACKER_FORGED + + + + + http://idp.example.com/metadata.php + + _ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7 + + + + + + + http://sp.example.com/demo1/metadata.php + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:Password + + + + + test + + + test@example.com + + + users + examplerole1 + + + + diff --git a/authentik/sources/saml/tests/fixtures/response_signed_assertion_xsw_nested.xml b/authentik/sources/saml/tests/fixtures/response_signed_assertion_xsw_nested.xml new file mode 100644 index 0000000000..cffb2902fa --- /dev/null +++ b/authentik/sources/saml/tests/fixtures/response_signed_assertion_xsw_nested.xml @@ -0,0 +1,35 @@ +http://idp.example.com/metadata.phphttp://idp.example.com/metadata.php + + + zNDuGxwP4gVkv/Dzt7kiKo/4gzk=GLP/vE8uxerB0uDpPslUgLPBL6ePQB619MoQ0I2Y5lAtFE6CB1zh8BnzChRx/bFjNy4byfOe8mFfM0r7WUi1PJOFWyUPoatdLl7wHHBIRTnPpYmu3Tb2Gz0sOP0F8wW7JkBft5gJfVw49nk5si9/3Q3o52jnJZ7dPtqfIOh8uNeopikK0HLF6sU05qCCtjcXfniEnLQFNBFMo9uY5GQqmR5n3nqPz1wYyyfFOAbVmGgBIoO2PfGX2GVLQhltc9qf2JMhks4jgZsZ8iLUIiH1lcLGWZEEs94k8k0P6gSv1uZ7Vbhksd/N9Jq9pCVuEJ/jRPcAdVjzbxqKQAj6ELwr8O6fepTzA+CAdwEolBnx/C6TmSbVZ+IWk6QUGe4x4+IAukC+0hkKENlO0ELOScksvyhpgHbxNA4rp+DhGupCaO/I2RrsQkmvavbqm+wSEspK7scK112SDunjDvqPHsPYgukD33T/97PxTLorg2kKP9HHJwPJKoXXeyOGcA6vwK+RqrAlZ2dLGAgcXo+sJcdCLuvxDNz9VXofBjBZIKVKdmYhm0QJaPYHtuQsAyFavQhdOBOmGHb7QX3YE3Xy4dX4LymtT+Jlb1I4FJSht/9HUIHW1FdhfDak4f7gUgjuMamMddLD0jVgeESupSREzFv/gj2IrctkbgjAO0iuuiBgKMg= +MIIFUzCCAzugAwIBAgIRAL6tbNcE9Ej9gNlbGKswfFMwDQYJKoZIhvcNAQELBQAwHTEbMBkGA1UEAwwSYXV0aGVudGlrIDIwMjUuNi4zMB4XDTI1MDcxNTE4MDQzNloXDTI2MDcxNjE4MDQzNlowVjEqMCgGA1UEAwwhYXV0aGVudGlrIFNlbGYtc2lnbmVkIENlcnRpZmljYXRlMRIwEAYDVQQKDAlhdXRoZW50aWsxFDASBgNVBAsMC1NlbGYtc2lnbmVkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjmut/+bBRLlyrbf+WIfg8ZTw9t6VnsiU1n04nPTulpRAz4nBOoOHNRIruSpZyFeFa6x9jwn4Ma5EFUH7HqnRvhoujm8U17OglXWZt0DLCZ6S5xPmdMogFXjJDmg9okIcI/cb9VbR6I8uvm1oiaOWCr36RTiqZ6rmdjQcuUPLr1+V/LxWQI463S+5QA2HZxAGalp45MJAz2sa9iczktKMgyYlfjj1cruFARxxeheu5qIK7aQWfyPj1QlMb9mi4VQaxUwGrAui4Tq614ivRJY2SkZb0Aq/LLSQoQWYHtYyQIasrOXJm0JuPDqhINPBDowyhu8DihC3uzOpmTXLKc5UoIQk+Q1h5iH74A3/kxOJUw13FXzRiDxC/yGthPYLyFHsDiJolscMKSCqlDvEMcpM4mxFeud9sKUb71SZr8sqmJl3qtvZmKpkR4y8pN2c00p10t0htqONmr5kyPxmhz0HCrosiPYB4olNjaydKviNTtPJ7TtnPyeA3iXGzCP1e80XzUoJrDqON5/GcpYgqsP/kGj8Qvqesa4Fez+1+5pAGHN2VzQbkHAgK3s4YRXrGLTs7wg27F9T0RE28Mm0RYBkYpdp4/5PuTTulthB9mkUBSJMgENmQAYkapvonFDsJkTi39qnsddbZusOLT4z3hsA38eFEwRqnbNZVUGPIp/O1SsCAwEAAaNVMFMwUQYDVR0RAQH/BEcwRYJDRUZ4QXVLRzV6SlVUTWpWNTJoMkRJMUQ5MXdLblZKaXFwNmpwRTRTTy5zZWxmLXNpZ25lZC5nb2F1dGhlbnRpay5pbzANBgkqhkiG9w0BAQsFAAOCAgEAYLThxDVpA1OIAVK/buueRJExIWr6y4s6NtpuR8UQEcfq5hfoc4zMFGHR5+u1WFIb5siK25xh/OnS7bLdLic6AkjZSrx91+0v2Jn9gfUqbs5AJ040XzAAdx/Mb4s0+537yhB+/JXPylR1QxhGbO7koXQ5JDhAXWKCw2O1C+80mN8dbhQvDkEtsXrHrtXclcqf2TT89XAzc5HAC8NmP4SF+FafAREQB1KdaG4QAbc/gnjsX2YJD89SDL+3jMp6F7R1Ym+bWt5oWqx2tkm6HGXd3fbpfQlnfrRN60tMjjLmw1cDMhOhpdragY5zokniEUL2pKVtrxFp7V1ZpoMI0Kt5MKkOXrezi542NWSgkGehlsDLD9wtuCNem2arR0mNnMLdYkMG7G0dpAq3Tl32dgfMfyKnNyE2O/6/EeEuzUH2NfTU1p7AUQfLrf4rtNcJEs9OAPuC9vy7w9YEpF997T+FhR2Ub1C423NQj4bwlS/9f7MIBkSi1EgnQuiSGB5epxAKI3oOVrmzOpTuvr6wZXV9pM3zdfbcoGuFWP6Ix7W8G5vg+0WvoSjc2fwGXYlidEK3xlQSMAaQ4CMClpPsKLScRq1nrQGzPYoiL1DYubsOWx9ohll6+jNjKI6f79WwbHYrW4EeRIOz38+m46EDjAWZBMgrE7J/3DhgeLEVJYBA5K0=FORGED_VICTIMhttp://sp.example.com/demo1/metadata.php + http://idp.example.com/metadata.php + + _ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7 + + + + + + + http://sp.example.com/demo1/metadata.php + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:Password + + + + + test + + + test@example.com + + + users + examplerole1 + + + \ No newline at end of file diff --git a/authentik/sources/saml/tests/fixtures/response_signed_response_uri_empty.xml b/authentik/sources/saml/tests/fixtures/response_signed_response_uri_empty.xml new file mode 100644 index 0000000000..702af0d307 --- /dev/null +++ b/authentik/sources/saml/tests/fixtures/response_signed_response_uri_empty.xml @@ -0,0 +1,40 @@ +http://idp.example.com/metadata.php + + + + + + + + + +h6La1VQZ6VO/Bi/a3g2Uhkp27kV1ijJvIB6xlgLGu7M= + + +ItqDe3xfN9sN44z9pudYI+GdVEz7MnLXvG3+afjS8ws52c8fUryoK0lgH3l0i3mY +cFKiQwiuRx86AB6uGH13ToOoXq3peK911PeGZ/fCt15x14x6v69r9vG+Mw4KAc+y +7J/a3BOltEoU9SfNHBAIkEgbHP35bF5iYSVlIfLho8BRCVT4rMv/K/n65Hd88Fas +Es2oxS95hYUf1of9pHDOuvEYM+VpMZw3OuMci7X8ZszCxdJbgP6D4m7w9wWBbAAi +xWWuLnU2sFkLNeBh5KuumLNLG1Dreqiz0d4/tu+P9F4NcP2FAdG79/Y+Ga9d0Wf7 +yf05WcieZgV4hhHNwQcauw== + + +MIICvjCCAaagAwIBAgIUGjmjYk5z7ya8VH2Faah5vvVmEHIwDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAwwOVVJJLWVtcHR5IHRlc3QwHhcNMTQwMTAxMDAwMDAwWhcN +MzAwMTAxMDAwMDAwWjAZMRcwFQYDVQQDDA5VUkktZW1wdHkgdGVzdDCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAKEuJi9Gny9OfQScZEG1R9Gy5pqbM7it +pGda6gm4PXuFQisMapdg0DwO3odY1grTQE6gFEjSCYtEoGyuJMSUNdILvc7sfCWa +QkuXCnkx7nXgyiHLNuhgkhw6kDXgRMzdaI/B7IlZkZYnfyXWi6A6lQ6dLJRU1p0o +C+JawKngvmOAAKvhaC1keEbfUh//8UhK/YkjJShV1c0ijFlucj4eYDQyf9x9lYw7 +oukjnVFKmhqiRO/SxwhYjYbyTppVRzC4sCJz0KRn4qwnGV5x2+jDqUvFUxJc5W7f +N+utrfNWeszmP5Elw+ezIsq+3D9ss+nHmtXIDxVNA3dp8tZQKzQ4WzcCAwEAATAN +BgkqhkiG9w0BAQsFAAOCAQEART2PW/rXPU5d+NTJnP2mdIi5Ft01gpaRgY1iBsFm ++D6zXT/k9wr56GEDLWUf2qOJeXsNc+f9FKI0BCVb/5vVoB60AAMHKbb+NxwoXLHi +dT6DaEPK1VGdEBnpL5uOII3pO42FMPewBUNuoUmb9zipyPoL6zbc7khRcBBIYMlr +05Y2tqJqECNAtGQ9+v5CBcECP3QI4L0UmCJMwpj7XH5TrfKfLPZuUEvQaET63dXb +ioi+P6KEpuxblOL0Uj2e2erhJYavqCnoxt+0eUDDBsrwuk7/sRtbBO0XjkgEtJZ8 +n3OS74cjqIwTx+PqObGThECnyBYwONY7RWU5G4r6kqMXBg== + + + +http://idp.example.com/metadata.phptest_userhttp://sp.example.com/demo1/metadata.phpurn:oasis:names:tc:SAML:2.0:ac:classes:Password \ No newline at end of file diff --git a/authentik/sources/saml/tests/fixtures/signature_cert_assertion_uri_empty.pem b/authentik/sources/saml/tests/fixtures/signature_cert_assertion_uri_empty.pem new file mode 100644 index 0000000000..0d76d6922c --- /dev/null +++ b/authentik/sources/saml/tests/fixtures/signature_cert_assertion_uri_empty.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC0jCCAbqgAwIBAgIUXHr2/LJAqtsJ4CcXkFjMwJo8HmQwDQYJKoZIhvcNAQEL +BQAwIzEhMB8GA1UEAwwYVVJJLWVtcHR5IGFzc2VydGlvbiB0ZXN0MB4XDTE0MDEw +MTAwMDAwMFoXDTMwMDEwMTAwMDAwMFowIzEhMB8GA1UEAwwYVVJJLWVtcHR5IGFz +c2VydGlvbiB0ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArzIB +E4WYhfFNJk2VcvxTX9+cdyJ+v+GCAU4BxSJ1/97BPf3UN4yob23qohnnnMlGAO2x +QBK2VBQKjNZo3qwy2t5xhsa0YLjjWGxAEL1s5K8cQlkYwkciTy+RFpMXmM80kk8p +ZdZFgrjf5ltincH4QuhJcN3/fsEibHQibymWeb/0I3Mba6Uh+gssMN82NYETl677 +I+5HV4wgJWMh1vaZFbid+YFBeWWJoIAIdbTQEAwIJriTA43lgbcK0Lo9A8/RCH1O +RVsQSkpA3kfs9yJ9AvKglBIThapR1iRgtVsC9LdiauHmiNU+8POSHXWByXaWOK0o +Izfg/lI+xKSWJerhUwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQA+/3kRE6jYFfgy +vHZdOX2cFOA5Y0N/RZmdt34tsfCiqP2vHtfQUje8gQAhjmV6dFk4wptPF1FsP601 +bMp+9LsRsb4y6pxy5m7xKVK9P/EI33N2zZZL9tlJ7CPIA81DPi53lYOvX54UIi2I +GpF6QyYMX2HTs/KVxo4gYnOnkyqPw6QrKaWpJLQndQd1rTn4/ybWW/9XU46RYf7/ +7Z8H8t4n4lhPYm5WGer4eG+k+F3R04yhwSm3Wi91gkQwQdGPgFSPe1z8TusDw/Q/ +1ax1a/mNoN9NCcZgg3L0xZgbtDnzBr/Gd/MWBQdDgRM7DpcWaVVcXq5GRLLeAvwM +uF73araE +-----END CERTIFICATE----- diff --git a/authentik/sources/saml/tests/fixtures/signature_cert_uri_empty.pem b/authentik/sources/saml/tests/fixtures/signature_cert_uri_empty.pem new file mode 100644 index 0000000000..e6ffd32b0c --- /dev/null +++ b/authentik/sources/saml/tests/fixtures/signature_cert_uri_empty.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICvjCCAaagAwIBAgIUGjmjYk5z7ya8VH2Faah5vvVmEHIwDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAwwOVVJJLWVtcHR5IHRlc3QwHhcNMTQwMTAxMDAwMDAwWhcN +MzAwMTAxMDAwMDAwWjAZMRcwFQYDVQQDDA5VUkktZW1wdHkgdGVzdDCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAKEuJi9Gny9OfQScZEG1R9Gy5pqbM7it +pGda6gm4PXuFQisMapdg0DwO3odY1grTQE6gFEjSCYtEoGyuJMSUNdILvc7sfCWa +QkuXCnkx7nXgyiHLNuhgkhw6kDXgRMzdaI/B7IlZkZYnfyXWi6A6lQ6dLJRU1p0o +C+JawKngvmOAAKvhaC1keEbfUh//8UhK/YkjJShV1c0ijFlucj4eYDQyf9x9lYw7 +oukjnVFKmhqiRO/SxwhYjYbyTppVRzC4sCJz0KRn4qwnGV5x2+jDqUvFUxJc5W7f +N+utrfNWeszmP5Elw+ezIsq+3D9ss+nHmtXIDxVNA3dp8tZQKzQ4WzcCAwEAATAN +BgkqhkiG9w0BAQsFAAOCAQEART2PW/rXPU5d+NTJnP2mdIi5Ft01gpaRgY1iBsFm ++D6zXT/k9wr56GEDLWUf2qOJeXsNc+f9FKI0BCVb/5vVoB60AAMHKbb+NxwoXLHi +dT6DaEPK1VGdEBnpL5uOII3pO42FMPewBUNuoUmb9zipyPoL6zbc7khRcBBIYMlr +05Y2tqJqECNAtGQ9+v5CBcECP3QI4L0UmCJMwpj7XH5TrfKfLPZuUEvQaET63dXb +ioi+P6KEpuxblOL0Uj2e2erhJYavqCnoxt+0eUDDBsrwuk7/sRtbBO0XjkgEtJZ8 +n3OS74cjqIwTx+PqObGThECnyBYwONY7RWU5G4r6kqMXBg== +-----END CERTIFICATE----- diff --git a/authentik/sources/saml/tests/test_response.py b/authentik/sources/saml/tests/test_response.py index fbf3542e62..c2029bb44d 100644 --- a/authentik/sources/saml/tests/test_response.py +++ b/authentik/sources/saml/tests/test_response.py @@ -196,10 +196,122 @@ class TestResponseProcessor(TestCase): self.assertNotEqual(parser._get_name_id()[1], "bad") self.assertEqual(parser._get_name_id()[1], "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7") - @freeze_time("2022-10-14T14:15:00") + @freeze_time("2014-07-17T01:02:18Z") + def test_verification_assertion_xsw_nested_duplicate_id(self): + """Nested-duplicate-ID XSW: a forged outer Assertion shares its ID with a + nested copy of the original signed Assertion (placed inside ), + so the Signature's Reference URI (#ORIG_ID) matches the outer Assertion's + ID *and* dereferences to legitimately-signed content. Must be rejected.""" + key = load_fixture("fixtures/signature_cert.pem") + kp = CertificateKeyPair.objects.create( + name=generate_id(), + certificate_data=key, + ) + self.source.verification_kp = kp + self.source.signed_assertion = True + self.source.signed_response = False + request = self.factory.post( + "/", + data={ + "SAMLResponse": b64encode( + load_fixture("fixtures/response_signed_assertion_xsw_nested.xml").encode() + ).decode() + }, + ) + + parser = ResponseProcessor(self.source, request) + with self.assertRaises(InvalidSignature): + parser.parse() + + @freeze_time("2014-07-17T01:02:18Z") + def test_verification_response_uri_empty(self): + """Some real-world IdPs (notably some Okta dev-tenant configurations + observed in the gosaml2 testdata corpus at saml.oktadev.com) sign the + Response with ds:Reference URI="" instead of URI="#". Per xmldsig + §4.4.3.2, URI="" covers the entire enclosing document via the + enveloped-signature transform — strictly more attested content than + "#" — so consuming the target is a subset of what was signed.""" + key = load_fixture("fixtures/signature_cert_uri_empty.pem") + kp = CertificateKeyPair.objects.create( + name=generate_id(), + certificate_data=key, + ) + self.source.verification_kp = kp + self.source.signed_response = True + self.source.signed_assertion = False + request = self.factory.post( + "/", + data={ + "SAMLResponse": b64encode( + load_fixture("fixtures/response_signed_response_uri_empty.xml").encode() + ).decode() + }, + ) + + parser = ResponseProcessor(self.source, request) + parser.parse() + + @freeze_time("2014-07-17T01:02:18Z") + def test_verification_assertion_uri_empty(self): + """Symmetric to test_verification_response_uri_empty but for an + Assertion-level signature: the same xmldsig "this document" semantics + still cover the whole enclosing document, so the Assertion we then + consume is part of the attested content. We have no real-world IdP + samples emitting this configuration, but the pre-fix code accepted it + and the cryptographic guarantee holds, so keep accepting it rather + than risk breaking an IdP we haven't sampled.""" + key = load_fixture("fixtures/signature_cert_assertion_uri_empty.pem") + kp = CertificateKeyPair.objects.create( + name=generate_id(), + certificate_data=key, + ) + self.source.verification_kp = kp + self.source.signed_assertion = True + self.source.signed_response = False + request = self.factory.post( + "/", + data={ + "SAMLResponse": b64encode( + load_fixture("fixtures/response_signed_assertion_uri_empty.xml").encode() + ).decode() + }, + ) + + parser = ResponseProcessor(self.source, request) + parser.parse() + + @freeze_time("2014-07-17T01:02:18Z") + def test_verification_assertion_xsw3(self): + """XSW-3 (signature relocation): a forged Assertion contains a Signature whose + ds:Reference URI points to a second Assertion in the document. The signature + verifies (because the digest matches the legitimate referenced Assertion), + but the verifier must NOT then consume the forged Assertion as if it were + signed.""" + key = load_fixture("fixtures/signature_cert.pem") + kp = CertificateKeyPair.objects.create( + name=generate_id(), + certificate_data=key, + ) + self.source.verification_kp = kp + self.source.signed_assertion = True + self.source.signed_response = False + request = self.factory.post( + "/", + data={ + "SAMLResponse": b64encode( + load_fixture("fixtures/response_signed_assertion_xsw3.xml").encode() + ).decode() + }, + ) + + parser = ResponseProcessor(self.source, request) + with self.assertRaises(InvalidSignature): + parser.parse() + + @freeze_time("2014-07-17T01:02:18Z") def test_name_id_comment(self): """Test comment in name ID""" - fixture = load_fixture("fixtures/response_signed_assertion_dup.xml") + fixture = load_fixture("fixtures/response_signed_assertion.xml") fixture = fixture.replace( "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7", "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7", diff --git a/locale/en/dictionaries/software-terms.txt b/locale/en/dictionaries/software-terms.txt index b6571992c3..51f3d0ba30 100644 --- a/locale/en/dictionaries/software-terms.txt +++ b/locale/en/dictionaries/software-terms.txt @@ -93,6 +93,7 @@ frie gcsp geoip glpi +gosaml grecaptcha guac guacd @@ -113,6 +114,7 @@ microsoft mmdb noopener noreferrer +oktadev openidc ouia ouid diff --git a/website/docs/security/cves/CVE-2026-47201.md b/website/docs/security/cves/CVE-2026-47201.md new file mode 100644 index 0000000000..611c9491f0 --- /dev/null +++ b/website/docs/security/cves/CVE-2026-47201.md @@ -0,0 +1,31 @@ +# CVE-2026-47201 + +## XML Signature Wrapping in SAML Source ACS allows authentication as arbitrary federated user + +### Summary + +authentik's SAML Source ACS endpoint is vulnerable to XML Signature Wrapping when validating upstream SAML responses. An attacker with any account at the upstream IdP can reuse a valid signed assertion to authenticate as another federated user. + +### Patches + +authentik 2026.5.1, 2026.2.4 and 2025.12.6 fix this issue. + +### Impact + +Affected: authentik deployments using a SAML Source for upstream SAML federation with signed assertions, or signed responses without signed assertions. Not affected: deployments that do not use SAML Source for upstream SAML federation. + +The SAML Source trusts that the verified XML signature belongs to the assertion or response that authentik later consumes. A crafted SAML response can make signature verification succeed against the attacker's original signed assertion while authentik reads identity data from a different forged assertion. + +An attacker first completes a legitimate login to the upstream IdP and captures the signed SAML response sent through their browser. They then submit a modified response to the ACS endpoint where the valid signature still verifies, but the consumed assertion contains a victim identifier or attacker-chosen attributes. + +The attacker can authenticate as a victim who has previously used the SAML Source, or as a local user matched by forged email or username when those matching modes are enabled. + +### Workarounds + +Disable affected SAML Sources, or block access to their ACS endpoints. + +### For more information + +If you have any questions or comments about this advisory: + +- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)