CVE-2023-39522 (published 2023-08-29T18:15Z)

goauthentik is an open-source Identity Provider. In affected versions using a recovery flow with an identification stage an attacker is able to determine if a username exists. Only setups configured with a recovery flow are impacted by this. Anyone with a user account on a system with the recovery flow described above is susceptible to having their username/email revealed as existing. An attacker can easily enumerate and check users' existence using the recovery flow, as a clear message is shown when a user doesn't exist. Depending on configuration this can either be done by username, email, or both. This issue has been addressed in versions 2023.5.6 and 2023.6.2. Users are advised to upgrade. There are no known workarounds for this issue.

CVSS v3 base score: 5.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N)

Weakness information:
cwe_id: CWE-203
cwe_name: Observable Discrepancy
description: The product behaves differently or sends different responses under different circumstances in a way that is observable to an unauthorized actor, which exposes security-relevant information about the state of the product, such as whether a particular operation was successful or not.
extended_description: Discrepancies can take many forms, and variations may be detectable in timing, control flow, communications such as replies or requests, or general behavior. These discrepancies can reveal information about the product's operation or internal state to an unauthorized actor. In some cases, discrepancies can be used by attackers to form a side channel.
         

Fix information:
repo_url: https://github.com/goauthentik/authentik
commit_details: repo_url: https://github.com/goauthentik/authentik
author: Jens L
author_date: 2023-08-29 19:07:49+02:00
msg: security: fix CVE-2023-39522 (#6665)

* stages/email: don't disclose whether a user exists or not when recovering

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* update website

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
repository: repo_url: https://github.com/goauthentik/authentik
repo_name: goauthentik/authentik
description: The authentication glue you need.
repo_language: Python
owner: goauthentik
file_changes: filename: stage.py
change_type: ModificationType.MODIFY
diff: @@ -12,7 +12,7 @@ from rest_framework.fields import CharField
 from rest_framework.serializers import ValidationError
 
 from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
-from authentik.flows.models import FlowToken
+from authentik.flows.models import FlowDesignation, FlowToken
 from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
 from authentik.flows.stage import ChallengeStageView
 from authentik.flows.views.executor import QS_KEY_TOKEN
@@ -82,6 +82,11 @@ class EmailStageView(ChallengeStageView):
         """Helper function that sends the actual email. Implies that you've
         already checked that there is a pending user."""
         pending_user = self.get_pending_user()
+        if not pending_user.pk and self.executor.flow.designation == FlowDesignation.RECOVERY:
+            # Pending user does not have a primary key, and we're in a recovery flow,
+            # which means the user entered an invalid identifier, so we pretend to send the
+            # email, to not disclose if the user exists
+            return
         email = self.executor.plan.context.get(PLAN_CONTEXT_EMAIL_OVERRIDE, None)
         if not email:
             email = pending_user.email

programming_language: Python
filename: test_sending.py
change_type: ModificationType.MODIFY
diff: @@ -5,18 +5,20 @@ from unittest.mock import MagicMock, PropertyMock, patch
 from django.core import mail
 from django.core.mail.backends.locmem import EmailBackend
 from django.urls import reverse
-from rest_framework.test import APITestCase
 
+from authentik.core.models import User
 from authentik.core.tests.utils import create_test_admin_user, create_test_flow
 from authentik.events.models import Event, EventAction
 from authentik.flows.markers import StageMarker
 from authentik.flows.models import FlowDesignation, FlowStageBinding
 from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
+from authentik.flows.tests import FlowTestCase
 from authentik.flows.views.executor import SESSION_KEY_PLAN
+from authentik.lib.generators import generate_id
 from authentik.stages.email.models import EmailStage
 
 
-class TestEmailStageSending(APITestCase):
+class TestEmailStageSending(FlowTestCase):
     """Email tests"""
 
     def setUp(self):
@@ -44,6 +46,13 @@ class TestEmailStageSending(APITestCase):
         ):
             response = self.client.post(url)
             self.assertEqual(response.status_code, 200)
+            self.assertStageResponse(
+                response,
+                self.flow,
+                response_errors={
+                    "non_field_errors": [{"string": "email-sent", "code": "email-sent"}]
+                },
+            )
             self.assertEqual(len(mail.outbox), 1)
             self.assertEqual(mail.outbox[0].subject, "authentik")
             events = Event.objects.filter(action=EventAction.EMAIL_SENT)
@@ -54,6 +63,32 @@ class TestEmailStageSending(APITestCase):
             self.assertEqual(event.context["to_email"], [self.user.email])
             self.assertEqual(event.context["from_email"], "system@authentik.local")
 
