*: backport CVE-2022-46145 fix

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2022-12-01 10:40:51 +02:00
parent cc8dc1403f
commit 87b8ca7be4
28 changed files with 207 additions and 8 deletions

View file

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

View file

@ -1,4 +1,6 @@
"""flow exceptions""" """flow exceptions"""
from typing import Optional
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from authentik.lib.sentry import SentryIgnoredException from authentik.lib.sentry import SentryIgnoredException
@ -6,15 +8,15 @@ from authentik.policies.types import PolicyResult
class FlowNonApplicableException(SentryIgnoredException): 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 @property
def messages(self) -> str: def messages(self) -> str:
"""Get messages from policy result, fallback to generic reason""" """Get messages from policy result, fallback to generic reason"""
if len(self.policy_result.messages) < 1: if not self.policy_result or len(self.policy_result.messages) < 1:
return _("Flow does not apply to current user (denied by policy).") return _("Flow does not apply to current user.")
return "\n".join(self.policy_result.messages) 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() 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): class NotConfiguredAction(models.TextChoices):
"""Decides how the FlowExecutor should proceed when a stage isn't configured""" """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."), 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 @property
def background_url(self) -> str: def background_url(self) -> str:
"""Get the URL to the background image. If the name is /static or starts with http """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.apps import HIST_FLOWS_PLAN_TIME
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from authentik.flows.markers import ReevaluateMarker, StageMarker 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.lib.config import CONFIG
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
@ -116,11 +123,30 @@ class FlowPlanner:
self.flow = flow self.flow = flow
self._logger = get_logger().bind(flow_slug=flow.slug) 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( def plan(
self, request: HttpRequest, default_context: Optional[dict[str, Any]] = None self, request: HttpRequest, default_context: Optional[dict[str, Any]] = None
) -> FlowPlan: ) -> FlowPlan:
"""Check each of the flows' policies, check policies for each stage with PolicyBinding """Check each of the flows' policies, check policies for each stage with PolicyBinding
and return ordered list""" and return ordered list"""
self._check_authentication(request)
with Hub.current.start_span( with Hub.current.start_span(
op="authentik.flow.planner.plan", description=self.flow.slug op="authentik.flow.planner.plan", description=self.flow.slug
) as span: ) as span:

View file

