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=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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)