Sep 14, 2026·6 min read·8 visits
A slice mutation bug in ZITADEL's Go backend causes role cascading logic to skip adjacent roles during bulk revocation, leaving users with unauthorized permissions on shared projects.
CVE-2026-76081 is a logical vulnerability in ZITADEL's role cascading logic where updating a Project Grant to drop multiple adjacent roles simultaneously fails to clean up associated User Grants due to an in-place slice mutation error in Go.
ZITADEL leverages a microservices-inspired structure to support multi-tenant Identity and Access Management (IAM) deployments. Within this architecture, organizations often dynamically delegate specific administrative and operational capabilities to external partner organizations via a mechanism designated as a Project Grant.
When a Project Grant is constructed, the owning organization designates a precise subset of project roles available to the receiving organization. Administrators in the receiving organization then allocate these roles to local identities using User Grants. This multi-tiered permission topology creates a complex dependency graph that relies on accurate state synchronization during administrative modifications.
CVE-2026-76081 introduces a vulnerability inside this cascade path, classified under CWE-193 (Off-by-one Error). When an upstream project owner revokes multiple roles from a Project Grant, the downstream user grants must automatically prune the revoked roles. Due to a logical slice mutation defect in the cascade path, this cleanup process silently fails, leaving active permissions assigned to unauthorized users.
The root cause of the vulnerability lies in the implementation of the removeRoleFromUserGrant method inside internal/command/user_grant.go. When executing cascade deletions, the application iterates over the user's assigned roles using a standard Go for i, key := range loop while simultaneously mutating the slice in-place.
In Go, slice headers contain a pointer to an underlying array, a length, and a capacity. When an element is removed from a slice in-place using copy(slice[i:], slice[i+1:]) followed by truncation, the remaining elements shift to the left, decreasing their index positions by one. The loop index i, however, increments sequentially regardless of inner slice mutations.
This behavior leads to a classic index-skipping flaw. If two adjacent elements in the user's role list are targeted for removal, deleting the first element causes the second adjacent element to shift left into the current index position i. In the next iteration, the index increments to i+1, entirely bypassing the evaluation of the shifted element. As a consequence, the skipped role remains active in the database state.
The original implementation of the role removal loop used an in-place modification pattern that was highly prone to indexing anomalies. The inner and outer loops directly modified existingUserGrant.RoleKeys using slice copies while counting forward.
// Vulnerable Implementation
keyExists := false
for i, key := range existingUserGrant.RoleKeys {
for _, roleKey := range roleKeys {
if key == roleKey {
keyExists = true
// Shifting elements left
copy(existingUserGrant.RoleKeys[i:], existingUserGrant.RoleKeys[i+1:])
// Truncating the slice
existingUserGrant.RoleKeys[len(existingUserGrant.RoleKeys)-1] = ""
existingUserGrant.RoleKeys = existingUserGrant.RoleKeys[:len(existingUserGrant.RoleKeys)-1]
continue
}
}
}To resolve this structural flaw, the patch in version 4.16.0 completely removes the custom, forward-counting loop. It introduces slices.DeleteFunc from the standard Go library, which performs in-place filtering in a single, index-safe pass. It also refactors the list of target roles into a map (roleKeysToRemove map[string]bool) to optimize lookup performance to $O(N)$ time complexity.
// Patched Implementation
beforeLen := len(existingUserGrant.RoleKeys)
// Safely filter the slice using standard library mechanisms
existingUserGrant.RoleKeys = slices.DeleteFunc(existingUserGrant.RoleKeys, func(role string) bool {
return roleKeysToRemove[role]
})
if beforeLen == len(existingUserGrant.RoleKeys) {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-5m8g9", "Errors.UserGrant.RoleKeyNotFound")
}Exploiting this vulnerability does not require complex binary-level manipulation or injection vectors. The exploit is executed entirely through valid administrative state changes that trigger the logical error in the backend database.
To demonstrate the vulnerability, consider an enterprise environment where Organization A delegates a project containing three roles (admin, viewer, and editor) to Organization B. An administrator in Organization B assigns all three roles to User_Beta via a dynamic User Grant.
When the project owner in Organization A decides to revoke both admin and viewer privileges from the Project Grant, they submit an update request to ZITADEL. The backend processes the change and attempts to cascade the revocation down to User_Beta. Because the roles admin and viewer are adjacent in the underlying storage array, the in-place slice mutation skips the viewer role. User_Beta retains active access authorized under the viewer scope, violating the access control policy defined by the project owner.
Because this vulnerability directly corrupts the persistent state of the database, deploying the patched binaries is insufficient on its own to remediate pre-existing permission desynchronizations. To solve this, ZITADEL developers implemented an automated database migration, designated as Setup Step 73 (FixUserGrantRoles).
This migration scans the database projections using a specific SQL query to identify active User Grants that contain role configurations that are no longer supported by their parent Project Grants. It excludes direct User Grants, which do not go through the multi-role cascade path and are unaffected.
WITH computed AS (
SELECT
ug.id,
ug.resource_owner,
ug.roles AS current_roles,
COALESCE(
ARRAY(
SELECT r
FROM unnest(ug.roles) AS r
WHERE r = ANY(pg.granted_role_keys)
),
ARRAY[]::TEXT[]
) AS valid_roles
FROM projections.user_grants5 ug
JOIN projections.project_grants4 pg
ON pg.instance_id = ug.instance_id
AND pg.grant_id = ug.grant_id
WHERE ug.instance_id = $1
AND ug.grant_id IS NOT NULL AND ug.grant_id <> ''
AND ug.roles IS NOT NULL
AND cardinality(ug.roles) > 0
)
SELECT id, resource_owner, valid_roles
FROM computed
WHERE cardinality(valid_roles) <> cardinality(current_roles);For every misaligned User Grant returned by this query, the migration command handler generates and pushes a usergrant.NewUserGrantCascadeChangedEvent to the transactional event store. This process re-synchronizes the historical state and enforces the correct administrative boundaries.
Remediation requires upgrading the ZITADEL deployment to a version that contains both the logic fixes and the automated database state correction.
Administrators on the v4.x branch must upgrade to version 4.16.0 or higher immediately. Upon startup, the updated application binary executes the migration scripts, scans for orphaned or desynchronized permissions, and automatically registers corrective events in the event store.
Administrators on the v3.x release line must migrate their deployments to the active v4.x release line, as the v3.x line has reached End-of-Life (EOL) and will not receive security backports. If an immediate upgrade is not feasible, administrators should run the diagnostic SQL query provided in the database migration analysis to manually identify affected accounts and execute manual updates via the ZITADEL Management API.
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
ZITADEL ZITADEL | >= 4.0.0, < 4.16.0 | 4.16.0 |
ZITADEL ZITADEL | >= 3.0.0, <= 3.4.12 | 4.16.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-193 (Off-by-one Error) |
| Attack Vector | Network (AV:N) |
| CVSS v3.1 Score | 5.5 (Medium) |
| Impact | Improper Permission Revocation |
| Exploit Status | Proof of Concept Available |
| CISA KEV Status | Not Listed |
An error where the software performs an operation on a loop index that results in skipping elements of a slice during dynamic structure mutation.
CVE-2026-46696 identifies a critical sandbox bypass vulnerability in the October CMS platform that affects the Twig template security policy when safe mode is enabled. An authenticated backend user with permissions to modify CMS markup templates can chain unrestricted session store method access with Eloquent database query forwarding omissions. This chain allows the attacker to execute arbitrary raw SQL queries to read system secrets and subsequently write those secrets directly to the active session payload, achieving unauthorized administrative privilege escalation.
A security vulnerability in October Content Management System (CMS) involves the deserialization of untrusted data (CWE-502) within the backend SessionMaker trait. Prior to the patched versions, October CMS stored widget session states as base64-encoded serialized PHP objects. When loading these states, the application consumed them using unserialize() without enforcing class restrictions (allowed_classes). In configurations where cms.safe_mode is enabled to sandbox users with markup editor privileges, an attacker can exploit this behavior to instantiate arbitrary PHP classes and execute arbitrary code via accessible gadget chains.
A security vulnerability in ZITADEL's backend implementation of the OAuth2 Token Exchange endpoint allows authenticated clients to perform scope escalation and cross-client audience bypass. Prior to version 4.15.3, the Token Exchange flow lacked crucial validation logic, enabling low-privilege tokens to be exchanged for high-privilege tokens or tokens valid within other client applications, violating the OAuth2 delegation model.
This report provides a technical analysis of GHSA-2XMM-M4WV-3FJH, an incomplete scheme validation vulnerability in the image resizing utility of October CMS. By exploiting this flaw, authenticated or privileged users can pass dangerous URI schemes to trigger deserialization of untrusted metadata.
An authentication bypass vulnerability in ESPHome Device Builder Dashboard allows unauthenticated remote attackers to gain administrative access. The flaw is caused by a backward compatibility break during an environment variable rename that silently disables dashboard authentication upon upgrade.
A critical prototype pollution vulnerability was discovered in the confetti yayson library prior to version 4.3.0. The library deserializes JSON:API structures into internal cache dictionaries mapped with standard JavaScript objects. An attacker can control the cache keys by supplying '__proto__' in properties like type or id, modifying the prototype of all JavaScript objects process-wide.