VYPR
High severityNVD Advisory· Published Feb 26, 2026· Updated Feb 26, 2026

ZITADEL Users Can Self-Verify Email/Phone via UpdateHumanUser API

CVE-2026-27946

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.

PackageAffected versionsPatched versions
github.com/zitadel/zitadelGo
>= 4.0.0, < 4.11.14.11.1
github.com/zitadel/zitadelGo
>= 2.43.0, < 3.4.73.4.7
github.com/zitadel/zitadelGo
< 1.80.0-v2.20.0.20260225053417-0261536243e51.80.0-v2.20.0.20260225053417-0261536243e5

Affected products

1

Patches

1
0261536243e5

Merge commit from fork

https://github.com/zitadel/zitadelLivio SpringFeb 25, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.