Skip to content

Commit

Permalink
security: fix CVE-2023-39522 (#6665)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
BeryJu committed Aug 29, 2023
1 parent 87f6552 commit aa874dd
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 4 deletions.
7 changes: 6 additions & 1 deletion authentik/stages/email/stage.py
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
39 changes: 37 additions & 2 deletions authentik/stages/email/tests/test_sending.py
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand All @@ -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()])
Expand Down
4 changes: 4 additions & 0 deletions authentik/stages/identification/stage.py
Expand Up @@ -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:
Expand Down
34 changes: 33 additions & 1 deletion authentik/stages/identification/tests.py
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
4 changes: 4 additions & 0 deletions website/docs/releases/2023/v2023.5.md
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions website/docs/releases/2023/v2023.6.md
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions website/docs/security/CVE-2023-39522.md
@@ -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)
1 change: 1 addition & 0 deletions website/sidebars.js
Expand Up @@ -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",
Expand Down

0 comments on commit aa874dd

Please sign in to comment.