VYPR
High severity8.7NVD Advisory· Published May 21, 2026

CVE-2026-40165

CVE-2026-40165

Description

authentik is an open-source identity provider. Versions 2025.12.4 and prior, and versions 2026.2.0-rc1 through 2026.2.2 were vulnerable to Authentication Bypass through SAML NameID XML Comment Injection. Due to how authentik extracted the NameID value from a SAML assertion, it was possible for an attacker to trick authentik into only seeing a part of the NameID value, potentially allowing an attacker to gain access to other accounts. This issue could be exploited on an authentik instance with a SAML Source, where the attacker had an account on the SAML Source and the ability to modify their NameID value (commonly username or E-mail), and XML Signing was enabled. The attacker could modify the SAML assertion given to authentik by injecting a comment within the NameID value, which effectively truncated the NameID value to the snippet before the comment, and gave the attacker access to any user account. This issue has been fixed in versions 2025.12.5 and 2026.2.3.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Authentik SAML NameID XML comment injection allows authentication bypass by truncating the identifier, letting an attacker impersonate any user.

Vulnerability

Authentik versions 2025.12.4 and prior, and 2026.2.0-rc1 through 2026.2.2 are vulnerable to Authentication Bypass through SAML NameID XML Comment Injection. The flaw resides in how authentik extracts the NameID value from a SAML assertion. By injecting an XML comment within the NameID value, an attacker can truncate the identifier, causing authentik to only see the part before the comment [3].

Exploitation

Exploitation requires an authentik instance with a SAML Source, the attacker must have an account on that SAML Source and the ability to modify their NameID value (commonly username or email), and XML Signing must be enabled. The attacker crafts a SAML assertion with a comment injected inside the NameID value (e.g., victim<!--comment-->@attacker.com), which effectively truncates the identifier to the snippet before the comment, allowing the attacker to impersonate any user account [3].

Impact

On successful exploitation, the attacker gains access to another user's account within authentik. This is a high-severity authentication bypass that leads to unauthorized access and potential privilege escalation, depending on the target account's permissions [3].

Mitigation

The issue is fixed in authentik versions 2025.12.5 and 2026.2.3, released on 2026-05-21 [1][2]. For users unable to upgrade, a workaround is to create a SAML Source property mapping with the expression if name_id.text != "".join(name_id.itertext()): raise ValueError("Mismatched NameID") and apply it to all SAML Sources [3].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2
  • authentik/authentikinferred2 versions
    <=2025.12.4 || >=2026.2.0-rc1,<=2026.2.2+ 1 more
    • (no CPE)range: <=2025.12.4 || >=2026.2.0-rc1,<=2026.2.2
    • (no CPE)range: <=2025.12.4, >=2026.2.0-rc1 <=2026.2.2

Patches

1
47dec5c6b7fb

