VYPR
Unrated severityNVD Advisory· Published May 22, 2026· Updated May 22, 2026

authentik: Privilege Escalation via User PATCH: Superuser Group Assignment Bypasses enable_group_superuser

CVE-2026-40172

Description

authentik is an open-source identity provider. In versions prior to 2025.12.5 and 2026.2.0-rc1 through 2026.2.2, the PATCH /api/v3/core/users/{pk}/ API allows a caller with change_user on a target user to assign arbitrary groups through UserSerializer, including groups with is_superuser=True, without requiring enable_group_superuser, leading to privilege escalation. This bypasses the stricter permission model enforced in group-management paths and enables delegated user-management permissions to escalate target users to administrator-equivalent privilege. Users with permissions to update groups or permissions to update users are able to add themselves or other users they have permissions on to users which have superuser permissions. This issue has been fixed in versions 22025.12.5 and 2026.2.3.

Affected products

1

Patches

1
31d8ddc88732

internal: Automated internal backport: CVE-2026-40172.sec.patch to authentik-main (#22300)

https://github.com/goauthentik/authentikauthentik-automation[bot]May 12, 2026via github-commit-search
4 files changed · +182 0
  • authentik/core/api/groups.py+19 0 modified
    @@ -246,6 +246,25 @@ def validate_is_superuser(self, superuser: bool):
                     )
             return superuser
     
    +    def validate_users(self, users: list) -> list:
    +        """Require add_user_to_group permission when adding new members via group PATCH."""
    +        request: Request = self.context.get("request", None)
    +        if not request:
    +            return users
    +        if not self.instance:
    +            return users
    +        # BulkManyRelatedField returns raw PKs, not model instances
    +        current_user_pks = set(self.instance.users.values_list("pk", flat=True))
    +        new_users = [u for u in users if u not in current_user_pks]
    +        if not new_users:
    +            return users
    +        has_perm = request.user.has_perm(
    +            "authentik_core.add_user_to_group"
    +        ) or request.user.has_perm("authentik_core.add_user_to_group", self.instance)
    +        if not has_perm:
    +            raise ValidationError(_("User does not have permission to add members to this group."))
    +        return users
    +
         class Meta:
             model = Group
             fields = [
    
  • authentik/core/api/users.py+30 0 modified
    @@ -297,6 +297,36 @@ def validate_type(self, user_type: str) -> str:
                 raise ValidationError(_("Setting a user to internal service account is not allowed."))
             return user_type
     
    +    def validate_groups(self, groups: list) -> list:
    +        """Require enable_group_superuser permission when adding a user to a superuser group."""
    +        request: Request = self.context.get("request", None)
    +        if not request:
    +            return groups
    +        current_groups = set(self.instance.groups.all()) if self.instance else set()
    +        for group in groups:
    +            if not group.is_superuser:
    +                continue
    +            if group in current_groups:
    +                continue
    +            if not request.user.has_perm("authentik_core.enable_group_superuser"):
    +                raise ValidationError(
    +                    _("User does not have permission to add members to a superuser group.")
    +                )
    +        return groups
    +
    +    def validate_roles(self, roles: list) -> list:
    +        """Require change_role permission when assigning new roles to a user."""
    +        request: Request = self.context.get("request", None)
    +        if not request:
    +            return roles
    +        current_roles = set(self.instance.roles.all()) if self.instance else set()
    +        new_roles = [r for r in roles if r not in current_roles]
    +        if not new_roles:
    +            return roles
    +        if not request.user.has_perm("authentik_rbac.change_role"):
    +            raise ValidationError(_("User does not have permission to assign roles."))
    +        return roles
    +
         def validate(self, attrs: dict) -> dict:
             if self.instance and self.instance.type == UserTypes.INTERNAL_SERVICE_ACCOUNT:
                 raise ValidationError(_("Can't modify internal service account users"))
    
  • authentik/core/tests/test_groups_api.py+55 0 modified
    @@ -158,3 +158,58 @@ def test_superuser_create(self):
                 data={"name": generate_id(), "is_superuser": True},
             )
             self.assertEqual(res.status_code, 201)
    +
    +    def test_patch_users_no_perm(self):
    +        """PATCH group with new users without add_user_to_group must be rejected."""
    +        group = Group.objects.create(name=generate_id())
    +        self.login_user.assign_perms_to_managed_role("authentik_core.view_group", group)
    +        self.login_user.assign_perms_to_managed_role("authentik_core.change_group", group)
    +        self.client.force_login(self.login_user)
    +        res = self.client.patch(
    +            reverse("authentik_api:group-detail", kwargs={"pk": group.pk}),
    +            data={"users": [self.user.pk]},
    +            content_type="application/json",
    +        )
    +        self.assertEqual(res.status_code, 400)
    +
    +    def test_patch_users_with_global_perm(self):
    +        """PATCH group with new users with global add_user_to_group must succeed."""
    +        group = Group.objects.create(name=generate_id())
    +        self.login_user.assign_perms_to_managed_role("authentik_core.view_group", group)
    +        self.login_user.assign_perms_to_managed_role("authentik_core.change_group", group)
    +        self.login_user.assign_perms_to_managed_role("authentik_core.add_user_to_group")
    +        self.client.force_login(self.login_user)
    +        res = self.client.patch(
    +            reverse("authentik_api:group-detail", kwargs={"pk": group.pk}),
    +            data={"users": [self.user.pk]},
    +            content_type="application/json",
    +        )
    +        self.assertEqual(res.status_code, 200)
    +
    +    def test_patch_users_with_obj_perm(self):
    +        """PATCH group with new users with object-level add_user_to_group must succeed."""
    +        group = Group.objects.create(name=generate_id())
    +        self.login_user.assign_perms_to_managed_role("authentik_core.view_group", group)
    +        self.login_user.assign_perms_to_managed_role("authentik_core.change_group", group)
    +        self.login_user.assign_perms_to_managed_role("authentik_core.add_user_to_group", group)
    +        self.client.force_login(self.login_user)
    +        res = self.client.patch(
    +            reverse("authentik_api:group-detail", kwargs={"pk": group.pk}),
    +            data={"users": [self.user.pk]},
    +            content_type="application/json",
    +        )
    +        self.assertEqual(res.status_code, 200)
    +
    +    def test_patch_existing_users_no_perm(self):
    +        """PATCH group keeping existing membership without add_user_to_group must succeed."""
    +        group = Group.objects.create(name=generate_id())
    +        group.users.add(self.user)
    +        self.login_user.assign_perms_to_managed_role("authentik_core.view_group", group)
    +        self.login_user.assign_perms_to_managed_role("authentik_core.change_group", group)
    +        self.client.force_login(self.login_user)
    +        res = self.client.patch(
    +            reverse("authentik_api:group-detail", kwargs={"pk": group.pk}),
    +            data={"users": [self.user.pk]},
    +            content_type="application/json",
    +        )
    +        self.assertEqual(res.status_code, 200)
    
  • authentik/core/tests/test_users_api.py+78 0 modified
    @@ -12,6 +12,7 @@
     from authentik.core.models import (
         USER_ATTRIBUTE_TOKEN_EXPIRING,
         AuthenticatedSession,
    +    Group,
         Session,
         Token,
         User,
    @@ -25,6 +26,7 @@
     )
     from authentik.flows.models import FlowAuthenticationRequirement, FlowDesignation
     from authentik.lib.generators import generate_id, generate_key
    +from authentik.rbac.models import Role
     from authentik.stages.email.models import EmailStage
     
     INVALID_PASSWORD_HASH = "not-a-valid-hash"
    @@ -939,3 +941,79 @@ def test_sort_by_last_login(self):
             self.assertIn(user2.pk, pks)
             # Verify user2 comes before user1 in descending order
             self.assertLess(pks.index(user2.pk), pks.index(user1.pk))
    +
    +
    +class TestUsersAPIGroupRoleValidation(APITestCase):
    +    """Test that PATCH /api/v3/core/users/{pk}/ enforces group and role permission checks."""
    +
    +    def setUp(self) -> None:
    +        self.actor = create_test_user()
    +        self.target = create_test_user()
    +
    +    def _patch(self, data: dict):
    +        self.client.force_login(self.actor)
    +        return self.client.patch(
    +            reverse("authentik_api:user-detail", kwargs={"pk": self.target.pk}),
    +            data=data,
    +            content_type="application/json",
    +        )
    +
    +    def test_patch_superuser_group_no_perm(self):
    +        """Assigning a superuser group without enable_group_superuser must be rejected."""
    +        self.actor.assign_perms_to_managed_role("authentik_core.view_user")
    +        self.actor.assign_perms_to_managed_role("authentik_core.change_user", self.target)
    +        group = Group.objects.create(name=generate_id(), is_superuser=True)
    +        res = self._patch({"groups": [str(group.pk)]})
    +        self.assertEqual(res.status_code, 400)
    +
    +    def test_patch_superuser_group_with_perm(self):
    +        """Assigning a superuser group with enable_group_superuser must succeed."""
    +        self.actor.assign_perms_to_managed_role("authentik_core.view_user")
    +        self.actor.assign_perms_to_managed_role("authentik_core.change_user", self.target)
    +        self.actor.assign_perms_to_managed_role("authentik_core.enable_group_superuser")
    +        group = Group.objects.create(name=generate_id(), is_superuser=True)
    +        res = self._patch({"groups": [str(group.pk)]})
    +        self.assertEqual(res.status_code, 200)
    +
    +    def test_patch_non_superuser_group_no_perm(self):
    +        """Assigning a non-superuser group without special permission must succeed."""
    +        self.actor.assign_perms_to_managed_role("authentik_core.view_user")
    +        self.actor.assign_perms_to_managed_role("authentik_core.change_user", self.target)
    +        group = Group.objects.create(name=generate_id(), is_superuser=False)
    +        res = self._patch({"groups": [str(group.pk)]})
    +        self.assertEqual(res.status_code, 200)
    +
    +    def test_patch_existing_superuser_group_no_perm(self):
    +        """Keeping an existing superuser group membership without the permission must succeed."""
    +        self.actor.assign_perms_to_managed_role("authentik_core.view_user")
    +        self.actor.assign_perms_to_managed_role("authentik_core.change_user", self.target)
    +        group = Group.objects.create(name=generate_id(), is_superuser=True)
    +        self.target.groups.add(group)
    +        res = self._patch({"groups": [str(group.pk)]})
    +        self.assertEqual(res.status_code, 200)
    +
    +    def test_patch_role_no_perm(self):
    +        """Assigning a new role without change_role must be rejected."""
    +        self.actor.assign_perms_to_managed_role("authentik_core.view_user")
    +        self.actor.assign_perms_to_managed_role("authentik_core.change_user", self.target)
    +        role = Role.objects.create(name=generate_id())
    +        res = self._patch({"roles": [str(role.pk)]})
    +        self.assertEqual(res.status_code, 400)
    +
    +    def test_patch_role_with_perm(self):
    +        """Assigning a new role with change_role must succeed."""
    +        self.actor.assign_perms_to_managed_role("authentik_core.view_user")
    +        self.actor.assign_perms_to_managed_role("authentik_core.change_user", self.target)
    +        self.actor.assign_perms_to_managed_role("authentik_rbac.change_role")
    +        role = Role.objects.create(name=generate_id())
    +        res = self._patch({"roles": [str(role.pk)]})
    +        self.assertEqual(res.status_code, 200)
    +
    +    def test_patch_existing_role_no_perm(self):
    +        """Keeping an existing role without change_role must succeed."""
    +        self.actor.assign_perms_to_managed_role("authentik_core.view_user")
    +        self.actor.assign_perms_to_managed_role("authentik_core.change_user", self.target)
    +        role = Role.objects.create(name=generate_id())
    +        self.target.roles.add(role)
    +        res = self._patch({"roles": [str(role.pk)]})
    +        self.assertEqual(res.status_code, 200)
    

