ZITADEL Users Can Self-Verify Email/Phone via UpdateHumanUser API
Description
ZITADEL is an open source identity management platform. Prior to versions 4.11.1 and 3.4.7, a vulnerability in Zitadel's self-management capability allowed users to mark their email and phone as verified without going through an actual verification process. The patch in versions 4.11.1 and 3.4.7 resolves the issue by requiring the correct permission in case the verification flag is provided and only allows self-management of the email address and/or phone number itself. If an upgrade is not possible, an action (v2) could be used to prevent setting the verification flag on the own user.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/zitadel/zitadelGo | >= 4.0.0, < 4.11.1 | 4.11.1 |
github.com/zitadel/zitadelGo | >= 2.43.0, < 3.4.7 | 3.4.7 |
github.com/zitadel/zitadelGo | < 1.80.0-v2.20.0.20260225053417-0261536243e5 | 1.80.0-v2.20.0.20260225053417-0261536243e5 |
Affected products
1Patches
10261536243e5Merge commit from fork
3 files changed · +112 −1
internal/api/grpc/user/v2/integration_test/user_test.go+50 −0 modified@@ -4016,6 +4016,56 @@ func TestServer_UpdateUserTypeHuman(t *testing.T) { } }, }, + { + name: "verified email", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + OrgCTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: integration.Email(), + Verification: &user.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + }, + }, + }, + }, + wantErr: false, + } + }, + }, + { + name: "verified email (self-management), error", + testCase: func(runId, userId string) testCase { + Instance.SetUserPassword(LoginCTX, userId, integration.UserPassword, false) + _, token, _, _ := Instance.CreatePasswordSession(t, LoginCTX, userId, integration.UserPassword) + return testCase{ + args: args{ + integration.WithAuthorizationToken(CTX, token), + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: integration.Email(), + Verification: &user.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, { name: "password not complexity conform", testCase: func(runId, userId string) testCase {
internal/command/user_v2_human.go+3 −1 modified@@ -297,7 +297,9 @@ func (c *Commands) ChangeUserHuman(ctx context.Context, human *ChangeHuman, alg } if human.Changed() { - if err := c.checkPermissionUpdateUser(ctx, existingHuman.ResourceOwner, existingHuman.AggregateID, !metadataChanged); err != nil { + // Changing metadata or setting email, resp. phone to verified is only allowed with user write permissions, but not for self-management. + requireWritePermission := metadataChanged || (human.Email != nil && human.Email.Verified) || (human.Phone != nil && human.Phone.Verified) + if err := c.checkPermissionUpdateUser(ctx, existingHuman.ResourceOwner, existingHuman.AggregateID, !requireWritePermission); err != nil { return err } }
internal/command/user_v2_human_test.go+59 −0 modified@@ -12,6 +12,7 @@ import ( "go.uber.org/mock/gomock" "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -2699,6 +2700,35 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { }, }, }, + { + name: "change human email verified (self-management), not allowed", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + newAddHumanEvent("$plain$x$password", true, true, "", language.English), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + tarpit: expectTarpit(0), + }, + args: args{ + ctx: authz.NewMockContext("instance1", "org1", "user1"), + orgID: "org1", + human: &ChangeHuman{ + Email: &Email{ + Address: "changed@example.com", + Verified: true, + }, + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) + }, + }, + }, { name: "change human email verified, ok", fields: fields{ @@ -3019,6 +3049,35 @@ func TestCommandSide_ChangeUserHuman(t *testing.T) { }, }, }, + { + name: "change human phone verified (self-management), not allowed", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + newAddHumanEvent("$plain$x$password", true, true, "", language.English), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + tarpit: expectTarpit(0), + }, + args: args{ + ctx: authz.NewMockContext("instance1", "org1", "user1"), + orgID: "org1", + human: &ChangeHuman{ + Phone: &Phone{ + Number: "+41791234567", + Verified: true, + }, + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) + }, + }, + }, { name: "change human phone verified, ok", fields: fields{
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-282g-fhmx-xf54ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-27946ghsaADVISORY
- github.com/zitadel/zitadel/commit/0261536243e500dccfd8c7f547d592c822478327ghsaWEB
- github.com/zitadel/zitadel/releases/tag/v3.4.7ghsaWEB
- github.com/zitadel/zitadel/releases/tag/v4.11.1ghsaWEB
- github.com/zitadel/zitadel/security/advisories/GHSA-282g-fhmx-xf54ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.