*: backport CVE-2022-46145 fix
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
cc8dc1403f
commit
87b8ca7be4
|
@ -72,6 +72,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
|
||||
|
||||
|
@ -116,11 +123,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
|
@ -25215,6 +25215,13 @@ components:
|
|||
- last_used
|
||||
- user
|
||||
- user_agent
|
||||
AuthenticationEnum:
|
||||
enum:
|
||||
- none
|
||||
- require_authenticated
|
||||
- require_unauthenticated
|
||||
- require_superuser
|
||||
type: string
|
||||
AuthenticatorAttachmentEnum:
|
||||
enum:
|
||||
- platform
|
||||
|
@ -27512,6 +27519,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
|
||||
|
@ -27804,6 +27816,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
|
||||
|
@ -33535,6 +33552,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}
|
||||
|
|
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
|
||||
```
|
|
@ -289,7 +289,7 @@ module.exports = {
|
|||
title: "Security",
|
||||
slug: "security",
|
||||
},
|
||||
items: ["security/policy"],
|
||||
items: ["security/policy", "security/CVE-2022-46145"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
Reference in a new issue