CVE-2023-47111 (Go, published 2023-11-08T22:15Z)
Description: value: ZITADEL provides identity infrastructure. ZITADEL provides administrators the possibility to define a `Lockout Policy` with a maximum amount of failed password check attempts. On every failed password check, the amount of failed checks is compared against the configured maximum. Exceeding the limit, will lock the user and prevent further authentication. In the affected implementation it was possible for an attacker to start multiple parallel password checks, giving him the possibility to try out more combinations than configured in the `Lockout Policy`. This vulnerability has been patched in versions 2.40.5 and 2.38.3.

Severity: nan (CVSS v3 base score: 3.7)
Weakness: CWE-362 \u2014 Concurrent Execution using Shared Resource with Improper Synchronization ('Race Condition')
The product contains a code sequence that can run concurrently with other code, and the code sequence requires temporary, exclusive access to a shared resource, but a timing window exists in which the shared resource can be modified by another code sequence that is operating concurrently.
Repository: https://github.com/zitadel/zitadel
Files changed:
internal/command/user_human_password.go
internal/command/user_human_password_model.go
internal/command/user_human_password_test.go
internal/query/user_password.go
Commit message: Merge pull request from GHSA-7h8m-vrxx-vr4m

* fix: handle locking policy correctly for multiple simultaneous password checks

* recheck events

Vulnerable code:
		return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3n77z", "Errors.User.Password.NotSet")


								"")),

Fixed code:
	if wm.UserState == domain.UserStateLocked {
		return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-JLK35", "Errors.User.Locked")
	}
		return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3nJ4t", "Errors.User.Password.NotSet")

	// recheck for additional events (failed password checks or locks)
	recheckErr := c.eventstore.FilterToQueryReducer(ctx, wm)
	if recheckErr != nil {
		return recheckErr
	}
	if wm.UserState == domain.UserStateLocked {
		return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-SFA3t", "Errors.User.Locked")
	}

		case *user.UserLockedEvent:
			wm.UserState = domain.UserStateLocked
			if wm.UserState != domain.UserStateDeleted {
				wm.UserState = domain.UserStateActive
			}
			user.UserLockedType,
	if wm.WriteModel.ProcessedSequence != 0 {
		query.SequenceGreater(wm.WriteModel.ProcessedSequence)
	}
		{
			name: "user locked, precondition error",
			fields: fields{
				eventstore: eventstoreExpect(
					t,
					expectFilter(
						eventFromEventPusher(
							org.NewLoginPolicyAddedEvent(context.Background(),
								&org.NewAggregate("org1").Aggregate,
								true,
								false,
								false,
								false,
								false,
								false,
								false,
								false,
								false,
								false,
								domain.PasswordlessTypeNotAllowed,
								"",
								time.Hour*1,
								time.Hour*2,
								time.Hour*3,
								time.Hour*4,
								time.Hour*5,
							),
						),
					),
					expectFilter(
						eventFromEventPusher(
							user.NewHumanAddedEvent(context.Background(),
								&user.NewAggregate("user1", "org1").Aggregate,
								"username",
								"firstname",
								"lastname",
								"nickname",
								"displayname",
								language.German,
								domain.GenderUnspecified,
								"email@test.ch",
								true,
							),
						),
						eventFromEventPusher(
							user.NewUserLockedEvent(context.Background(),
								&user.NewAggregate("user1", "org1").Aggregate,
							),
						),
					),
				),
			},
			args: args{
				ctx:           context.Background(),
				userID:        "user1",
				resourceOwner: "org1",
				password:      "password",
			},
			res: res{
				err: caos_errs.IsPreconditionFailed,
			},
		},
					expectFilter(),
								""),
						),
					expectFilter(),
					expectFilter(),
					expectFilter(),
		{
			name: "check password ok, locked in the mean time",
			fields: fields{
				eventstore: eventstoreExpect(
					t,
					expectFilter(
						eventFromEventPusher(
							org.NewLoginPolicyAddedEvent(context.Background(),
								&org.NewAggregate("org1").Aggregate,
								true,
								false,
								false,
								false,
								false,
								false,
								false,
								false,
								false,
								false,
								domain.PasswordlessTypeNotAllowed,
								"",
								time.Hour*1,
								time.Hour*2,
								time.Hour*3,
								time.Hour*4,
								time.Hour*5,
							),
						),
					),
					expectFilter(
						eventFromEventPusher(
							user.NewHumanAddedEvent(context.Background(),
								&user.NewAggregate("user1", "org1").Aggregate,
								"username",
								"firstname",
								"lastname",
								"nickname",
								"displayname",
								language.German,
								domain.GenderUnspecified,
								"email@test.ch",
								true,
							),
						),
						eventFromEventPusher(
							user.NewHumanEmailVerifiedEvent(context.Background(),
								&user.NewAggregate("user1", "org1").Aggregate,
							),
						),
						eventFromEventPusher(
							user.NewHumanPasswordChangedEvent(context.Background(),
								&user.NewAggregate("user1", "org1").Aggregate,
								"$plain$x$password",
								false,
								"")),
					),
					expectFilter(
						eventFromEventPusher(
							user.NewUserLockedEvent(context.Background(),
								&user.NewAggregate("user1", "org1").Aggregate,
							),
						),
					),
				),
				userPasswordHasher: mockPasswordHasher("x"),
			},
			args: args{
				ctx:           context.Background(),
				userID:        "user1",
				resourceOwner: "org1",
				password:      "password",
				authReq: &domain.AuthRequest{
					ID:      "request1",
					AgentID: "agent1",
				},
			},
			res: res{
				err: caos_errs.IsPreconditionFailed,
			},
		},
					expectFilter(),
		case *user.UserLockedEvent:
			wm.UserState = domain.UserStateLocked
			if wm.UserState != domain.UserStateDeleted {
				wm.UserState = domain.UserStateActive
			}
			user.UserLockedType,