security: fix CVE 2022 46145 (#4140)
* add flow authentication requirement Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add website for cve Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add tests Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * flows: handle FlowNonApplicableException without policy result Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add release notes Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		| @ -71,6 +71,7 @@ class FlowSerializer(ModelSerializer): | ||||
|             "export_url", | ||||
|             "layout", | ||||
|             "denied_action", | ||||
|             "authentication", | ||||
|         ] | ||||
|         extra_kwargs = { | ||||
|             "background": {"read_only": True}, | ||||
|  | ||||
| @ -1,4 +1,6 @@ | ||||
| """flow exceptions""" | ||||
| from typing import Optional | ||||
|  | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
|  | ||||
| from authentik.lib.sentry import SentryIgnoredException | ||||
| @ -6,15 +8,15 @@ from authentik.policies.types import PolicyResult | ||||
|  | ||||
|  | ||||
| class FlowNonApplicableException(SentryIgnoredException): | ||||
|     """Flow does not apply to current user (denied by policy).""" | ||||
|     """Flow does not apply to current user (denied by policy, or otherwise).""" | ||||
|  | ||||
|     policy_result: PolicyResult | ||||
|     policy_result: Optional[PolicyResult] = None | ||||
|  | ||||
|     @property | ||||
|     def messages(self) -> str: | ||||
|         """Get messages from policy result, fallback to generic reason""" | ||||
|         if len(self.policy_result.messages) < 1: | ||||
|             return _("Flow does not apply to current user (denied by policy).") | ||||
|         if not self.policy_result or len(self.policy_result.messages) < 1: | ||||
|             return _("Flow does not apply to current user.") | ||||
|         return "\n".join(self.policy_result.messages) | ||||
|  | ||||
|  | ||||
|  | ||||
							
								
								
									
										27
									
								
								authentik/flows/migrations/0024_flow_authentication.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								authentik/flows/migrations/0024_flow_authentication.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | ||||
| # Generated by Django 4.1.3 on 2022-11-30 09:04 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_flows", "0023_flow_denied_action"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="flow", | ||||
|             name="authentication", | ||||
|             field=models.TextField( | ||||
|                 choices=[ | ||||
|                     ("none", "None"), | ||||
|                     ("require_authenticated", "Require Authenticated"), | ||||
|                     ("require_unauthenticated", "Require Unauthenticated"), | ||||
|                     ("require_superuser", "Require Superuser"), | ||||
|                 ], | ||||
|                 default="none", | ||||
|                 help_text="Required level of authentication and authorization to access a flow.", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @ -23,6 +23,15 @@ if TYPE_CHECKING: | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| class FlowAuthenticationRequirement(models.TextChoices): | ||||
|     """Required level of authentication and authorization to access a flow""" | ||||
|  | ||||
|     NONE = "none" | ||||
|     REQUIRE_AUTHENTICATED = "require_authenticated" | ||||
|     REQUIRE_UNAUTHENTICATED = "require_unauthenticated" | ||||
|     REQUIRE_SUPERUSER = "require_superuser" | ||||
|  | ||||
|  | ||||
| class NotConfiguredAction(models.TextChoices): | ||||
|     """Decides how the FlowExecutor should proceed when a stage isn't configured""" | ||||
|  | ||||
| @ -152,6 +161,12 @@ class Flow(SerializerModel, PolicyBindingModel): | ||||
|         help_text=_("Configure what should happen when a flow denies access to a user."), | ||||
|     ) | ||||
|  | ||||
|     authentication = models.TextField( | ||||
|         choices=FlowAuthenticationRequirement.choices, | ||||
|         default=FlowAuthenticationRequirement.NONE, | ||||
|         help_text=_("Required level of authentication and authorization to access a flow."), | ||||
|     ) | ||||
|  | ||||
|     @property | ||||
|     def background_url(self) -> str: | ||||
|         """Get the URL to the background image. If the name is /static or starts with http | ||||
|  | ||||
| @ -13,7 +13,14 @@ from authentik.events.models import cleanse_dict | ||||
| from authentik.flows.apps import HIST_FLOWS_PLAN_TIME | ||||
| from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException | ||||
| from authentik.flows.markers import ReevaluateMarker, StageMarker | ||||
| from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, Stage, in_memory_stage | ||||
| from authentik.flows.models import ( | ||||
|     Flow, | ||||
|     FlowAuthenticationRequirement, | ||||
|     FlowDesignation, | ||||
|     FlowStageBinding, | ||||
|     Stage, | ||||
|     in_memory_stage, | ||||
| ) | ||||
| from authentik.lib.config import CONFIG | ||||
| from authentik.policies.engine import PolicyEngine | ||||
|  | ||||
| @ -117,11 +124,30 @@ class FlowPlanner: | ||||
|         self.flow = flow | ||||
|         self._logger = get_logger().bind(flow_slug=flow.slug) | ||||
|  | ||||
|     def _check_authentication(self, request: HttpRequest): | ||||
|         """Check the flow's authentication level is matched by `request`""" | ||||
|         if ( | ||||
|             self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_AUTHENTICATED | ||||
|             and not request.user.is_authenticated | ||||
|         ): | ||||
|             raise FlowNonApplicableException() | ||||
|         if ( | ||||
|             self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_UNAUTHENTICATED | ||||
|             and request.user.is_authenticated | ||||
|         ): | ||||
|             raise FlowNonApplicableException() | ||||
|         if ( | ||||
|             self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_SUPERUSER | ||||
|             and not request.user.is_superuser | ||||
|         ): | ||||
|             raise FlowNonApplicableException() | ||||
|  | ||||
|     def plan( | ||||
|         self, request: HttpRequest, default_context: Optional[dict[str, Any]] = None | ||||
|     ) -> FlowPlan: | ||||
|         """Check each of the flows' policies, check policies for each stage with PolicyBinding | ||||
|         and return ordered list""" | ||||
|         self._check_authentication(request) | ||||
|         with Hub.current.start_span( | ||||
|             op="authentik.flow.planner.plan", description=self.flow.slug | ||||
|         ) as span: | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| """flow planner tests""" | ||||
| from unittest.mock import MagicMock, Mock, PropertyMock, patch | ||||
|  | ||||
| from django.contrib.auth.models import AnonymousUser | ||||
| from django.contrib.sessions.middleware import SessionMiddleware | ||||
| from django.core.cache import cache | ||||
| from django.test import RequestFactory, TestCase | ||||
| @ -8,10 +9,10 @@ from django.urls import reverse | ||||
| from guardian.shortcuts import get_anonymous_user | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.core.tests.utils import create_test_flow | ||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_flow | ||||
| from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException | ||||
| from authentik.flows.markers import ReevaluateMarker, StageMarker | ||||
| from authentik.flows.models import FlowDesignation, FlowStageBinding | ||||
| from authentik.flows.models import FlowAuthenticationRequirement, FlowDesignation, FlowStageBinding | ||||
| from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key | ||||
| from authentik.lib.tests.utils import dummy_get_response | ||||
| from authentik.policies.dummy.models import DummyPolicy | ||||
| @ -43,6 +44,30 @@ class TestFlowPlanner(TestCase): | ||||
|             planner = FlowPlanner(flow) | ||||
|             planner.plan(request) | ||||
|  | ||||
|     def test_authentication(self): | ||||
|         """Test flow authentication""" | ||||
|         flow = create_test_flow() | ||||
|         flow.authentication = FlowAuthenticationRequirement.NONE | ||||
|         request = self.request_factory.get( | ||||
|             reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), | ||||
|         ) | ||||
|         request.user = AnonymousUser() | ||||
|         planner = FlowPlanner(flow) | ||||
|         planner.allow_empty_flows = True | ||||
|         planner.plan(request) | ||||
|  | ||||
|         with self.assertRaises(FlowNonApplicableException): | ||||
|             flow.authentication = FlowAuthenticationRequirement.REQUIRE_AUTHENTICATED | ||||
|             FlowPlanner(flow).plan(request) | ||||
|         with self.assertRaises(FlowNonApplicableException): | ||||
|             flow.authentication = FlowAuthenticationRequirement.REQUIRE_SUPERUSER | ||||
|             FlowPlanner(flow).plan(request) | ||||
|  | ||||
|         request.user = create_test_admin_user() | ||||
|         planner = FlowPlanner(flow) | ||||
|         planner.allow_empty_flows = True | ||||
|         planner.plan(request) | ||||
|  | ||||
|     @patch( | ||||
|         "authentik.policies.engine.PolicyEngine.result", | ||||
|         POLICY_RETURN_FALSE, | ||||
|  | ||||
| @ -6,6 +6,7 @@ entries: | ||||
|     designation: stage_configuration | ||||
|     name: Change Password | ||||
|     title: Change password | ||||
|     authentication: require_authenticated | ||||
|   identifiers: | ||||
|     slug: default-password-change | ||||
|   model: authentik_flows.flow | ||||
|  | ||||
| @ -11,6 +11,7 @@ entries: | ||||
|     designation: authentication | ||||
|     name: Welcome to authentik! | ||||
|     title: Welcome to authentik! | ||||
|     authentication: require_unauthenticated | ||||
|   identifiers: | ||||
|     slug: default-authentication-flow | ||||
|   model: authentik_flows.flow | ||||
|  | ||||
| @ -6,6 +6,7 @@ entries: | ||||
|     designation: invalidation | ||||
|     name: Logout | ||||
|     title: Default Invalidation Flow | ||||
|     authentication: require_authenticated | ||||
|   identifiers: | ||||
|     slug: default-invalidation-flow | ||||
|   model: authentik_flows.flow | ||||
|  | ||||
| @ -6,6 +6,7 @@ entries: | ||||
|     designation: stage_configuration | ||||
|     name: default-authenticator-static-setup | ||||
|     title: Setup Static OTP Tokens | ||||
|     authentication: require_authenticated | ||||
|   identifiers: | ||||
|     slug: default-authenticator-static-setup | ||||
|   model: authentik_flows.flow | ||||
|  | ||||
| @ -6,6 +6,7 @@ entries: | ||||
|     designation: stage_configuration | ||||
|     name: default-authenticator-totp-setup | ||||
|     title: Setup Two-Factor authentication | ||||
|     authentication: require_authenticated | ||||
|   identifiers: | ||||
|     slug: default-authenticator-totp-setup | ||||
|   model: authentik_flows.flow | ||||
|  | ||||
| @ -6,6 +6,7 @@ entries: | ||||
|     designation: stage_configuration | ||||
|     name: default-authenticator-webauthn-setup | ||||
|     title: Setup WebAuthn | ||||
|     authentication: require_authenticated | ||||
|   identifiers: | ||||
|     slug: default-authenticator-webauthn-setup | ||||
|   model: authentik_flows.flow | ||||
|  | ||||
| @ -6,6 +6,7 @@ entries: | ||||
|     designation: authorization | ||||
|     name: Authorize Application | ||||
|     title: Redirecting to %(app)s | ||||
|     authentication: require_authenticated | ||||
|   identifiers: | ||||
|     slug: default-provider-authorization-explicit-consent | ||||
|   model: authentik_flows.flow | ||||
|  | ||||
| @ -6,6 +6,7 @@ entries: | ||||
|     designation: authorization | ||||
|     name: Authorize Application | ||||
|     title: Redirecting to %(app)s | ||||
|     authentication: require_authenticated | ||||
|   identifiers: | ||||
|     slug: default-provider-authorization-implicit-consent | ||||
|   model: authentik_flows.flow | ||||
|  | ||||
| @ -6,6 +6,7 @@ entries: | ||||
|     designation: authentication | ||||
|     name: Welcome to authentik! | ||||
|     title: Welcome to authentik! | ||||
|     authentication: require_unauthenticated | ||||
|   identifiers: | ||||
|     slug: default-source-authentication | ||||
|   model: authentik_flows.flow | ||||
|  | ||||
| @ -6,6 +6,7 @@ entries: | ||||
|     designation: enrollment | ||||
|     name: Welcome to authentik! Please select a username. | ||||
|     title: Welcome to authentik! Please select a username. | ||||
|     authentication: none | ||||
|   identifiers: | ||||
|     slug: default-source-enrollment | ||||
|   model: authentik_flows.flow | ||||
|  | ||||
| @ -6,6 +6,7 @@ entries: | ||||
|     designation: stage_configuration | ||||
|     name: Pre-Authentication | ||||
|     title: Pre-authentication | ||||
|     authentication: none | ||||
|   identifiers: | ||||
|     slug: default-source-pre-authentication | ||||
|   model: authentik_flows.flow | ||||
|  | ||||
| @ -6,6 +6,7 @@ entries: | ||||
|     designation: stage_configuration | ||||
|     name: User settings | ||||
|     title: Update your info | ||||
|     authentication: require_authenticated | ||||
|   identifiers: | ||||
|     slug: default-user-settings-flow | ||||
|   model: authentik_flows.flow | ||||
|  | ||||
| @ -12,6 +12,7 @@ entries: | ||||
|       name: Default enrollment Flow | ||||
|       title: Welcome to authentik! | ||||
|       designation: enrollment | ||||
|       authentication: require_unauthenticated | ||||
|   - identifiers: | ||||
|       field_key: username | ||||
|       label: Username | ||||
|  | ||||
| @ -12,6 +12,7 @@ entries: | ||||
|       name: Default enrollment Flow | ||||
|       title: Welcome to authentik! | ||||
|       designation: enrollment | ||||
|       authentication: require_unauthenticated | ||||
|   - identifiers: | ||||
|       field_key: username | ||||
|       label: Username | ||||
|  | ||||
| @ -12,6 +12,7 @@ entries: | ||||
|       name: Default Authentication Flow | ||||
|       title: Welcome to authentik! | ||||
|       designation: authentication | ||||
|       authentication: require_unauthenticated | ||||
|   - identifiers: | ||||
|       name: test-not-app-password | ||||
|     id: test-not-app-password | ||||
|  | ||||
| @ -12,6 +12,7 @@ entries: | ||||
|       name: Default Authentication Flow | ||||
|       title: Welcome to authentik! | ||||
|       designation: authentication | ||||
|       authentication: require_unauthenticated | ||||
|   - identifiers: | ||||
|       name: default-authentication-login | ||||
|     id: default-authentication-login | ||||
|  | ||||
| @ -12,6 +12,7 @@ entries: | ||||
|       name: Default recovery flow | ||||
|       title: Reset your password | ||||
|       designation: recovery | ||||
|       authentication: require_unauthenticated | ||||
|   - identifiers: | ||||
|       field_key: password | ||||
|       label: Password | ||||
|  | ||||
| @ -12,6 +12,7 @@ entries: | ||||
|       name: Default unenrollment flow | ||||
|       title: Delete your account | ||||
|       designation: unenrollment | ||||
|       authentication: require_authenticated | ||||
|   - identifiers: | ||||
|       name: default-unenrollment-user-delete | ||||
|     id: default-unenrollment-user-delete | ||||
|  | ||||
							
								
								
									
										22
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								schema.yml
									
									
									
									
									
								
							| @ -25269,6 +25269,13 @@ components: | ||||
|       - last_used | ||||
|       - user | ||||
|       - user_agent | ||||
|     AuthenticationEnum: | ||||
|       enum: | ||||
|       - none | ||||
|       - require_authenticated | ||||
|       - require_unauthenticated | ||||
|       - require_superuser | ||||
|       type: string | ||||
|     AuthenticatorAttachmentEnum: | ||||
|       enum: | ||||
|       - platform | ||||
| @ -27578,6 +27585,11 @@ components: | ||||
|           - $ref: '#/components/schemas/DeniedActionEnum' | ||||
|           description: Configure what should happen when a flow denies access to a | ||||
|             user. | ||||
|         authentication: | ||||
|           allOf: | ||||
|           - $ref: '#/components/schemas/AuthenticationEnum' | ||||
|           description: Required level of authentication and authorization to access | ||||
|             a flow. | ||||
|       required: | ||||
|       - background | ||||
|       - cache_count | ||||
| @ -27774,6 +27786,11 @@ components: | ||||
|           - $ref: '#/components/schemas/DeniedActionEnum' | ||||
|           description: Configure what should happen when a flow denies access to a | ||||
|             user. | ||||
|         authentication: | ||||
|           allOf: | ||||
|           - $ref: '#/components/schemas/AuthenticationEnum' | ||||
|           description: Required level of authentication and authorization to access | ||||
|             a flow. | ||||
|       required: | ||||
|       - designation | ||||
|       - name | ||||
| @ -33651,6 +33668,11 @@ components: | ||||
|           - $ref: '#/components/schemas/DeniedActionEnum' | ||||
|           description: Configure what should happen when a flow denies access to a | ||||
|             user. | ||||
|         authentication: | ||||
|           allOf: | ||||
|           - $ref: '#/components/schemas/AuthenticationEnum' | ||||
|           description: Required level of authentication and authorization to access | ||||
|             a flow. | ||||
|     PatchedFlowStageBindingRequest: | ||||
|       type: object | ||||
|       description: FlowStageBinding Serializer | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| import { DesignationToLabel, LayoutToLabel } from "@goauthentik/admin/flows/utils"; | ||||
| import { AuthenticationEnum } from "@goauthentik/api/dist/models/AuthenticationEnum"; | ||||
| import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; | ||||
| import { first } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/elements/forms/HorizontalFormElement"; | ||||
| @ -141,6 +142,37 @@ export class FlowForm extends ModelForm<Flow, string> { | ||||
|             </option>`; | ||||
|     } | ||||
|  | ||||
|     renderAuthentication(): TemplateResult { | ||||
|         return html` | ||||
|             <option | ||||
|                 value=${AuthenticationEnum.None} | ||||
|                 ?selected=${this.instance?.authentication === AuthenticationEnum.None} | ||||
|             > | ||||
|                 ${t`No requirement`} | ||||
|             </option> | ||||
|             <option | ||||
|                 value=${AuthenticationEnum.RequireAuthenticated} | ||||
|                 ?selected=${this.instance?.authentication === | ||||
|                 AuthenticationEnum.RequireAuthenticated} | ||||
|             > | ||||
|                 ${t`Require authentication`} | ||||
|             </option> | ||||
|             <option | ||||
|                 value=${AuthenticationEnum.RequireUnauthenticated} | ||||
|                 ?selected=${this.instance?.authentication === | ||||
|                 AuthenticationEnum.RequireUnauthenticated} | ||||
|             > | ||||
|                 ${t`Require no authentication.`} | ||||
|             </option> | ||||
|             <option | ||||
|                 value=${AuthenticationEnum.RequireSuperuser} | ||||
|                 ?selected=${this.instance?.authentication === AuthenticationEnum.RequireSuperuser} | ||||
|             > | ||||
|                 ${t`Require superuser.`} | ||||
|             </option> | ||||
|         `; | ||||
|     } | ||||
|  | ||||
|     renderLayout(): TemplateResult { | ||||
|         return html` | ||||
|             <option | ||||
| @ -224,6 +256,18 @@ export class FlowForm extends ModelForm<Flow, string> { | ||||
|                     </option> | ||||
|                 </select> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal | ||||
|                 label=${t`Authentication`} | ||||
|                 ?required=${true} | ||||
|                 name="authentication" | ||||
|             > | ||||
|                 <select class="pf-c-form-control"> | ||||
|                     ${this.renderAuthentication()} | ||||
|                 </select> | ||||
|                 <p class="pf-c-form__helper-text"> | ||||
|                     ${t`Required authentication level for this flow.`} | ||||
|                 </p> | ||||
|             </ak-form-element-horizontal> | ||||
|             <ak-form-element-horizontal | ||||
|                 label=${t`Designation`} | ||||
|                 ?required=${true} | ||||
|  | ||||
| @ -3802,6 +3802,10 @@ Changed response : **200 OK** | ||||
| -   sources/saml: set username field to name_id attribute | ||||
| -   web/common: disable API Drawer by default in user interface | ||||
|  | ||||
| ## Fixed in 2022.10.2 | ||||
|  | ||||
| -   \*: fix CVE-2022-46145 | ||||
|  | ||||
| ## Upgrading | ||||
|  | ||||
| This release does not introduce any new requirements. | ||||
|  | ||||
| @ -71,6 +71,10 @@ image: | ||||
| -   web/admin: fix error when importing duo devices | ||||
| -   web/admin: reset cookie_domain when setting non-domain forward auth | ||||
|  | ||||
| ## Fixed in 2022.11.2 | ||||
|  | ||||
| -   \*: fix CVE-2022-46145 | ||||
|  | ||||
| ## API Changes | ||||
|  | ||||
| #### What's Changed | ||||
|  | ||||
							
								
								
									
										19
									
								
								website/docs/security/CVE-2022-46145.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								website/docs/security/CVE-2022-46145.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | ||||
| # CVE-2022-46145 | ||||
|  | ||||
| ## Unauthorized user creation and potential account takeover | ||||
|  | ||||
| ### Impact | ||||
|  | ||||
| With the default flows, unauthenticated users can create new accounts in authentik. If a flow exists that allows for email-verified password recovery, this can be used to overwrite the email address of admin accounts and take over their accounts | ||||
|  | ||||
| ### Patches | ||||
|  | ||||
| authentik 2022.11.2 and 2022.10.2 fix this issue, for other versions the workaround can be used. | ||||
|  | ||||
| ### Workarounds | ||||
|  | ||||
| A policy can be created and bound to the `default-user-settings-flow` flow with the following contents | ||||
|  | ||||
| ```python | ||||
| return request.user.is_authenticated | ||||
| ``` | ||||
| @ -290,7 +290,7 @@ module.exports = { | ||||
|                 title: "Security", | ||||
|                 slug: "security", | ||||
|             }, | ||||
|             items: ["security/policy"], | ||||
|             items: ["security/policy", "security/CVE-2022-46145"], | ||||
|         }, | ||||
|     ], | ||||
| }; | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jens L
					Jens L