security: fix CVE-2023-26481 (#4832)

fix CVE-2023-26481

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L 2023-03-02 20:15:33 +01:00 committed by Jens Langhammer
parent 093573f89a
commit 2b74a1f03b
No known key found for this signature in database
7 changed files with 81 additions and 11 deletions

View file

@ -162,7 +162,7 @@ class FlowExecutorView(APIView):
token.delete() token.delete()
if not isinstance(plan, FlowPlan): if not isinstance(plan, FlowPlan):
return None return None
plan.context[PLAN_CONTEXT_IS_RESTORED] = True plan.context[PLAN_CONTEXT_IS_RESTORED] = token
self._logger.debug("f(exec): restored flow plan from token", plan=plan) self._logger.debug("f(exec): restored flow plan from token", plan=plan)
return plan return plan

View file

@ -15,7 +15,7 @@ from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTyp
from authentik.flows.models import FlowToken from authentik.flows.models import 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, SESSION_KEY_GET from authentik.flows.views.executor import QS_KEY_TOKEN
from authentik.stages.email.models import EmailStage from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage from authentik.stages.email.utils import TemplateEmailMessage
@ -103,12 +103,14 @@ class EmailStageView(ChallengeStageView):
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
# Check if the user came back from the email link to verify # Check if the user came back from the email link to verify
if QS_KEY_TOKEN in request.session.get( restore_token: FlowToken = self.executor.plan.context.get(PLAN_CONTEXT_IS_RESTORED, None)
SESSION_KEY_GET, {} user = self.get_pending_user()
) and self.executor.plan.context.get(PLAN_CONTEXT_IS_RESTORED, False): if restore_token:
if restore_token.user != user:
self.logger.warning("Flow token for non-matching user, denying request")
return self.executor.stage_invalid()
messages.success(request, _("Successfully verified Email.")) messages.success(request, _("Successfully verified Email."))
if self.executor.current_stage.activate_user_on_success: if self.executor.current_stage.activate_user_on_success:
user = self.get_pending_user()
user.is_active = True user.is_active = True
user.save() user.save()
return self.executor.stage_ok() return self.executor.stage_ok()

View file

@ -7,10 +7,9 @@ from django.core.mail.backends.smtp import EmailBackend as SMTPEmailBackend
from django.urls import reverse from django.urls import reverse
from django.utils.http import urlencode from django.utils.http import urlencode
from authentik.core.models import Token
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.flows.markers import StageMarker from authentik.flows.markers import StageMarker
from authentik.flows.models import FlowDesignation, FlowStageBinding from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken
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.tests import FlowTestCase
from authentik.flows.views.executor import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
@ -134,7 +133,7 @@ class TestEmailStage(FlowTestCase):
session = self.client.session session = self.client.session
session[SESSION_KEY_PLAN] = plan session[SESSION_KEY_PLAN] = plan
session.save() session.save()
token: Token = Token.objects.get(user=self.user) token: FlowToken = FlowToken.objects.get(user=self.user)
with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()): with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()):
# Call the executor shell to preseed the session # Call the executor shell to preseed the session
@ -165,3 +164,43 @@ class TestEmailStage(FlowTestCase):
plan: FlowPlan = session[SESSION_KEY_PLAN] plan: FlowPlan = session[SESSION_KEY_PLAN]
self.assertEqual(plan.context[PLAN_CONTEXT_PENDING_USER], self.user) self.assertEqual(plan.context[PLAN_CONTEXT_PENDING_USER], self.user)
self.assertTrue(plan.context[PLAN_CONTEXT_PENDING_USER].is_active) self.assertTrue(plan.context[PLAN_CONTEXT_PENDING_USER].is_active)
def test_token_invalid_user(self):
"""Test with token with invalid user"""
# Make sure token exists
self.test_pending_user()
self.user.is_active = False
self.user.save()
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
# Set flow token user to a different user
token: FlowToken = FlowToken.objects.get(user=self.user)
token.user = create_test_admin_user()
token.save()
with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()):
# Call the executor shell to preseed the session
url = reverse(
"authentik_api:flow-executor",
kwargs={"flow_slug": self.flow.slug},
)
url_query = urlencode(
{
QS_KEY_TOKEN: token.key,
}
)
url += f"?query={url_query}"
self.client.get(url)
# Call the actual executor to get the JSON Response
response = self.client.get(
reverse(
"authentik_api:flow-executor",
kwargs={"flow_slug": self.flow.slug},
)
)
self.assertEqual(response.status_code, 200)
self.assertStageResponse(response, component="ak-stage-access-denied")

View file

@ -154,6 +154,7 @@ entries:
policy: !KeyOf default-recovery-skip-if-restored policy: !KeyOf default-recovery-skip-if-restored
target: !KeyOf flow-binding-email target: !KeyOf flow-binding-email
order: 0 order: 0
state: absent
model: authentik_policies.policybinding model: authentik_policies.policybinding
attrs: attrs:
negate: false negate: false

View file

@ -99,7 +99,7 @@ This includes the following:
- `context['application']`: The application the user is in the process of authorizing. (Optional) - `context['application']`: The application the user is in the process of authorizing. (Optional)
- `context['source']`: The source the user is authenticating/enrolling with. (Optional) - `context['source']`: The source the user is authenticating/enrolling with. (Optional)
- `context['pending_user']`: The currently pending user, see [User](../user-group/user.md#object-attributes) - `context['pending_user']`: The currently pending user, see [User](../user-group/user.md#object-attributes)
- `context['is_restored']`: Set to `True` when the flow plan has been restored from a flow token, for example the user clicked a link to a flow which was sent by an email stage. (Optional) - `context['is_restored']`: Contains the flow token when the flow plan was restored from a link, for example the user clicked a link to a flow which was sent by an email stage. (Optional)
- `context['auth_method']`: Authentication method (this value is set by password stages) (Optional) - `context['auth_method']`: Authentication method (this value is set by password stages) (Optional)
Depending on method, `context['auth_method_args']` is also set. Depending on method, `context['auth_method_args']` is also set.

View file

@ -0,0 +1,27 @@
# CVE-2023-26481
_Reported by [@fuomag9](https://github.com/fuomag9)_
## Insufficient user check in FlowTokens by Email stage
### Summary
Due to an insufficient access check, a recovery flow link that is created by an admin (or sent via email by an admin) can be used to set the password for any arbitrary user.
### Patches
authentik 2022.12.3, 2023.1.3, 2023.2.3 fix this issue.
### Impact
This attack is only possible if a recovery flow exists, which has both an Identification and an Email stage bound to it. If the flow has policies on the identification stage to skip it when the flow is restored (by checking `request.context['is_restored']`), the flow is not affected by this. With this flow in place, an administrator must create a recovery Link or send a recovery URL to the attacker, who can, due to the improper validation of the token create, set the password for any account.
### Workaround
It is recommended to upgrade to the patched version of authentik. Regardless, for custom recovery flows it is recommended to add a policy that checks if the flow is restored, and skips the identification stage.
### For more information
If you have any questions or comments about this advisory:
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)

View file

@ -300,9 +300,10 @@ module.exports = {
}, },
items: [ items: [
"security/policy", "security/policy",
"security/CVE-2022-23555",
"security/CVE-2022-46145", "security/CVE-2022-46145",
"security/CVE-2022-46172", "security/CVE-2022-46172",
"security/CVE-2022-23555", "security/CVE-2023-26481",
], ],
}, },
], ],