Username enumeration attack in goauthentik
Description
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.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
In goauthentik, the recovery flow leaks whether a username exists, enabling user enumeration before patching.
Vulnerability
Overview
CVE-2023-39522 is a username enumeration vulnerability in goauthentik, an open-source Identity Provider. In affected versions (before 2023.5.6 and 2023.6.2) that have a recovery flow configured, the identification stage of the recovery flow reveals whether a given username or email address exists. This occurs because the application returns a distinct error message when a user does not exist, while a different message or proceeding flow step is shown for valid users [1][4].
Exploitation
An attacker can exploit this by simply attempting to start a password recovery process for a candidate username or email. The response from the recovery endpoint clearly indicates whether the account exists. This does not require authentication or any special network position, as the recovery flow is typically exposed to unauthenticated users. The enumeration can be performed manually or automated, and it works for both username and email lookup depending on the flow configuration [1][4].
Impact
Successful exploitation allows an attacker to build a list of valid usernames or email addresses on the system. This information can be used for targeted phishing, brute-force attacks, or social engineering. The vulnerability does not leak other data, but the existence of accounts is a starting point for further attacks [1].
Mitigation
The issue has been fixed in goauthentik versions 2023.5.6 and 2023.6.2. The fix was implemented in commit aa874dd92a770d5f8cd8f265b7cdd31cd73a4599, which ensures that the recovery flow behaves consistently for both valid and invalid users, such as always showing the same message and sending an email regardless [2]. Users are advised to upgrade to the patched versions; there are no known workarounds for this vulnerability [1][4].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@goauthentik/apinpm | >= 2023.6.0, < 2023.6.2 | 2023.6.2 |
@goauthentik/apinpm | < 2023.5.6 | 2023.5.6 |
Affected products
3- osv-coords2 versions
< 2023.5.6+ 1 more
- (no CPE)range: < 2023.5.6
- (no CPE)range: >= 2023.6.0, < 2023.6.2
- goauthentik/authentikv5Range: >= 2023.6.0, < 2023.6.2
Patches
1aa874dd92a77security: fix CVE-2023-39522 (#6665)
8 files changed · +116 −4
authentik/stages/email/stage.py+6 −1 modified@@ -12,7 +12,7 @@ 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 @@ def send_email(self): """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
authentik/stages/email/tests/test_sending.py+37 −2 modified@@ -5,18 +5,20 @@ 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 @@ def test_pending_user(self): ): 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 @@ def test_pending_user(self): 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()])
authentik/stages/identification/stage.py+4 −0 modified@@ -118,8 +118,12 @@ def validate(self, attrs: dict[str, Any]) -> dict[str, Any]: 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:
authentik/stages/identification/tests.py+33 −1 modified@@ -188,7 +188,7 @@ def test_enrollment_flow(self): ], ) - 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 @@ def test_recovery_flow(self): ], ) + 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(
website/docs/releases/2023/v2023.5.md+4 −0 modified@@ -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
website/docs/releases/2023/v2023.6.md+4 −0 modified@@ -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
website/docs/security/CVE-2023-39522.md+27 −0 added@@ -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)
website/sidebars.js+1 −0 modified@@ -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",
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-vmf9-6pcv-xr87ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-39522ghsaADVISORY
- github.com/goauthentik/authentik/commit/aa874dd92a770d5f8cd8f265b7cdd31cd73a4599ghsax_refsource_MISCWEB
- github.com/goauthentik/authentik/security/advisories/GHSA-vmf9-6pcv-xr87ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.