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> # Conflicts: # website/docs/releases/2023/v2023.5.md # website/docs/releases/2023/v2023.6.md
This commit is contained in:
parent
b99ac01228
commit
54d5aa20ba
|
@ -12,7 +12,7 @@ from rest_framework.fields import CharField
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
|
|
||||||
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
|
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.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
|
||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
from authentik.flows.views.executor import QS_KEY_TOKEN
|
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
|
"""Helper function that sends the actual email. Implies that you've
|
||||||
already checked that there is a pending user."""
|
already checked that there is a pending user."""
|
||||||
pending_user = self.get_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)
|
email = self.executor.plan.context.get(PLAN_CONTEXT_EMAIL_OVERRIDE, None)
|
||||||
if not email:
|
if not email:
|
||||||
email = pending_user.email
|
email = pending_user.email
|
||||||
|
|
|
@ -5,18 +5,20 @@ from unittest.mock import MagicMock, PropertyMock, patch
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
from django.core.mail.backends.locmem import EmailBackend
|
from django.core.mail.backends.locmem import EmailBackend
|
||||||
from django.urls import reverse
|
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.core.tests.utils import create_test_admin_user, create_test_flow
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.flows.markers import StageMarker
|
from authentik.flows.markers import StageMarker
|
||||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
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.flows.views.executor import SESSION_KEY_PLAN
|
||||||
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.stages.email.models import EmailStage
|
from authentik.stages.email.models import EmailStage
|
||||||
|
|
||||||
|
|
||||||
class TestEmailStageSending(APITestCase):
|
class TestEmailStageSending(FlowTestCase):
|
||||||
"""Email tests"""
|
"""Email tests"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -44,6 +46,13 @@ class TestEmailStageSending(APITestCase):
|
||||||
):
|
):
|
||||||
response = self.client.post(url)
|
response = self.client.post(url)
|
||||||
self.assertEqual(response.status_code, 200)
|
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(len(mail.outbox), 1)
|
||||||
self.assertEqual(mail.outbox[0].subject, "authentik")
|
self.assertEqual(mail.outbox[0].subject, "authentik")
|
||||||
events = Event.objects.filter(action=EventAction.EMAIL_SENT)
|
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["to_email"], [self.user.email])
|
||||||
self.assertEqual(event.context["from_email"], "system@authentik.local")
|
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):
|
def test_send_error(self):
|
||||||
"""Test error during sending (sending will be retried)"""
|
"""Test error during sending (sending will be retried)"""
|
||||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
|
|
|
@ -118,8 +118,12 @@ class IdentificationChallengeResponse(ChallengeResponse):
|
||||||
username=uid_field,
|
username=uid_field,
|
||||||
email=uid_field,
|
email=uid_field,
|
||||||
)
|
)
|
||||||
|
self.pre_user = self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
||||||
if not current_stage.show_matched_user:
|
if not current_stage.show_matched_user:
|
||||||
self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER_IDENTIFIER] = uid_field
|
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.")
|
raise ValidationError("Failed to authenticate.")
|
||||||
self.pre_user = pre_user
|
self.pre_user = pre_user
|
||||||
if not current_stage.password_stage:
|
if not current_stage.password_stage:
|
||||||
|
|
|
@ -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"""
|
"""Test that recovery flow is linked correctly"""
|
||||||
flow = create_test_flow()
|
flow = create_test_flow()
|
||||||
self.stage.recovery_flow = 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):
|
def test_api_validate(self):
|
||||||
"""Test API validation"""
|
"""Test API validation"""
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
|
|
|
@ -118,6 +118,37 @@ image:
|
||||||
- web/flows: improve UI for TOTP code input (#5676)
|
- web/flows: improve UI for TOTP code input (#5676)
|
||||||
- web/flows: update flow background (#5639)
|
- web/flows: update flow background (#5639)
|
||||||
|
|
||||||
|
## Fixed in 2023.5.2
|
||||||
|
|
||||||
|
- blueprints: fix check for file path not being run on worker (#5703)
|
||||||
|
- blueprints: support custom ports for OCI blueprints (#5727)
|
||||||
|
- core: bump coverage from 7.2.5 to 7.2.6 (#5738)
|
||||||
|
- core: make groups field for user optional (#5702)
|
||||||
|
- events: fix ak_create_event using wrong request for event creation (#5731)
|
||||||
|
- lib: add tests for ak_create_event (#5710)
|
||||||
|
- outposts: fix missing radius outpost controller (#5730)
|
||||||
|
- web/user: fix MFA enroll dropdown broken when password stage has no configuration flow (#5744)
|
||||||
|
|
||||||
|
## Fixed in 2023.5.3
|
||||||
|
|
||||||
|
- blueprints: fix API validation with OCI blueprint path (#5822)
|
||||||
|
- ci: build outpost binaries statically linked (#5823)
|
||||||
|
- ci: replace github bot account with github app (#5819)
|
||||||
|
- providers/ldap: fix LDAP Outpost application selection (#5812)
|
||||||
|
- web/flows: fix RedirectStage not detecting absolute URLs correctly (#5781)
|
||||||
|
|
||||||
|
## Fixed in 2023.5.4
|
||||||
|
|
||||||
|
- security: Address pen-test findings from the [2023-06 Cure53 Code audit](../../security/2023-06-cure53.md)
|
||||||
|
|
||||||
|
## Fixed in 2023.5.5
|
||||||
|
|
||||||
|
- \*: 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
|
## API Changes
|
||||||
|
|
||||||
#### What's Changed
|
#### What's Changed
|
||||||
|
|
|
@ -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)
|
|
@ -321,6 +321,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
items: [
|
items: [
|
||||||
"security/policy",
|
"security/policy",
|
||||||
|
"security/CVE-2023-39522",
|
||||||
"security/CVE-2023-36456",
|
"security/CVE-2023-36456",
|
||||||
"security/CVE-2023-26481",
|
"security/CVE-2023-26481",
|
||||||
"security/CVE-2022-23555",
|
"security/CVE-2022-23555",
|
||||||
|
|
Reference in New Issue