Skip to content

Commit aa874dd

Browse files
authored
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>
1 parent 87f6552 commit aa874dd

File tree

8 files changed

+116
-4
lines changed

8 files changed

+116
-4
lines changed

Diff for: authentik/stages/email/stage.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from rest_framework.serializers import ValidationError
1313

1414
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
15-
from authentik.flows.models import FlowToken
15+
from authentik.flows.models import FlowDesignation, FlowToken
1616
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
1717
from authentik.flows.stage import ChallengeStageView
1818
from authentik.flows.views.executor import QS_KEY_TOKEN
@@ -82,6 +82,11 @@ def send_email(self):
8282
"""Helper function that sends the actual email. Implies that you've
8383
already checked that there is a pending user."""
8484
pending_user = self.get_pending_user()
85+
if not pending_user.pk and self.executor.flow.designation == FlowDesignation.RECOVERY:
86+
# Pending user does not have a primary key, and we're in a recovery flow,
87+
# which means the user entered an invalid identifier, so we pretend to send the
88+
# email, to not disclose if the user exists
89+
return
8590
email = self.executor.plan.context.get(PLAN_CONTEXT_EMAIL_OVERRIDE, None)
8691
if not email:
8792
email = pending_user.email

Diff for: authentik/stages/email/tests/test_sending.py

+37-2
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,20 @@
55
from django.core import mail
66
from django.core.mail.backends.locmem import EmailBackend
77
from django.urls import reverse
8-
from rest_framework.test import APITestCase
98

9+
from authentik.core.models import User
1010
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
1111
from authentik.events.models import Event, EventAction
1212
from authentik.flows.markers import StageMarker
1313
from authentik.flows.models import FlowDesignation, FlowStageBinding
1414
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
15+
from authentik.flows.tests import FlowTestCase
1516
from authentik.flows.views.executor import SESSION_KEY_PLAN
17+
from authentik.lib.generators import generate_id
1618
from authentik.stages.email.models import EmailStage
1719

1820

19-
class TestEmailStageSending(APITestCase):
21+
class TestEmailStageSending(FlowTestCase):
2022
"""Email tests"""
2123

2224
def setUp(self):
@@ -44,6 +46,13 @@ def test_pending_user(self):
4446
):
4547
response = self.client.post(url)
4648
self.assertEqual(response.status_code, 200)
49+
self.assertStageResponse(
50+
response,
51+
self.flow,
52+
response_errors={
53+
"non_field_errors": [{"string": "email-sent", "code": "email-sent"}]
54+
},
55+
)
4756
self.assertEqual(len(mail.outbox), 1)
4857
self.assertEqual(mail.outbox[0].subject, "authentik")
4958
events = Event.objects.filter(action=EventAction.EMAIL_SENT)
@@ -54,6 +63,32 @@ def test_pending_user(self):
5463
self.assertEqual(event.context["to_email"], [self.user.email])
5564
self.assertEqual(event.context["from_email"], "system@authentik.local")
5665

66+
def test_pending_fake_user(self):
67+
"""Test with pending (fake) user"""
68+
self.flow.designation = FlowDesignation.RECOVERY
69+
self.flow.save()
70+
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
71+
plan.context[PLAN_CONTEXT_PENDING_USER] = User(username=generate_id())
72+
session = self.client.session
73+
session[SESSION_KEY_PLAN] = plan
74+
session.save()
75+
76+
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
77+
with patch(
78+
"authentik.stages.email.models.EmailStage.backend_class",
79+
PropertyMock(return_value=EmailBackend),
80+
):
81+
response = self.client.post(url)
82+
self.assertEqual(response.status_code, 200)
83+
self.assertStageResponse(
84+
response,
85+
self.flow,
86+
response_errors={
87+
"non_field_errors": [{"string": "email-sent", "code": "email-sent"}]
88+
},
89+
)
90+
self.assertEqual(len(mail.outbox), 0)
91+
5792
def test_send_error(self):
5893
"""Test error during sending (sending will be retried)"""
5994
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])

Diff for: authentik/stages/identification/stage.py