+    def test_pending_fake_user(self):
+        """Test with pending (fake) user"""
+        self.flow.designation = FlowDesignation.RECOVERY
+        self.flow.save()
+        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
+        plan.context[PLAN_CONTEXT_PENDING_USER] = User(username=generate_id())
+        session = self.client.session
+        session[SESSION_KEY_PLAN] = plan
+        session.save()
+
+        url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
+        with patch(
+            "authentik.stages.email.models.EmailStage.backend_class",
+            PropertyMock(return_value=EmailBackend),
+        ):
+            response = self.client.post(url)
+            self.assertEqual(response.status_code, 200)
+            self.assertStageResponse(
+                response,
+                self.flow,
+                response_errors={
+                    "non_field_errors": [{"string": "email-sent", "code": "email-sent"}]
+                },
+            )
+            self.assertEqual(len(mail.outbox), 0)
+
     def test_send_error(self):
         """Test error during sending (sending will be retried)"""
         plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])

programming_language: Python
filename: stage.py
change_type: ModificationType.MODIFY
diff: @@ -118,8 +118,12 @@ class IdentificationChallengeResponse(ChallengeResponse):
                 username=uid_field,
                 email=uid_field,
             )
+            self.pre_user = self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
             if not current_stage.show_matched_user:
                 self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER_IDENTIFIER] = uid_field
+            if self.stage.executor.flow.designation == FlowDesignation.RECOVERY:
+                # When used in a recovery flow, always continue to not disclose if a user exists
+                return attrs
             raise ValidationError("Failed to authenticate.")
         self.pre_user = pre_user
         if not current_stage.password_stage:

programming_language: Python
filename: tests.py
change_type: ModificationType.MODIFY
diff: @@ -188,7 +188,7 @@ class TestIdentificationStage(FlowTestCase):
             ],
         )
 
-    def test_recovery_flow(self):
+    def test_link_recovery_flow(self):
         """Test that recovery flow is linked correctly"""
         flow = create_test_flow()
         self.stage.recovery_flow = flow
@@ -226,6 +226,38 @@ class TestIdentificationStage(FlowTestCase):
             ],
         )
 
+    def test_recovery_flow_invalid_user(self):
+        """Test that an invalid user can proceed in a recovery flow"""
+        self.flow.designation = FlowDesignation.RECOVERY
+        self.flow.save()
+        response = self.client.get(
+            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
+        )
+        self.assertStageResponse(
+            response,
+            self.flow,
+            component="ak-stage-identification",
+            user_fields=["email"],
+            password_fields=False,
+            show_source_labels=False,
+            primary_action="Continue",
+            sources=[
+                {
+                    "challenge": {
+                        "component": "xak-flow-redirect",
+                        "to": "/source/oauth/login/test/",
+                        "type": ChallengeTypes.REDIRECT.value,
+                    },
+                    "icon_url": "/static/authentik/sources/default.svg",
+                    "name": "test",
+                }
+            ],
+        )
+        form_data = {"uid_field": generate_id()}
+        url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
+        response = self.client.post(url, form_data)
+        self.assertEqual(response.status_code, 200)
+
     def test_api_validate(self):
         """Test API validation"""
         self.assertTrue(

programming_language: Python
filename: v2023.5.md
change_type: ModificationType.MODIFY
diff: @@ -152,6 +152,10 @@ image:
 
 -   \*: fix [CVE-2023-36456](../security/CVE-2023-36456), Reported by [@thijsa](https://github.com/thijsa)
 
+## Fixed in 2023.5.6
+
+-   \*: fix [CVE-2023-39522](../security/CVE-2023-39522), Reported by [@markrassamni](https://github.com/markrassamni)
+
 ## API Changes
 
 #### What's Changed

programming_language: Markdown
filename: v2023.6.md
change_type: ModificationType.MODIFY
diff: @@ -88,6 +88,10 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2023.6
 -   sources/ldap: fix more errors (#6191)
 -   sources/ldap: fix page size (#6187)
 
+## Fixed in 2023.6.2
+
+-   \*: fix [CVE-2023-39522](../security/CVE-2023-39522), Reported by [@markrassamni](https://github.com/markrassamni)
+
 ## API Changes
 
 #### What's New

programming_language: Markdown
filename: CVE-2023-39522.md
change_type: ModificationType.ADD
diff: @@ -0,0 +1,27 @@
+# CVE-2023-39522
+
+_Reported by [@markrassamni](https://github.com/markrassamni)_
+
+## Username enumeration attack
+
+### Summary
+
+Using a recovery flow with an identification stage an attacker is able to determine if a username exists.
+
+### Patches
+
+authentik 2023.5.6 and 2023.6.2 fix this issue.
+
+### Impact
+
+Only setups configured with a recovery flow are impacted by this.
+
+### Details
+
+An attacker can easily enumerate and check users' existence using the recovery flow, as a clear message is shown when a user doesn't exist. Depending on configuration this can either be done by username, email, or both.
+
+### For more information
+
+If you have any questions or comments about this advisory:
+
+-   Email us at [security@goauthentik.io](mailto:security@goauthentik.io)

programming_language: Markdown
filename: sidebars.js
change_type: ModificationType.MODIFY
diff: @@ -361,6 +361,7 @@ const docsSidebar = {
             },
             items: [
                 "security/policy",
+                "security/CVE-2023-39522",
                 "security/CVE-2023-36456",
                 "security/2023-06-cure53",
                 "security/CVE-2023-26481",

programming_language: JavaScript