Low severityNVD Advisory· Published Aug 26, 2022· Updated Aug 3, 2024
CVE-2021-3754
CVE-2021-3754
Description
A flaw was found in keycloak where an attacker is able to register himself with the username same as the email ID of any existing user. This may cause trouble in getting password recovery email in case the user forgets the password.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.keycloak:keycloak-servicesMaven | < 24.0.1 | 24.0.1 |
Affected products
1Patches
1f9708037383aCheck email and username for duplicated if isLoginWithEmailAllowed
9 files changed · +123 −31
services/src/main/java/org/keycloak/services/resources/admin/UserResource.java+7 −0 modified@@ -78,6 +78,7 @@ import org.keycloak.services.managers.BruteForceProtector; import org.keycloak.services.managers.UserConsentManager; import org.keycloak.services.managers.UserSessionManager; +import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; @@ -241,6 +242,12 @@ public static Response validateUserProfile(UserProfile profile, KeycloakSession } catch (ValidationException pve) { List<ErrorRepresentation> errors = new ArrayList<>(); for (ValidationException.Error error : pve.getErrors()) { + // some messages are managed directly as before + switch (error.getMessage()) { + case Messages.MISSING_USERNAME -> throw ErrorResponse.error("User name is missing", Response.Status.BAD_REQUEST); + case Messages.USERNAME_EXISTS -> throw ErrorResponse.exists("User exists with same username"); + case Messages.EMAIL_EXISTS -> throw ErrorResponse.exists("User exists with same email"); + } errors.add(new ErrorRepresentation(error.getAttribute(), error.getMessage(), error.getMessageParameters())); }
services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java+0 −18 modified@@ -24,7 +24,6 @@ import org.jboss.resteasy.reactive.NoCache; import org.keycloak.common.ClientConnection; import org.keycloak.common.Profile; -import org.keycloak.common.util.ObjectUtil; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; import org.keycloak.models.Constants; @@ -133,23 +132,6 @@ public Response createUser(final UserRepresentation rep) { if(realm.isRegistrationEmailAsUsername()) { username = rep.getEmail(); } - if (ObjectUtil.isBlank(username)) { - throw ErrorResponse.error("User name is missing", Response.Status.BAD_REQUEST); - } - - // Double-check duplicated username and email here due to federation - if (session.users().getUserByUsername(realm, username) != null) { - throw ErrorResponse.exists("User exists with same username"); - } - if (rep.getEmail() != null && !realm.isDuplicateEmailsAllowed()) { - try { - if(session.users().getUserByEmail(realm, rep.getEmail()) != null) { - throw ErrorResponse.exists("User exists with same email"); - } - } catch (ModelDuplicateException e) { - throw ErrorResponse.exists("User exists with same email"); - } - } UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class);
services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProviderFactory.java+7 −2 modified@@ -440,9 +440,14 @@ private UserProfileMetadata createUserResourceValidation(Config.Scope config) { UserProfileMetadata metadata = new UserProfileMetadata(USER_API); - metadata.addAttribute(UserModel.USERNAME, -2, new AttributeValidatorMetadata(UsernameHasValueValidator.ID)) + metadata.addAttribute(UserModel.USERNAME, -2, + new AttributeValidatorMetadata(UsernameHasValueValidator.ID), + new AttributeValidatorMetadata(DuplicateUsernameValidator.ID)) .addWriteCondition(DeclarativeUserProfileProviderFactory::editUsernameCondition); - metadata.addAttribute(UserModel.EMAIL, -1, new AttributeValidatorMetadata(EmailValidator.ID, ValidatorConfig.builder().config(EmailValidator.IGNORE_EMPTY_VALUE, true).build())) + metadata.addAttribute(UserModel.EMAIL, -1, + new AttributeValidatorMetadata(DuplicateEmailValidator.ID), + new AttributeValidatorMetadata(EmailExistsAsUsernameValidator.ID), + new AttributeValidatorMetadata(EmailValidator.ID, ValidatorConfig.builder().config(EmailValidator.IGNORE_EMPTY_VALUE, true).build())) .addWriteCondition(DeclarativeUserProfileProviderFactory::editEmailCondition); List<AttributeValidatorMetadata> readonlyValidators = new ArrayList<>();
services/src/main/java/org/keycloak/userprofile/validator/DuplicateEmailValidator.java+7 −0 modified@@ -70,6 +70,13 @@ public ValidationContext validate(Object input, String inputHint, ValidationCont if (userByEmail != null && (user == null || !userByEmail.getId().equals(user.getId()))) { context.addError(new ValidationError(ID, inputHint, Messages.EMAIL_EXISTS) .setStatusCode(Response.Status.CONFLICT)); + } else if (realm.isLoginWithEmailAllowed()) { + // check for duplicated username + userByEmail = session.users().getUserByUsername(realm, value); + if (userByEmail != null && (user == null || !userByEmail.getId().equals(user.getId()))) { + context.addError(new ValidationError(ID, inputHint, Messages.EMAIL_EXISTS) + .setStatusCode(Response.Status.CONFLICT)); + } } }
services/src/main/java/org/keycloak/userprofile/validator/DuplicateUsernameValidator.java+11 −2 modified@@ -64,12 +64,21 @@ public ValidationContext validate(Object input, String inputHint, ValidationCont KeycloakSession session = context.getSession(); UserModel existing = session.users().getUserByUsername(session.getContext().getRealm(), value); UserModel user = UserProfileAttributeValidationContext.from(context).getAttributeContext().getUser(); + String valueLowercased = value.toLowerCase(); - if (! KeycloakModelUtils.isUsernameCaseSensitive(session.getContext().getRealm())) value = value.toLowerCase(); + if (! KeycloakModelUtils.isUsernameCaseSensitive(session.getContext().getRealm())) value = valueLowercased; - if (user != null && !value.equals(user.getFirstAttribute(UserModel.USERNAME)) && (existing != null && !existing.getId().equals(user.getId()))) { + RealmModel realm = session.getContext().getRealm(); + if (existing != null && (user == null || !existing.getId().equals(user.getId()))) { context.addError(new ValidationError(ID, inputHint, Messages.USERNAME_EXISTS) .setStatusCode(Response.Status.CONFLICT)); + } else if (realm.isLoginWithEmailAllowed() && value.indexOf('@') > 0) { + // check the username does not collide with an email + existing = session.users().getUserByEmail(realm, valueLowercased); + if (existing != null && (user == null || !existing.getId().equals(user.getId()))) { + context.addError(new ValidationError(ID, inputHint, Messages.USERNAME_EXISTS) + .setStatusCode(Response.Status.CONFLICT)); + } } return context;
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java+50 −0 modified@@ -332,6 +332,56 @@ public void updateProfileDuplicatedEmail() { events.assertEmpty(); } + @Test + public void updateProfileDuplicateUsernameWithEmail() { + getCleanup().addUserId(createUser(TEST_REALM_NAME, "user1@local.com", "password", "user1", "user1", "user1@local.org")); + + loginPage.open(); + + loginPage.login("john-doh@localhost", "password"); + + updateProfilePage.assertCurrent(); + + updateProfilePage.prepareUpdate().username("user1@local.org").firstName("New first").lastName("New last").email("new@email.com").submit(); + + updateProfilePage.assertCurrent(); + + // assert that form holds submitted values during validation error + Assert.assertEquals("New first", updateProfilePage.getFirstName()); + Assert.assertEquals("New last", updateProfilePage.getLastName()); + Assert.assertEquals("new@email.com", updateProfilePage.getEmail()); + Assert.assertEquals("user1@local.org", updateProfilePage.getUsername()); + + Assert.assertEquals("Username already exists.", updateProfilePage.getInputErrors().getUsernameError()); + + events.assertEmpty(); + } + + @Test + public void updateProfileDuplicatedEmailWithUsername() { + getCleanup().addUserId(createUser(TEST_REALM_NAME, "user1@local.com", "password", "user1", "user1", "user1@local.org")); + + loginPage.open(); + + loginPage.login("test-user@localhost", "password"); + + updateProfilePage.assertCurrent(); + + updateProfilePage.prepareUpdate().username("test-user@localhost").firstName("New first").lastName("New last") + .email("user1@local.com").submit(); + + updateProfilePage.assertCurrent(); + + // assert that form holds submitted values during validation error + Assert.assertEquals("New first", updateProfilePage.getFirstName()); + Assert.assertEquals("New last", updateProfilePage.getLastName()); + Assert.assertEquals("user1@local.com", updateProfilePage.getEmail()); + + Assert.assertEquals("Email already exists.", updateProfilePage.getInputErrors().getEmailError()); + + events.assertEmpty(); + } + @Test public void updateProfileExpiredCookies() { loginPage.open();
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java+35 −2 modified@@ -325,6 +325,39 @@ public void createDuplicatedUser2() { } } + @Test + public void createDuplicatedUsernameWithEmail() { + createUser("user1@local.com", "user1@local.org"); + + UserRepresentation user = new UserRepresentation(); + user.setUsername("user1@local.org"); + user.setEmail("user2@localhost"); + try (Response response = realm.users().create(user)) { + assertEquals(409, response.getStatus()); + assertAdminEvents.assertEmpty(); + + ErrorRepresentation error = response.readEntity(ErrorRepresentation.class); + Assert.assertEquals("User exists with same username", error.getErrorMessage()); + } + } + + @Test + public void createDuplicatedEmailWithUsername() { + createUser("user1@local.com", "user1@local.org"); + + UserRepresentation user = new UserRepresentation(); + user.setUsername("user2"); + user.setEmail("user1@local.com"); + + try (Response response = realm.users().create(user)) { + assertEquals(409, response.getStatus()); + assertAdminEvents.assertEmpty(); + + ErrorRepresentation error = response.readEntity(ErrorRepresentation.class); + Assert.assertEquals("User exists with same email", error.getErrorMessage()); + } + } + //KEYCLOAK-14611 @Test public void createDuplicateEmailWithExistingDuplicates() { @@ -352,7 +385,7 @@ public void createDuplicateEmailWithExistingDuplicates() { try (Response response = realm.users().create(user)) { assertEquals(409, response.getStatus()); ErrorRepresentation error = response.readEntity(ErrorRepresentation.class); - Assert.assertEquals("User exists with same email", error.getErrorMessage()); + Assert.assertEquals("User exists with same username or email", error.getErrorMessage()); assertAdminEvents.assertEmpty(); } } @@ -2619,7 +2652,7 @@ public void updateUserWithExistingEmail() { assertThat(e.getResponse().getStatus(), is(409)); ErrorRepresentation error = e.getResponse().readEntity(ErrorRepresentation.class); - Assert.assertEquals("User exists with same username or email", error.getErrorMessage()); + Assert.assertEquals("User exists with same email", error.getErrorMessage()); assertAdminEvents.assertEmpty(); } }
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java+4 −5 modified@@ -26,23 +26,20 @@ import org.junit.ClassRule; import org.junit.Test; import org.keycloak.admin.client.resource.UserResource; -import org.keycloak.common.Profile; import org.keycloak.common.constants.KerberosConstants; import org.keycloak.federation.kerberos.CommonKerberosConfig; import org.keycloak.federation.kerberos.KerberosConfig; import org.keycloak.federation.kerberos.KerberosFederationProviderFactory; import org.keycloak.models.UserModel; import org.keycloak.representations.idm.ComponentRepresentation; -import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.ErrorRepresentation; import org.keycloak.representations.idm.UserProfileAttributeMetadata; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.storage.UserStorageProvider; import org.keycloak.testsuite.ActionURIUtils; import org.keycloak.testsuite.KerberosEmbeddedServer; import org.keycloak.testsuite.admin.ApiUtil; -import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected; -import org.keycloak.testsuite.forms.VerifyProfileTest; import org.keycloak.testsuite.util.KerberosRule; import static org.keycloak.userprofile.UserProfileUtil.USER_METADATA_GROUP; @@ -181,7 +178,9 @@ public void handleUnknownKerberosRealm() throws Exception { UserRepresentation john = new UserRepresentation(); john.setUsername("john"); Response response = testRealmResource().users().create(john); - Assert.assertEquals(500, response.getStatus()); + Assert.assertEquals(400, response.getStatus()); + ErrorRepresentation error = response.readEntity(ErrorRepresentation.class); + Assert.assertEquals("Could not create user", error.getErrorMessage()); response.close(); }
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java+2 −2 modified@@ -202,7 +202,7 @@ public void testCustomAttributeInAnyContext() { private static void testCustomAttributeInAnyContext(KeycloakSession session) { Map<String, Object> attributes = new HashMap<>(); - attributes.put(UserModel.USERNAME, "profiled-user"); + attributes.put(UserModel.USERNAME, org.keycloak.models.utils.KeycloakModelUtils.generateId()); attributes.put(UserModel.FIRST_NAME, "John"); attributes.put(UserModel.LAST_NAME, "Doe"); attributes.put(UserModel.EMAIL, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org"); @@ -243,7 +243,7 @@ private static void testResolveProfile(KeycloakSession session) { Map<String, Object> attributes = new HashMap<>(); - attributes.put(UserModel.USERNAME, "profiled-user"); + attributes.put(UserModel.USERNAME, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org"); attributes.put(UserModel.FIRST_NAME, "John"); attributes.put(UserModel.LAST_NAME, "Doe"); attributes.put(UserModel.EMAIL, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org");
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-4vc8-pg5c-vg4xghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-3754ghsaADVISORY
- access.redhat.com/security/cve/CVE-2021-3754ghsax_refsource_MISCWEB
- bugzilla.redhat.com/show_bug.cgighsax_refsource_MISCWEB
- github.com/keycloak/keycloak/commit/f9708037383aa98741e4850447de64dc4a0d4b4eghsaWEB
- github.com/keycloak/keycloak/security/advisories/GHSA-4vc8-pg5c-vg4xghsaWEB
News mentions
0No linked articles in our index yet.