@ -1,6 +1,7 @@
"""flow planner tests""" """flow planner tests"""
from unittest.mock import MagicMock, Mock, PropertyMock, patch from unittest.mock import MagicMock, Mock, PropertyMock, patch
from django.contrib.auth.models import AnonymousUser
from django.contrib.sessions.middleware import SessionMiddleware from django.contrib.sessions.middleware import SessionMiddleware
from django.core.cache import cache from django.core.cache import cache
from django.test import RequestFactory, TestCase from django.test import RequestFactory, TestCase
@ -8,10 +9,10 @@ from django.urls import reverse
from guardian.shortcuts import get_anonymous_user from guardian.shortcuts import get_anonymous_user
from authentik.core.models import 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.exceptions import EmptyFlowException, FlowNonApplicableException
from authentik.flows.markers import ReevaluateMarker, StageMarker 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.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
from authentik.lib.tests.utils import dummy_get_response from authentik.lib.tests.utils import dummy_get_response
from authentik.policies.dummy.models import DummyPolicy from authentik.policies.dummy.models import DummyPolicy
@ -43,6 +44,30 @@ class TestFlowPlanner(TestCase):
planner = FlowPlanner(flow) planner = FlowPlanner(flow)
planner.plan(request) 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( @patch(
"authentik.policies.engine.PolicyEngine.result", "authentik.policies.engine.PolicyEngine.result",
POLICY_RETURN_FALSE, POLICY_RETURN_FALSE,

View file

@ -6,6 +6,7 @@ entries:
designation: stage_configuration designation: stage_configuration
name: Change Password name: Change Password
title: Change password title: Change password
authentication: require_authenticated
identifiers: identifiers:
slug: default-password-change slug: default-password-change
model: authentik_flows.flow model: authentik_flows.flow

View file

@ -11,6 +11,7 @@ entries:
designation: authentication designation: authentication
name: Welcome to authentik! name: Welcome to authentik!
title: Welcome to authentik! title: Welcome to authentik!
authentication: require_unauthenticated
identifiers: identifiers:
slug: default-authentication-flow slug: default-authentication-flow
model: authentik_flows.flow model: authentik_flows.flow

View file

@ -6,6 +6,7 @@ entries:
designation: invalidation designation: invalidation
name: Logout name: Logout
title: Default Invalidation Flow title: Default Invalidation Flow
authentication: require_authenticated
identifiers: identifiers:
slug: default-invalidation-flow slug: default-invalidation-flow
model: authentik_flows.flow model: authentik_flows.flow

View file

@ -6,6 +6,7 @@ entries:
designation: stage_configuration designation: stage_configuration
name: default-authenticator-static-setup name: default-authenticator-static-setup
title: Setup Static OTP Tokens title: Setup Static OTP Tokens
authentication: require_authenticated
identifiers: identifiers:
slug: default-authenticator-static-setup slug: default-authenticator-static-setup
model: authentik_flows.flow model: authentik_flows.flow

View file

@ -6,6 +6,7 @@ entries:
designation: stage_configuration designation: stage_configuration
name: default-authenticator-totp-setup name: default-authenticator-totp-setup
title: Setup Two-Factor authentication title: Setup Two-Factor authentication
authentication: require_authenticated
identifiers: identifiers:
slug: default-authenticator-totp-setup slug: default-authenticator-totp-setup
model: authentik_flows.flow model: authentik_flows.flow

View file

@ -6,6 +6,7 @@ entries:
designation: stage_configuration designation: stage_configuration
name: default-authenticator-webauthn-setup name: default-authenticator-webauthn-setup
title: Setup WebAuthn title: Setup WebAuthn
authentication: require_authenticated
identifiers: identifiers:
slug: default-authenticator-webauthn-setup slug: default-authenticator-webauthn-setup
model: authentik_flows.flow model: authentik_flows.flow

View file

@ -6,6 +6,7 @@ entries:
designation: authorization designation: authorization
name: Authorize Application name: Authorize Application
title: Redirecting to %(app)s title: Redirecting to %(app)s
authentication: require_authenticated
identifiers: identifiers:
slug: default-provider-authorization-explicit-consent slug: default-provider-authorization-explicit-consent
model: authentik_flows.flow model: authentik_flows.flow

View file

@ -6,6 +6,7 @@ entries:
designation: authorization designation: authorization
name: Authorize Application name: Authorize Application
title: Redirecting to %(app)s title: Redirecting to %(app)s
authentication: require_authenticated
identifiers: identifiers:
slug: default-provider-authorization-implicit-consent slug: default-provider-authorization-implicit-consent
model: authentik_flows.flow model: authentik_flows.flow

View file

@ -6,6 +6,7 @@ entries:
designation: authentication designation: authentication
name: Welcome to authentik! name: Welcome to authentik!
title: Welcome to authentik! title: Welcome to authentik!
authentication: require_unauthenticated
identifiers: identifiers:
slug: default-source-authentication slug: default-source-authentication
model: authentik_flows.flow model: authentik_flows.flow

View file

@ -6,6 +6,7 @@ entries:
designation: enrollment designation: enrollment
name: Welcome to authentik! Please select a username. name: Welcome to authentik! Please select a username.
title: Welcome to authentik! Please select a username. title: Welcome to authentik! Please select a username.
authentication: none
identifiers: identifiers:
slug: default-source-enrollment slug: default-source-enrollment
model: authentik_flows.flow model: authentik_flows.flow

View file

@ -6,6 +6,7 @@ entries:
designation: stage_configuration designation: stage_configuration
name: Pre-Authentication name: Pre-Authentication
title: Pre-authentication title: Pre-authentication
authentication: none
identifiers: identifiers:
slug: default-source-pre-authentication slug: default-source-pre-authentication
model: authentik_flows.flow model: authentik_flows.flow

View file

@ -6,6 +6,7 @@ entries:
designation: stage_configuration designation: stage_configuration
name: User settings name: User settings
title: Update your info title: Update your info
authentication: require_authenticated
identifiers: identifiers:
slug: default-user-settings-flow slug: default-user-settings-flow
model: authentik_flows.flow model: authentik_flows.flow

View file

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

View file

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

View file

@ -12,6 +12,7 @@ entries:
name: Default Authentication Flow name: Default Authentication Flow
title: Welcome to authentik! title: Welcome to authentik!
designation: authentication designation: authentication
authentication: require_unauthenticated
- identifiers: - identifiers:
name: test-not-app-password name: test-not-app-password
id: test-not-app-password id: test-not-app-password

View file

@ -12,6 +12,7 @@ entries:
name: Default Authentication Flow name: Default Authentication Flow
title: Welcome to authentik! title: Welcome to authentik!
designation: authentication designation: authentication
authentication: require_unauthenticated
- identifiers: - identifiers:
name: default-authentication-login name: default-authentication-login
id: default-authentication-login id: default-authentication-login

View file

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

View file

@ -12,6 +12,7 @@ entries:
name: Default unenrollment flow name: Default unenrollment flow
title: Delete your account title: Delete your account
designation: unenrollment designation: unenrollment
authentication: require_authenticated
- identifiers: - identifiers:
name: default-unenrollment-user-delete name: default-unenrollment-user-delete
id: default-unenrollment-user-delete id: default-unenrollment-user-delete

View file

@ -25215,6 +25215,13 @@ components:
- last_used - last_used
- user - user
- user_agent - user_agent
AuthenticationEnum:
enum:
- none
- require_authenticated
- require_unauthenticated
- require_superuser
type: string
AuthenticatorAttachmentEnum: AuthenticatorAttachmentEnum:
enum: enum:
- platform - platform
@ -27512,6 +27519,11 @@ components:
- $ref: '#/components/schemas/DeniedActionEnum' - $ref: '#/components/schemas/DeniedActionEnum'
description: Configure what should happen when a flow denies access to a description: Configure what should happen when a flow denies access to a
user. user.
authentication:
allOf:
- $ref: '#/components/schemas/AuthenticationEnum'
description: Required level of authentication and authorization to access
a flow.
required: required:
- background - background
- cache_count - cache_count
@ -27804,6 +27816,11 @@ components:
- $ref: '#/components/schemas/DeniedActionEnum' - $ref: '#/components/schemas/DeniedActionEnum'
description: Configure what should happen when a flow denies access to a description: Configure what should happen when a flow denies access to a
user. user.
authentication:
allOf:
- $ref: '#/components/schemas/AuthenticationEnum'
description: Required level of authentication and authorization to access
a flow.
required: required:
- designation - designation
- name - name
@ -33535,6 +33552,11 @@ components:
- $ref: '#/components/schemas/DeniedActionEnum' - $ref: '#/components/schemas/DeniedActionEnum'
description: Configure what should happen when a flow denies access to a description: Configure what should happen when a flow denies access to a
user. user.
authentication:
allOf:
- $ref: '#/components/schemas/AuthenticationEnum'
description: Required level of authentication and authorization to access
a flow.
PatchedFlowStageBindingRequest: PatchedFlowStageBindingRequest:
type: object type: object
description: FlowStageBinding Serializer description: FlowStageBinding Serializer

View file

@ -1,4 +1,5 @@
import { DesignationToLabel, LayoutToLabel } from "@goauthentik/admin/flows/utils"; 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 { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
@ -141,6 +142,37 @@ export class FlowForm extends ModelForm<Flow, string> {
</option>`; </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 { renderLayout(): TemplateResult {
return html` return html`
<option <option
@ -224,6 +256,18 @@ export class FlowForm extends ModelForm<Flow, string> {
</option> </option>
</select> </select>
</ak-form-element-horizontal> </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 <ak-form-element-horizontal
label=${t`Designation`} label=${t`Designation`}
?required=${true} ?required=${true}

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

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