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:
Jens L 2022-12-02 16:14:25 +01:00 committed by GitHub
parent 860c85d012
commit db95dfe38d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 215 additions and 8 deletions

View File

@ -71,6 +71,7 @@ class FlowSerializer(ModelSerializer):
"export_url",
"layout",
"denied_action",
"authentication",
]
extra_kwargs = {
"background": {"read_only": True},

View File

@ -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)

View 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.",
),
),
]

View File

@ -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

View File

@ -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:

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -12,6 +12,7 @@ entries:
name: Default enrollment Flow
title: Welcome to authentik!
designation: enrollment
authentication: require_unauthenticated
- identifiers:
field_key: username
label: Username

View File

@ -12,6 +12,7 @@ entries:
name: Default enrollment Flow
title: Welcome to authentik!
designation: enrollment
authentication: require_unauthenticated
- identifiers:
field_key: username
label: Username

View File

@ -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

View File

@ -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

View File

@ -12,6 +12,7 @@ entries:
name: Default recovery flow
title: Reset your password
designation: recovery
authentication: require_unauthenticated
- identifiers:
field_key: password
label: Password

View File

@ -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

View File

@ -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

View File

@ -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}

View File

@ -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.

View File

@ -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

View 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
```

View File

@ -290,7 +290,7 @@ module.exports = {
title: "Security",
slug: "security",
},
items: ["security/policy"],
items: ["security/policy", "security/CVE-2022-46145"],
},
],
};