Sep 14, 2026·6 min read·41 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-115908 (GHSA-hjqx-j4rw-g7xv) is a high-severity path traversal and link-following vulnerability in the Rust zip library (zip-rs/zip2) affecting versions >= 7.2.0 and < 9.0.0. Because ZipArchive::extract and ZipArchive::extract_unwrapped_root_dir validate symbolic link targets solely via lexical prefix comparisons rather than resolving intermediate filesystem links, an attacker who knows or can guess the absolute extraction path can construct multi-hop symlink chains that escape the destination directory, enabling arbitrary file creation and overwrite with the privileges of the extracting process.
CVE-2026-115907 is an uncontrolled resource consumption vulnerability in the Rust zip crate (versions 5.1.0 through 8.6.0, and 9.0.0 pre-releases) when compiled with the optional legacy-zip feature flag. The vulnerability resides in the legacy decompression decoders—Shrink, Reduce, and Implode—which directly pass an archive-declared 64-bit uncompressed size from a ZIP64 extended information extra field to Vec::reserve() before validating the compressed data stream. By supplying a malformed ZIP archive with a declared uncompressed size exceeding isize::MAX (such as u64::MAX), an unauthenticated attacker can trigger an unhandled capacity overflow panic in the Rust standard library allocator, terminating the calling thread or aborting the entire process.
Twisted versions up to and including 26.4.0 are vulnerable to an unauthenticated remote denial-of-service attack via the HTTP/2 Rapid Reset technique (a variant of CVE-2023-44487). The HTTP/2 connection handler in twisted.web processes pipelined HEADERS and RST_STREAM frames sequentially within the same TCP segment. Because the request dispatch and synchronous rendering occur upon receiving the StreamEnded event before the subsequent StreamReset event is evaluated, an attacker can force the server to expend CPU cycles rendering responses while immediately evicting stream state to bypass concurrent stream limits.
An inefficient algorithmic complexity vulnerability exists in the npm package decode-uri-component prior to version 0.5.1. When processing malformed URI strings containing invalid percent-escape sequences, the library falls back to a custom decoder that collects distinct percent-encoded runs into a dictionary and executes a full-string regular expression replacement for each entry. Supplying a payload with thousands of distinct percent-encoded sequences alongside a malformed escape triggers quadratic O(K * L) execution time, starving the single-threaded Node.js event loop and resulting in an unauthenticated Denial of Service.
Open Neural Network Exchange (ONNX) versions 1.4.0 prior to 1.22.0 contain a null pointer dereference (CWE-476) and a signed-to-unsigned integer conversion error (CWE-195) in the shape inference functions for the Scan operator. When processing an untrusted ONNX model, the functions ScanInferenceFunction, ScanInferenceFunction_opset8, and ScanInferenceFunction_opset9 fail to validate the presence and value of the required num_scan_inputs attribute. Omitting the attribute results in a direct null pointer dereference that crashes the host process via SIGSEGV, while supplying a negative value triggers an integer wrap-around during type narrowing that leads to an unhandled allocation failure. Notably, models containing a negative num_scan_inputs value bypass standard validation via onnx.checker.check_model().
CVE-2026-91403 is a critical cryptographic trust validation flaw (CWE-327) in the Java Secure Socket Extension (JSSE) component of Oracle Java SE, Oracle GraalVM, and OpenJDK builds. Triggered by the compromise of private signing keys for eight intermediate Certificate Authorities operated by eMudhra (emSign), unpatched Java runtime environments implicitly trust fraudulent TLS and code-signing certificates issued by these compromised intermediates. Because Java disables online certificate revocation checking (OCSP/CRL) by default, an unauthenticated network attacker in an Adversary-in-the-Middle (AiTM) position can intercept, decrypt, and modify encrypted TLS communications or bypass code signature verification without triggering security exceptions.