Vulnerability mechanics

Root cause

"Missing input validation in UserSerializer allows assigning superuser groups and roles without requiring the corresponding permissions."

Attack vector

An attacker with the `change_user` permission on a target user can send a PATCH request to `/api/v3/core/users/{pk}/` with a payload containing a group that has `is_superuser=True`. Because the `UserSerializer` did not validate group assignments against the `enable_group_superuser` permission, the attacker can escalate the target user to administrator-equivalent privilege without needing that permission. The same endpoint also allowed assigning arbitrary roles without requiring `change_role`. This bypasses the stricter permission model enforced in group-management paths [patch_id=1693362].

Affected code

The vulnerability resides in `authentik/core/api/users.py` in the `UserSerializer` class. The `validate_groups` and `validate_roles` methods were missing, allowing the PATCH `/api/v3/core/users/{pk}/` endpoint to accept arbitrary group and role assignments without permission checks. The patch adds these validation methods to enforce `authentik_core.enable_group_superuser` for superuser groups and `authentik_rbac.change_role` for new roles. Additionally, `authentik/core/api/groups.py` was missing a `validate_users` method to require `authentik_core.add_user_to_group` when adding members via group PATCH.

What the fix does

The patch adds three validation methods. In `authentik/core/api/users.py`, `validate_groups` checks whether any newly assigned group has `is_superuser=True` and, if so, requires the `authentik_core.enable_group_superuser` permission. `validate_roles` checks whether any new roles are being assigned and requires `authentik_rbac.change_role`. In `authentik/core/api/groups.py`, `validate_users` checks whether new users are being added to a group and requires `authentik_core.add_user_to_group` (either globally or on the specific group). These validators run during serializer deserialization, rejecting unauthorized requests with a 400 status before any database changes occur [patch_id=1693362].

Preconditions

  • authThe attacker must have the change_user permission on the target user.
  • networkThe attacker must be able to send authenticated PATCH requests to /api/v3/core/users/{pk}/.
  • configA group with is_superuser=True must exist in the system.

Generated on May 23, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

3

News mentions

0

No linked articles in our index yet.