internal: Automated internal backport: CVE-2026-40165.sec.patch to authentik-2025.12 (#22275)

https://github.com/goauthentik/authentikauthentik-automation[bot]May 12, 2026via nvd-ref
3 files changed · +75 20
  • authentik/sources/saml/processors/response.py+15 18 modified
    @@ -231,12 +231,12 @@ def _handle_name_id_transient(self) -> SourceFlowManager:
             user has an attribute that refers to our Source for cleanup. The user is also deleted
             on logout and periodically."""
             # Create a temporary User
    -        name_id = self._get_name_id()
    +        name_id_el, name_id = self._get_name_id()
             expiry = mktime(
                 (now() + timedelta_from_string(self._source.temporary_user_delete_after)).timetuple()
             )
             user: User = User.objects.create(
    -            username=name_id.text,
    +            username=name_id,
                 attributes={
                     USER_ATTRIBUTE_GENERATED: True,
                     USER_ATTRIBUTE_SOURCES: [
    @@ -247,20 +247,18 @@ def _handle_name_id_transient(self) -> SourceFlowManager:
                 },
                 path=self._source.get_user_path(),
             )
    -        LOGGER.debug("Created temporary user for NameID Transient", username=name_id.text)
    +        LOGGER.debug("Created temporary user for NameID Transient", username=name_id)
             user.set_unusable_password()
             user.save()
    -        UserSAMLSourceConnection.objects.create(
    -            source=self._source, user=user, identifier=name_id.text
    -        )
    +        UserSAMLSourceConnection.objects.create(source=self._source, user=user, identifier=name_id)
             return SAMLSourceFlowManager(
                 source=self._source,
                 request=self._http_request,
    -            identifier=str(name_id.text),
    +            identifier=str(name_id),
                 user_info={
                     "root": self._root,
                     "assertion": self.get_assertion(),
    -                "name_id": name_id,
    +                "name_id": name_id_el,
                 },
                 policy_context={},
             )
    @@ -271,7 +269,7 @@ def get_assertion(self) -> _Element | None:
                 return self._assertion
             return self._root.find(f"{{{NS_SAML_ASSERTION}}}Assertion")
     
    -    def _get_name_id(self) -> _Element:
    +    def _get_name_id(self) -> tuple[_Element, str]:
             """Get NameID Element"""
             assertion = self.get_assertion()
             if assertion is None:
    @@ -282,12 +280,11 @@ def _get_name_id(self) -> _Element:
             name_id = subject.find(f"{{{NS_SAML_ASSERTION}}}NameID")
             if name_id is None:
                 raise ValueError("NameID element not found")
    -        return name_id
    +        return name_id, "".join(name_id.itertext())
     
         def _get_name_id_filter(self) -> dict[str, str]:
             """Returns the subject's NameID as a Filter for the `User`"""
    -        name_id_el = self._get_name_id()
    -        name_id = name_id_el.text
    +        name_id_el, name_id = self._get_name_id()
             if not name_id:
                 raise UnsupportedNameIDFormat("Subject's NameID is empty.")
             _format = name_id_el.attrib["Format"]
    @@ -308,26 +305,26 @@ def _get_name_id_filter(self) -> dict[str, str]:
     
         def prepare_flow_manager(self) -> SourceFlowManager:
             """Prepare flow plan depending on whether or not the user exists"""
    -        name_id = self._get_name_id()
    +        name_id_el, name_id = self._get_name_id()
             # Sanity check, show a warning if NameIDPolicy doesn't match what we go
    -        if self._source.name_id_policy != name_id.attrib["Format"]:
    +        if self._source.name_id_policy != name_id_el.attrib["Format"]:
                 LOGGER.warning(
                     "NameID from IdP doesn't match our policy",
                     expected=self._source.name_id_policy,
    -                got=name_id.attrib["Format"],
    +                got=name_id_el.attrib["Format"],
                 )
             # transient NameIDs are handled separately as they don't have to go through flows.
    -        if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_TRANSIENT:
    +        if name_id_el.attrib["Format"] == SAML_NAME_ID_FORMAT_TRANSIENT:
                 return self._handle_name_id_transient()
     
             return SAMLSourceFlowManager(
                 source=self._source,
                 request=self._http_request,
    -            identifier=str(name_id.text),
    +            identifier=name_id,
                 user_info={
                     "root": self._root,
                     "assertion": self.get_assertion(),
    -                "name_id": name_id,
    +                "name_id": name_id_el,
                 },
                 policy_context={
                     "saml_response": etree.tostring(self._root),
    
  • authentik/sources/saml/tests/test_response.py+27 2 modified
    @@ -193,8 +193,33 @@ def test_verification_assertion_duplicate(self):
     
             parser = ResponseProcessor(self.source, request)
             parser.parse()
    -        self.assertNotEqual(parser._get_name_id().text, "bad")
    -        self.assertEqual(parser._get_name_id().text, "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7")
    +        self.assertNotEqual(parser._get_name_id()[1], "bad")
    +        self.assertEqual(parser._get_name_id()[1], "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7")
    +
    +    @freeze_time("2022-10-14T14:15:00")
    +    def test_name_id_comment(self):
    +        """Test comment in name ID"""
    +        fixture = load_fixture("fixtures/response_signed_assertion_dup.xml")
    +        fixture = fixture.replace(
    +            "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7",
    +            "_ce3d2948b4cf20146dee0a0b3dd6f<!--x-->69b6cf86f62d7",
    +        )
    +        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(fixture.encode()).decode()},
    +        )
    +
    +        parser = ResponseProcessor(self.source, request)
    +        parser.parse()
    +        self.assertEqual(parser._get_name_id()[1], "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7")
     
         @freeze_time("2014-07-17T01:02:18Z")
         def test_verification_response(self):
    
  • website/docs/security/cves/CVE-2026-40165.md+33 0 added
    @@ -0,0 +1,33 @@
    +# CVE-2026-40165
    +
    +_Reported by [@kodareef5](https://github.com/kodareef5), [@Android-Login-Analysis](https://github.com/Android-Login-Analysis), and [@AyushParkara](https://github.com/AyushParkara)_
    +
    +## SAML Source Fails to Validate Assertion Conditions
    +
    +### Summary
    +
    +Due to how authentik used to extract the NameID value from a SAML assertion, it was possible for an attacker to trick authentik into only seeing a part of the NameID value, potentially allowing an attacker to get access to other accounts.
    +
    +### Patches
    +
    +authentik 2025.12.5 and 2026.2.3 fix this issue, for other versions the workaround below can be used.
    +
    +### Impact
    +
    +This issue can be exploited given an authentik instance with a SAML Source, where the attacker has an account on the SAML Source and the ability to modify their NameID value (commonly username or E-mail), and XML Signing is enabled. The attacker can modify the SAML assertion given to authentik by injecting a comment within the NameID value, which effectively truncates the NameID value to the snippet before the comment, and give the attack access to any user account.
    +
    +### Workarounds
    +
    +Create a SAML Source property mapping with the following expression and add it to all SAML Sources:
    +
    +```python
    +if name_id.text != "".join(name_id.itertext()):
    +  raise ValueError("Mismatched NameID")
    +return {}
    +```
    +
    +### For more information
    +
    +If you have any questions or comments about this advisory:
    +
    +- Email us at [security@goauthentik.io](mailto:security@goauthentik.io).
    

Vulnerability mechanics

Root cause

"The SAML response parser used `name_id.text` to extract the NameID value, which only returns the text before the first XML comment, allowing an attacker to truncate the identifier by injecting an XML comment."

Attack vector

An attacker who has an account on a SAML Identity Provider (IdP) configured as a SAML Source in authentik, and who can modify their NameID value (commonly username or email), can inject an XML comment (e.g., `<!--x-->`) into the NameID. Because authentik's original code used `name_id.text` to extract the identifier, the comment causes the parser to return only the text preceding the comment, effectively truncating the NameID. The attacker then crafts a SAML assertion with a NameID that matches another user's identifier up to the comment boundary, and when authentik processes the assertion, it authenticates the attacker as that other user. This requires XML Signing to be enabled on the SAML Source so the assertion is accepted as valid [CWE-287][CWE-436].

Affected code

The vulnerable code is in `authentik/sources/saml/processors/response.py` in the `_get_name_id()` method and all callers (`_handle_name_id_transient`, `_get_name_id_filter`, `prepare_flow_manager`). These methods used `name_id.text` to extract the NameID value, which stops at the first XML comment node.

What the fix does

The patch changes `_get_name_id()` to return a tuple `(name_id_el, name_id)` where `name_id` is computed via `"".join(name_id.itertext())` instead of `name_id.text`. The `itertext()` method concatenates all text nodes including those after XML comments, so the full NameID value is recovered regardless of injected comments. All call sites are updated to use the new return value, and the identifier passed to user lookup and creation now uses the complete, un-truncated string [patch_id=1029353]. A corresponding test (`test_name_id_comment`) was added to verify that a NameID containing a comment is correctly parsed to its full value.

Preconditions

  • configauthentik instance must have a SAML Source configured
  • authAttacker must have an account on the SAML Identity Provider (IdP)
  • inputAttacker must be able to modify their NameID value on the IdP (commonly username or email)
  • configXML Signing must be enabled on the SAML Source (signed_assertion or signed_response)

Generated on May 21, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

3

News mentions

0

No linked articles in our index yet.