security: fix CVE 2022 23555 (#4274)
* add flow to invitation Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * show warning on invitation page Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add security advisory Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add tests Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		| @ -8,6 +8,7 @@ from rest_framework.viewsets import ModelViewSet | |||||||
| from authentik.core.api.groups import GroupMemberSerializer | from authentik.core.api.groups import GroupMemberSerializer | ||||||
| from authentik.core.api.used_by import UsedByMixin | from authentik.core.api.used_by import UsedByMixin | ||||||
| from authentik.core.api.utils import is_dict | from authentik.core.api.utils import is_dict | ||||||
|  | from authentik.flows.api.flows import FlowSerializer | ||||||
| from authentik.flows.api.stages import StageSerializer | from authentik.flows.api.stages import StageSerializer | ||||||
| from authentik.stages.invitation.models import Invitation, InvitationStage | from authentik.stages.invitation.models import Invitation, InvitationStage | ||||||
|  |  | ||||||
| @ -49,6 +50,7 @@ class InvitationSerializer(ModelSerializer): | |||||||
|  |  | ||||||
|     created_by = GroupMemberSerializer(read_only=True) |     created_by = GroupMemberSerializer(read_only=True) | ||||||
|     fixed_data = JSONField(validators=[is_dict], required=False) |     fixed_data = JSONField(validators=[is_dict], required=False) | ||||||
|  |     flow_obj = FlowSerializer(read_only=True, required=False, source="flow") | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|  |  | ||||||
| @ -60,6 +62,8 @@ class InvitationSerializer(ModelSerializer): | |||||||
|             "fixed_data", |             "fixed_data", | ||||||
|             "created_by", |             "created_by", | ||||||
|             "single_use", |             "single_use", | ||||||
|  |             "flow", | ||||||
|  |             "flow_obj", | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -69,8 +73,8 @@ class InvitationViewSet(UsedByMixin, ModelViewSet): | |||||||
|     queryset = Invitation.objects.all() |     queryset = Invitation.objects.all() | ||||||
|     serializer_class = InvitationSerializer |     serializer_class = InvitationSerializer | ||||||
|     ordering = ["-expires"] |     ordering = ["-expires"] | ||||||
|     search_fields = ["name", "created_by__username", "expires"] |     search_fields = ["name", "created_by__username", "expires", "flow__slug"] | ||||||
|     filterset_fields = ["name", "created_by__username", "expires"] |     filterset_fields = ["name", "created_by__username", "expires", "flow__slug"] | ||||||
|  |  | ||||||
|     def perform_create(self, serializer: InvitationSerializer): |     def perform_create(self, serializer: InvitationSerializer): | ||||||
|         serializer.save(created_by=self.request.user) |         serializer.save(created_by=self.request.user) | ||||||
|  | |||||||
| @ -0,0 +1,26 @@ | |||||||
|  | # Generated by Django 4.1.4 on 2022-12-20 13:43 | ||||||
|  |  | ||||||
|  | import django.db.models.deletion | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_flows", "0024_flow_authentication"), | ||||||
|  |         ("authentik_stages_invitation", "0001_squashed_0006_invitation_name"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name="invitation", | ||||||
|  |             name="flow", | ||||||
|  |             field=models.ForeignKey( | ||||||
|  |                 default=None, | ||||||
|  |                 help_text="When set, only the configured flow can use this invitation.", | ||||||
|  |                 null=True, | ||||||
|  |                 on_delete=django.db.models.deletion.SET_DEFAULT, | ||||||
|  |                 to="authentik_flows.flow", | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -55,6 +55,13 @@ class Invitation(SerializerModel, ExpiringModel): | |||||||
|  |  | ||||||
|     name = models.SlugField() |     name = models.SlugField() | ||||||
|  |  | ||||||
|  |     flow = models.ForeignKey( | ||||||
|  |         "authentik_flows.Flow", | ||||||
|  |         default=None, | ||||||
|  |         null=True, | ||||||
|  |         on_delete=models.SET_DEFAULT, | ||||||
|  |         help_text=_("When set, only the configured flow can use this invitation."), | ||||||
|  |     ) | ||||||
|     single_use = models.BooleanField( |     single_use = models.BooleanField( | ||||||
|         default=False, |         default=False, | ||||||
|         help_text=_("When enabled, the invitation will be deleted after usage."), |         help_text=_("When enabled, the invitation will be deleted after usage."), | ||||||
|  | |||||||
| @ -35,22 +35,30 @@ class InvitationStageView(StageView): | |||||||
|             return self.executor.plan.context[PLAN_CONTEXT_PROMPT][INVITATION_TOKEN_KEY_CONTEXT] |             return self.executor.plan.context[PLAN_CONTEXT_PROMPT][INVITATION_TOKEN_KEY_CONTEXT] | ||||||
|         return None |         return None | ||||||
|  |  | ||||||
|  |     def get_invite(self) -> Optional[Invitation]: | ||||||
|  |         """Check the token, find the invite and check it's flow""" | ||||||
|  |         token = self.get_token() | ||||||
|  |         if not token: | ||||||
|  |             return None | ||||||
|  |         invite: Invitation = Invitation.objects.filter(pk=token).first() | ||||||
|  |         if not invite: | ||||||
|  |             self.logger.debug("invalid invitation", token=token) | ||||||
|  |             return None | ||||||
|  |         if invite.flow and invite.flow.pk != self.executor.plan.flow_pk: | ||||||
|  |             self.logger.debug("invite for incorrect flow", expected=invite.flow.slug) | ||||||
|  |             return None | ||||||
|  |         return invite | ||||||
|  |  | ||||||
|     def get(self, request: HttpRequest) -> HttpResponse: |     def get(self, request: HttpRequest) -> HttpResponse: | ||||||
|         """Apply data to the current flow based on a URL""" |         """Apply data to the current flow based on a URL""" | ||||||
|         stage: InvitationStage = self.executor.current_stage |         stage: InvitationStage = self.executor.current_stage | ||||||
|         token = self.get_token() |  | ||||||
|         if not token: |         invite = self.get_invite() | ||||||
|             # No Invitation was given, raise error or continue |         if not invite: | ||||||
|             if stage.continue_flow_without_invitation: |             if stage.continue_flow_without_invitation: | ||||||
|                 return self.executor.stage_ok() |                 return self.executor.stage_ok() | ||||||
|             return self.executor.stage_invalid() |             return self.executor.stage_invalid() | ||||||
|  |  | ||||||
|         invite: Invitation = Invitation.objects.filter(pk=token).first() |  | ||||||
|         if not invite: |  | ||||||
|             self.logger.debug("invalid invitation", token=token) |  | ||||||
|             if stage.continue_flow_without_invitation: |  | ||||||
|                 return self.executor.stage_ok() |  | ||||||
|             return self.executor.stage_invalid() |  | ||||||
|         self.executor.plan.context[INVITATION_IN_EFFECT] = True |         self.executor.plan.context[INVITATION_IN_EFFECT] = True | ||||||
|         self.executor.plan.context[INVITATION] = invite |         self.executor.plan.context[INVITATION] = invite | ||||||
|  |  | ||||||
|  | |||||||
| @ -23,7 +23,7 @@ from authentik.stages.password import BACKEND_INBUILT | |||||||
| from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND | from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestUserLoginStage(FlowTestCase): | class TestInvitationStage(FlowTestCase): | ||||||
|     """Login tests""" |     """Login tests""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
| @ -98,6 +98,33 @@ class TestUserLoginStage(FlowTestCase): | |||||||
|         self.assertEqual(response.status_code, 200) |         self.assertEqual(response.status_code, 200) | ||||||
|         self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) |         self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) | ||||||
|  |  | ||||||
|  |     def test_invalid_flow(self): | ||||||
|  |         """Test with invitation, invalid flow limit""" | ||||||
|  |         invalid_flow = create_test_flow(FlowDesignation.ENROLLMENT) | ||||||
|  |         plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) | ||||||
|  |         session = self.client.session | ||||||
|  |         session[SESSION_KEY_PLAN] = plan | ||||||
|  |         session.save() | ||||||
|  |  | ||||||
|  |         data = {"foo": "bar"} | ||||||
|  |         invite = Invitation.objects.create( | ||||||
|  |             created_by=get_anonymous_user(), fixed_data=data, flow=invalid_flow | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()): | ||||||
|  |             base_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) | ||||||
|  |             args = urlencode({INVITATION_TOKEN_KEY: invite.pk.hex}) | ||||||
|  |             response = self.client.get(base_url + f"?query={args}") | ||||||
|  |  | ||||||
|  |         session = self.client.session | ||||||
|  |         plan: FlowPlan = session[SESSION_KEY_PLAN] | ||||||
|  |  | ||||||
|  |         self.assertStageResponse( | ||||||
|  |             response, | ||||||
|  |             flow=self.flow, | ||||||
|  |             component="ak-stage-access-denied", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     def test_with_invitation_prompt_data(self): |     def test_with_invitation_prompt_data(self): | ||||||
|         """Test with invitation, check data in session""" |         """Test with invitation, check data in session""" | ||||||
|         data = {"foo": "bar"} |         data = {"foo": "bar"} | ||||||
|  | |||||||
							
								
								
									
										24
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								schema.yml
									
									
									
									
									
								
							| @ -22391,6 +22391,10 @@ paths: | |||||||
|         schema: |         schema: | ||||||
|           type: string |           type: string | ||||||
|           format: date-time |           format: date-time | ||||||
|  |       - in: query | ||||||
|  |         name: flow__slug | ||||||
|  |         schema: | ||||||
|  |           type: string | ||||||
|       - in: query |       - in: query | ||||||
|         name: name |         name: name | ||||||
|         schema: |         schema: | ||||||
| @ -28531,8 +28535,18 @@ components: | |||||||
|         single_use: |         single_use: | ||||||
|           type: boolean |           type: boolean | ||||||
|           description: When enabled, the invitation will be deleted after usage. |           description: When enabled, the invitation will be deleted after usage. | ||||||
|  |         flow: | ||||||
|  |           type: string | ||||||
|  |           format: uuid | ||||||
|  |           nullable: true | ||||||
|  |           description: When set, only the configured flow can use this invitation. | ||||||
|  |         flow_obj: | ||||||
|  |           allOf: | ||||||
|  |           - $ref: '#/components/schemas/Flow' | ||||||
|  |           readOnly: true | ||||||
|       required: |       required: | ||||||
|       - created_by |       - created_by | ||||||
|  |       - flow_obj | ||||||
|       - name |       - name | ||||||
|       - pk |       - pk | ||||||
|     InvitationRequest: |     InvitationRequest: | ||||||
| @ -28553,6 +28567,11 @@ components: | |||||||
|         single_use: |         single_use: | ||||||
|           type: boolean |           type: boolean | ||||||
|           description: When enabled, the invitation will be deleted after usage. |           description: When enabled, the invitation will be deleted after usage. | ||||||
|  |         flow: | ||||||
|  |           type: string | ||||||
|  |           format: uuid | ||||||
|  |           nullable: true | ||||||
|  |           description: When set, only the configured flow can use this invitation. | ||||||
|       required: |       required: | ||||||
|       - name |       - name | ||||||
|     InvitationStage: |     InvitationStage: | ||||||
| @ -33904,6 +33923,11 @@ components: | |||||||
|         single_use: |         single_use: | ||||||
|           type: boolean |           type: boolean | ||||||
|           description: When enabled, the invitation will be deleted after usage. |           description: When enabled, the invitation will be deleted after usage. | ||||||
|  |         flow: | ||||||
|  |           type: string | ||||||
|  |           format: uuid | ||||||
|  |           nullable: true | ||||||
|  |           description: When set, only the configured flow can use this invitation. | ||||||
|     PatchedInvitationStageRequest: |     PatchedInvitationStageRequest: | ||||||
|       type: object |       type: object | ||||||
|       description: InvitationStage Serializer |       description: InvitationStage Serializer | ||||||
|  | |||||||
| @ -9,8 +9,15 @@ import { t } from "@lingui/macro"; | |||||||
|  |  | ||||||
| import { TemplateResult, html } from "lit"; | import { TemplateResult, html } from "lit"; | ||||||
| import { customElement } from "lit/decorators.js"; | import { customElement } from "lit/decorators.js"; | ||||||
|  | import { ifDefined } from "lit/directives/if-defined.js"; | ||||||
|  | import { until } from "lit/directives/until.js"; | ||||||
|  |  | ||||||
| import { Invitation, StagesApi } from "@goauthentik/api"; | import { | ||||||
|  |     FlowsApi, | ||||||
|  |     FlowsInstancesListDesignationEnum, | ||||||
|  |     Invitation, | ||||||
|  |     StagesApi, | ||||||
|  | } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| @customElement("ak-invitation-form") | @customElement("ak-invitation-form") | ||||||
| export class InvitationForm extends ModelForm<Invitation, string> { | export class InvitationForm extends ModelForm<Invitation, string> { | ||||||
| @ -66,6 +73,34 @@ export class InvitationForm extends ModelForm<Invitation, string> { | |||||||
|                     value="${dateTimeLocal(first(this.instance?.expires, new Date()))}" |                     value="${dateTimeLocal(first(this.instance?.expires, new Date()))}" | ||||||
|                 /> |                 /> | ||||||
|             </ak-form-element-horizontal> |             </ak-form-element-horizontal> | ||||||
|  |             <ak-form-element-horizontal label=${t`Flow`} ?required=${true} name="flow"> | ||||||
|  |                 <select class="pf-c-form-control"> | ||||||
|  |                     <option value="" ?selected=${this.instance?.flow === undefined}> | ||||||
|  |                         --------- | ||||||
|  |                     </option> | ||||||
|  |                     ${until( | ||||||
|  |                         new FlowsApi(DEFAULT_CONFIG) | ||||||
|  |                             .flowsInstancesList({ | ||||||
|  |                                 ordering: "slug", | ||||||
|  |                                 designation: FlowsInstancesListDesignationEnum.Enrollment, | ||||||
|  |                             }) | ||||||
|  |                             .then((flows) => { | ||||||
|  |                                 return flows.results.map((flow) => { | ||||||
|  |                                     return html`<option | ||||||
|  |                                         value=${ifDefined(flow.pk)} | ||||||
|  |                                         ?selected=${this.instance?.flow === flow.pk} | ||||||
|  |                                     > | ||||||
|  |                                         ${flow.name} (${flow.slug}) | ||||||
|  |                                     </option>`; | ||||||
|  |                                 }); | ||||||
|  |                             }), | ||||||
|  |                         html`<option>${t`Loading...`}</option>`, | ||||||
|  |                     )} | ||||||
|  |                 </select> | ||||||
|  |                 <p class="pf-c-form__helper-text"> | ||||||
|  |                     ${t`When selected, the invite will only be usable with the flow. By default the invite is accepted on all flows with invitation stages.`} | ||||||
|  |                 </p> | ||||||
|  |             </ak-form-element-horizontal> | ||||||
|             <ak-form-element-horizontal label=${t`Attributes`} name="fixedData"> |             <ak-form-element-horizontal label=${t`Attributes`} name="fixedData"> | ||||||
|                 <ak-codemirror |                 <ak-codemirror | ||||||
|                     mode="yaml" |                     mode="yaml" | ||||||
|  | |||||||
| @ -14,12 +14,12 @@ import PFFormControl from "@patternfly/patternfly/components/FormControl/form-co | |||||||
| import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css"; | import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css"; | ||||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||||
|  |  | ||||||
| import { StagesApi } from "@goauthentik/api"; | import { Invitation, StagesApi } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| @customElement("ak-stage-invitation-list-link") | @customElement("ak-stage-invitation-list-link") | ||||||
| export class InvitationListLink extends AKElement { | export class InvitationListLink extends AKElement { | ||||||
|     @property() |     @property({ attribute: false }) | ||||||
|     invitation?: string; |     invitation?: Invitation; | ||||||
|  |  | ||||||
|     @property() |     @property() | ||||||
|     selectedFlow?: string; |     selectedFlow?: string; | ||||||
| @ -29,12 +29,14 @@ export class InvitationListLink extends AKElement { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderLink(): string { |     renderLink(): string { | ||||||
|         return `${window.location.protocol}//${window.location.host}/if/flow/${this.selectedFlow}/?itoken=${this.invitation}`; |         if (this.invitation?.flowObj) { | ||||||
|  |             this.selectedFlow = this.invitation.flowObj?.slug; | ||||||
|  |         } | ||||||
|  |         return `${window.location.protocol}//${window.location.host}/if/flow/${this.selectedFlow}/?itoken=${this.invitation?.pk}`; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     render(): TemplateResult { |     renderFlowSelector(): TemplateResult { | ||||||
|         return html`<dl class="pf-c-description-list pf-m-horizontal"> |         return html`<div class="pf-c-description-list__group"> | ||||||
|             <div class="pf-c-description-list__group"> |  | ||||||
|             <dt class="pf-c-description-list__term"> |             <dt class="pf-c-description-list__term"> | ||||||
|                 <span class="pf-c-description-list__text">${t`Select an enrollment flow`}</span> |                 <span class="pf-c-description-list__text">${t`Select an enrollment flow`}</span> | ||||||
|             </dt> |             </dt> | ||||||
| @ -82,7 +84,12 @@ export class InvitationListLink extends AKElement { | |||||||
|                     </select> |                     </select> | ||||||
|                 </div> |                 </div> | ||||||
|             </dd> |             </dd> | ||||||
|             </div> |         </div>`; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     render(): TemplateResult { | ||||||
|  |         return html`<dl class="pf-c-description-list pf-m-horizontal"> | ||||||
|  |             ${this.invitation?.flow === undefined ? this.renderFlowSelector() : html``} | ||||||
|             <div class="pf-c-description-list__group"> |             <div class="pf-c-description-list__group"> | ||||||
|                 <dt class="pf-c-description-list__term"> |                 <dt class="pf-c-description-list__term"> | ||||||
|                     <span class="pf-c-description-list__text" |                     <span class="pf-c-description-list__text" | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ import "@goauthentik/admin/stages/invitation/InvitationForm"; | |||||||
| import "@goauthentik/admin/stages/invitation/InvitationListLink"; | import "@goauthentik/admin/stages/invitation/InvitationListLink"; | ||||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||||
| import { uiConfig } from "@goauthentik/common/ui/config"; | import { uiConfig } from "@goauthentik/common/ui/config"; | ||||||
|  | import { PFColor } from "@goauthentik/elements/Label"; | ||||||
| import "@goauthentik/elements/buttons/ModalButton"; | import "@goauthentik/elements/buttons/ModalButton"; | ||||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | import "@goauthentik/elements/buttons/SpinnerButton"; | ||||||
| import "@goauthentik/elements/forms/DeleteBulkForm"; | import "@goauthentik/elements/forms/DeleteBulkForm"; | ||||||
| @ -18,7 +19,7 @@ import { ifDefined } from "lit/directives/if-defined.js"; | |||||||
|  |  | ||||||
| import PFBanner from "@patternfly/patternfly/components/Banner/banner.css"; | import PFBanner from "@patternfly/patternfly/components/Banner/banner.css"; | ||||||
|  |  | ||||||
| import { Invitation, StagesApi } from "@goauthentik/api"; | import { FlowDesignationEnum, Invitation, StagesApi } from "@goauthentik/api"; | ||||||
|  |  | ||||||
| @customElement("ak-stage-invitation-list") | @customElement("ak-stage-invitation-list") | ||||||
| export class InvitationListPage extends TablePage<Invitation> { | export class InvitationListPage extends TablePage<Invitation> { | ||||||
| @ -49,12 +50,24 @@ export class InvitationListPage extends TablePage<Invitation> { | |||||||
|     @state() |     @state() | ||||||
|     invitationStageExists = false; |     invitationStageExists = false; | ||||||
|  |  | ||||||
|  |     @state() | ||||||
|  |     multipleEnrollmentFlows = false; | ||||||
|  |  | ||||||
|     async apiEndpoint(page: number): Promise<PaginatedResponse<Invitation>> { |     async apiEndpoint(page: number): Promise<PaginatedResponse<Invitation>> { | ||||||
|  |         // Check if any invitation stages exist | ||||||
|         const stages = await new StagesApi(DEFAULT_CONFIG).stagesInvitationStagesList({ |         const stages = await new StagesApi(DEFAULT_CONFIG).stagesInvitationStagesList({ | ||||||
|             noFlows: false, |             noFlows: false, | ||||||
|         }); |         }); | ||||||
|         this.invitationStageExists = stages.pagination.count > 0; |         this.invitationStageExists = stages.pagination.count > 0; | ||||||
|         this.expandable = this.invitationStageExists; |         this.expandable = this.invitationStageExists; | ||||||
|  |         stages.results.forEach((stage) => { | ||||||
|  |             const enrollmentFlows = (stage.flowSet || []).filter( | ||||||
|  |                 (flow) => flow.designation === FlowDesignationEnum.Enrollment, | ||||||
|  |             ); | ||||||
|  |             if (enrollmentFlows.length > 1) { | ||||||
|  |                 this.multipleEnrollmentFlows = true; | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|         return new StagesApi(DEFAULT_CONFIG).stagesInvitationInvitationsList({ |         return new StagesApi(DEFAULT_CONFIG).stagesInvitationInvitationsList({ | ||||||
|             ordering: this.order, |             ordering: this.order, | ||||||
|             page: page, |             page: page, | ||||||
| @ -96,7 +109,14 @@ export class InvitationListPage extends TablePage<Invitation> { | |||||||
|  |  | ||||||
|     row(item: Invitation): TemplateResult[] { |     row(item: Invitation): TemplateResult[] { | ||||||
|         return [ |         return [ | ||||||
|             html`${item.name}`, |             html`<div>${item.name}</div> | ||||||
|  |                 ${!item.flowObj && this.multipleEnrollmentFlows | ||||||
|  |                     ? html` | ||||||
|  |                           <ak-label color=${PFColor.Orange}> | ||||||
|  |                               ${t`Invitation not limited to any flow, and can be used with any enrollment flow.`} | ||||||
|  |                           </ak-label> | ||||||
|  |                       ` | ||||||
|  |                     : html``}`, | ||||||
|             html`${item.createdBy?.username}`, |             html`${item.createdBy?.username}`, | ||||||
|             html`${item.expires?.toLocaleString() || t`-`}`, |             html`${item.expires?.toLocaleString() || t`-`}`, | ||||||
|             html` <ak-forms-modal> |             html` <ak-forms-modal> | ||||||
| @ -114,7 +134,7 @@ export class InvitationListPage extends TablePage<Invitation> { | |||||||
|         return html` <td role="cell" colspan="3"> |         return html` <td role="cell" colspan="3"> | ||||||
|                 <div class="pf-c-table__expandable-row-content"> |                 <div class="pf-c-table__expandable-row-content"> | ||||||
|                     <ak-stage-invitation-list-link |                     <ak-stage-invitation-list-link | ||||||
|                         invitation=${item.pk} |                         .invitation=${item} | ||||||
|                     ></ak-stage-invitation-list-link> |                     ></ak-stage-invitation-list-link> | ||||||
|                 </div> |                 </div> | ||||||
|             </td> |             </td> | ||||||
|  | |||||||
							
								
								
									
										29
									
								
								website/docs/security/CVE-2022-23555.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								website/docs/security/CVE-2022-23555.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | |||||||
|  | # CVE-2022-23555 | ||||||
|  |  | ||||||
|  | ## Token reuse in invitation URLs leads to access control bypass via the use of a different enrollment flow | ||||||
|  |  | ||||||
|  | ### Summary | ||||||
|  |  | ||||||
|  | Token reuse in invitation URLs leads to access control bypass via the use of a different enrollment flow than in the one provided. | ||||||
|  |  | ||||||
|  | ### Patches | ||||||
|  |  | ||||||
|  | authentik 2022.11.4, 2022.10.4 and 2022.12.0 fix this issue, for other versions the workaround can be used. | ||||||
|  |  | ||||||
|  | ### Impact | ||||||
|  |  | ||||||
|  | Only configurations using both invitations and have multiple enrollment flows with invitation stages that grant different permissions are affected. The default configuration is not vulnerable, and neither are configurations with a single enrollment flow. | ||||||
|  |  | ||||||
|  | ### Details | ||||||
|  |  | ||||||
|  | The vulnerability allows an attacker that knows different invitation flows names (e.g. `enrollment-invitation-test` and `enrollment-invitation-admin`) via either different invite links or via brute forcing to signup via a single invitation url for any valid invite link received (it can even be a url for a third flow as long as it's a valid invite) as the token used in the `Invitations` section of the Admin interface does NOT change when a different `enrollment flow` is selected via the interface and it is NOT bound to the selected flow, so it will be valid for any flow when used. | ||||||
|  |  | ||||||
|  | ### Workarounds | ||||||
|  |  | ||||||
|  | As a workaround, fixed data can be added to invitations which can be checked in the flow to deny requests. Alternatively, an identifier with high entropy (like a UUID) can be used as flow slug, mitigating the attack vector by exponentially decreasing the possibility of discovering other flows. | ||||||
|  |  | ||||||
|  | ### For more information | ||||||
|  |  | ||||||
|  | If you have any questions or comments about this advisory: | ||||||
|  |  | ||||||
|  | -   Email us at [security@goauthentik.io](mailto:security@goauthentik.io) | ||||||
| @ -296,6 +296,7 @@ module.exports = { | |||||||
|                 "security/policy", |                 "security/policy", | ||||||
|                 "security/CVE-2022-46145", |                 "security/CVE-2022-46145", | ||||||
|                 "security/CVE-2022-46172", |                 "security/CVE-2022-46172", | ||||||
|  |                 "security/CVE-2022-23555", | ||||||
|             ], |             ], | ||||||
|         }, |         }, | ||||||
|     ], |     ], | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Jens L
					Jens L