+4
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,12 @@ def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
118118
username=uid_field,
119119
email=uid_field,
120120
)
121+
self.pre_user = self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
121122
if not current_stage.show_matched_user:
122123
self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER_IDENTIFIER] = uid_field
124+
if self.stage.executor.flow.designation == FlowDesignation.RECOVERY:
125+
# When used in a recovery flow, always continue to not disclose if a user exists
126+
return attrs
123127
raise ValidationError("Failed to authenticate.")
124128
self.pre_user = pre_user
125129
if not current_stage.password_stage:

Diff for: authentik/stages/identification/tests.py

+33-1
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ def test_enrollment_flow(self):
188188
],
189189
)
190190

191-
def test_recovery_flow(self):
191+
def test_link_recovery_flow(self):
192192
"""Test that recovery flow is linked correctly"""
193193
flow = create_test_flow()
194194
self.stage.recovery_flow = flow
@@ -226,6 +226,38 @@ def test_recovery_flow(self):
226226
],
227227
)
228228

229+
def test_recovery_flow_invalid_user(self):
230+
"""Test that an invalid user can proceed in a recovery flow"""
231+
self.flow.designation = FlowDesignation.RECOVERY
232+
self.flow.save()
233+
response = self.client.get(
234+
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
235+
)
236+
self.assertStageResponse(
237+
response,
238+
self.flow,
239+
component="ak-stage-identification",
240+
user_fields=["email"],
241+
password_fields=False,
242+
show_source_labels=False,
243+
primary_action="Continue",
244+
sources=[
245+
{
246+
"challenge": {
247+
"component": "xak-flow-redirect",
248+
"to": "/source/oauth/login/test/",
249+
"type": ChallengeTypes.REDIRECT.value,
250+
},
251+
"icon_url": "/static/authentik/sources/default.svg",
252+
"name": "test",
253+
}
254+
],
255+
)
256+
form_data = {"uid_field": generate_id()}
257+
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
258+
response = self.client.post(url, form_data)
259+
self.assertEqual(response.status_code, 200)
260+
229261
def test_api_validate(self):
230262
"""Test API validation"""
231263
self.assertTrue(

Diff for: website/docs/releases/2023/v2023.5.md

+4
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,10 @@ image:
152152
153153
- \*: fix [CVE-2023-36456](../security/CVE-2023-36456), Reported by [@thijsa](https://github.com/thijsa)
154154
155+
## Fixed in 2023.5.6
156+
157+
- \*: fix [CVE-2023-39522](../security/CVE-2023-39522), Reported by [@markrassamni](https://github.com/markrassamni)
158+
155159
## API Changes
156160
157161
#### What's Changed

Diff for: website/docs/releases/2023/v2023.6.md

+4
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2023.6
8888
- sources/ldap: fix more errors (#6191)
8989
- sources/ldap: fix page size (#6187)
9090

91+
## Fixed in 2023.6.2
92+
93+
- \*: fix [CVE-2023-39522](../security/CVE-2023-39522), Reported by [@markrassamni](https://github.com/markrassamni)
94+
9195
## API Changes
9296

9397
#### What's New

Diff for: website/docs/security/CVE-2023-39522.md

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# CVE-2023-39522
2+
3+
_Reported by [@markrassamni](https://github.com/markrassamni)_
4+
5+
## Username enumeration attack
6+
7+
### Summary
8+
9+
Using a recovery flow with an identification stage an attacker is able to determine if a username exists.
10+
11+
### Patches
12+
13+
authentik 2023.5.6 and 2023.6.2 fix this issue.
14+
15+
### Impact
16+
17+
Only setups configured with a recovery flow are impacted by this.
18+
19+
### Details
20+
21+
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.
22+
23+
### For more information
24+
25+
If you have any questions or comments about this advisory:
26+
27+
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)

Diff for: website/sidebars.js

+1
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ const docsSidebar = {
361361
},
362362
items: [
363363
"security/policy",
364+
"security/CVE-2023-39522",
364365
"security/CVE-2023-36456",
365366
"security/2023-06-cure53",
366367
"security/CVE-2023-26481",

0 commit comments

Comments
 (0)