Moderate severityNVD Advisory· Published Mar 29, 2023· Updated Aug 2, 2024
CVE-2022-1274
CVE-2022-1274
Description
A flaw was found in Keycloak in the execute-actions-email endpoint. This issue allows arbitrary HTML to be injected into emails sent to Keycloak users and can be misused to perform phishing or other attacks against users.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.keycloak:keycloak-servicesMaven | < 20.0.5 | 20.0.5 |
Affected products
1Patches
1fc3c61235fa3HTML Injection in Keycloak Admin REST API (#16764)
9 files changed · +118 −6
services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java+10 −2 modified@@ -16,10 +16,12 @@ */ package org.keycloak.authentication.actiontoken.execactions; +import org.keycloak.TokenVerifier; import org.keycloak.TokenVerifier.Predicate; import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.authentication.actiontoken.*; +import org.keycloak.authentication.requiredactions.util.RequiredActionsValidator; import org.keycloak.events.Errors; import org.keycloak.events.EventType; import org.keycloak.forms.login.LoginFormsProvider; @@ -64,7 +66,8 @@ public Predicate<? super ExecuteActionsActionToken>[] getVerifiers(ActionTokenCo Messages.INVALID_REDIRECT_URI ), - verifyEmail(tokenContext) + verifyEmail(tokenContext), + verifyRequiredActions(tokenContext) ); } @@ -127,5 +130,10 @@ public boolean canUseTokenRepeatedly(ExecuteActionsActionToken token, ActionToke .noneMatch(RequiredActionFactory::isOneTimeAction); } - + // Verify required actions included in the token are valid + protected TokenVerifier.Predicate<ExecuteActionsActionToken> verifyRequiredActions(ActionTokenContext<ExecuteActionsActionToken> tokenContext) { + return TokenUtils.checkThat(t -> RequiredActionsValidator.validRequiredActions(tokenContext.getSession(), t.getRequiredActions()), + Errors.RESOLVE_REQUIRED_ACTIONS, Messages.INVALID_TOKEN_REQUIRED_ACTIONS + ); + } }
services/src/main/java/org/keycloak/authentication/requiredactions/util/RequiredActionsValidator.java+43 −0 added@@ -0,0 +1,43 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.authentication.requiredactions.util; + +import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + +import java.util.List; + +public class RequiredActionsValidator { + /** + * Validate provided required actions + * + * @param session the {@code KeycloakSession} + * @param requiredActions IDs of tested required actions + */ + public static boolean validRequiredActions(KeycloakSession session, List<String> requiredActions) { + final KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); + + for (String action : requiredActions) { + if (sessionFactory.getProviderFactory(RequiredActionProvider.class, action) == null) { + return false; + } + } + return true; + } +}
services/src/main/java/org/keycloak/services/messages/Messages.java+2 −0 modified@@ -219,6 +219,8 @@ public class Messages { public static final String MISSING_IDENTITY_PROVIDER = "missingIdentityProviderMessage"; + public static final String INVALID_TOKEN_REQUIRED_ACTIONS = "invalidTokenRequiredActions"; + public static final String INVALID_FEDERATED_IDENTITY_ACTION = "invalidFederatedIdentityActionMessage"; public static final String FEDERATED_IDENTITY_NOT_ACTIVE = "federatedIdentityLinkNotActiveMessage";
services/src/main/java/org/keycloak/services/resources/admin/UserResource.java+9 −2 modified@@ -21,8 +21,10 @@ import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.authentication.actiontoken.execactions.ExecuteActionsActionToken; +import org.keycloak.authentication.requiredactions.util.RequiredActionsValidator; import org.keycloak.common.ClientConnection; import org.keycloak.common.Profile; +import org.keycloak.common.util.CollectionUtil; import org.keycloak.common.util.Time; import org.keycloak.credential.CredentialModel; import org.keycloak.email.EmailException; @@ -763,7 +765,7 @@ public Response resetPasswordEmail(@QueryParam(OIDCLoginProtocol.REDIRECT_URI_PA /** - * Send a update account email to the user + * Send an email to the user with a link they can click to execute particular actions. * * An email contains a link the user can click to perform a set of required actions. * The redirectUri and clientId parameters are optional. If no redirect is given, then there will @@ -773,7 +775,7 @@ public Response resetPasswordEmail(@QueryParam(OIDCLoginProtocol.REDIRECT_URI_PA * @param redirectUri Redirect uri * @param clientId Client id * @param lifespan Number of seconds after which the generated token expires - * @param actions required actions the user needs to complete + * @param actions Required actions the user needs to complete * @return */ @Path("execute-actions-email") @@ -803,6 +805,11 @@ public Response executeActionsEmail(@QueryParam(OIDCLoginProtocol.REDIRECT_URI_P clientId = Constants.ACCOUNT_MANAGEMENT_CLIENT_ID; } + if (CollectionUtil.isNotEmpty(actions) && !RequiredActionsValidator.validRequiredActions(session, actions)) { + throw new WebApplicationException( + ErrorResponse.error("Provided invalid required actions", Status.BAD_REQUEST)); + } + ClientModel client = realm.getClientByClientId(clientId); if (client == null) { logger.debugf("Client %s doesn't exist", clientId);
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java+30 −0 modified@@ -41,6 +41,7 @@ import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.arquillian.AuthServerTestEnricher; import org.keycloak.testsuite.arquillian.annotation.DisableFeature; +import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.auth.page.AuthRealm; import org.keycloak.testsuite.cluster.AuthenticationSessionFailoverClusterTest; import org.keycloak.testsuite.pages.AppPage; @@ -66,6 +67,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.hamcrest.Matchers; @@ -74,9 +76,11 @@ import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; +import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; /** * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> @@ -1027,4 +1031,30 @@ public void changeEmailAddressAfterSendingEmail() throws Exception { errorPage.assertCurrent(); assertEquals("The link you clicked is an old stale link and is no longer valid. Maybe you have already verified your email.", errorPage.getError()); } + + @Test + @EnableFeature(value = Profile.Feature.UPDATE_EMAIL, skipRestart = true) + public void actionTokenWithInvalidRequiredActions() throws IOException { + // Send email with required action + testRealm().users().get(testUserId).executeActionsEmail(List.of(RequiredAction.UPDATE_EMAIL.name())); + + Assert.assertEquals(1, greenMail.getReceivedMessages().length); + MimeMessage message = greenMail.getLastReceivedMessage(); + + MailUtils.EmailBody body = MailUtils.getBody(message); + assertThat(body, notNullValue()); + + final String link = MailUtils.getLink(body.getText()); + assertThat(link, notNullValue()); + + // Disable feature and the required action UPDATE_EMAIL provider is not present + testingClient.disableFeature(Profile.Feature.UPDATE_EMAIL); + + driver.navigate().to(link); + + errorPage.assertCurrent(); + + // Required action included in the action token is not valid anymore, because we don't know the provider for it + assertThat(errorPage.getError(), is("Required actions included in the link are not valid")); + } }
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java+19 −1 modified@@ -1553,7 +1553,25 @@ public void sendResetPasswordEmail() { userRep.setEnabled(true); updateUser(user, userRep); - user.executeActionsEmail("invalidClientId", "invalidUri", actions); + user.executeActionsEmail(Arrays.asList( + UserModel.RequiredAction.UPDATE_PASSWORD.name(), + "invalid\"<img src=\"alert(0)\">") + ); + fail("Expected failure"); + } catch (ClientErrorException e) { + assertEquals(400, e.getResponse().getStatus()); + + ErrorRepresentation error = e.getResponse().readEntity(ErrorRepresentation.class); + Assert.assertEquals("Provided invalid required actions", error.getErrorMessage()); + assertAdminEvents.assertEmpty(); + } + + try { + user.executeActionsEmail( + "invalidClientId", + "invalidUri", + Collections.singletonList(UserModel.RequiredAction.UPDATE_PASSWORD.name()) + ); fail("Expected failure"); } catch (ClientErrorException e) { assertEquals(400, e.getResponse().getStatus());
themes/src/main/resources-community/theme/base/login/messages/messages_cs.properties+2 −0 modified@@ -341,6 +341,8 @@ requiredAction.UPDATE_PASSWORD=Aktualizace hesla requiredAction.UPDATE_PROFILE=Aktualizovat profil requiredAction.VERIFY_EMAIL=Ověřit e-mail +invalidTokenRequiredActions=Požadované akce obsažené v daném odkazu nejsou validní + doX509Login=Budete přihlášeni jako\: clientCertificate=Klientský X509 certifikát\: noCertificate=[Žádný certifikát]
themes/src/main/resources/theme/base/login/info.ftl+1 −1 modified@@ -8,7 +8,7 @@ </#if> <#elseif section = "form"> <div id="kc-info-message"> - <p class="instruction">${message.summary}<#if requiredActions??><#list requiredActions>: <b><#items as reqActionItem>${msg("requiredAction.${reqActionItem}")}<#sep>, </#items></b></#list><#else></#if></p> + <p class="instruction">${message.summary}<#if requiredActions??><#list requiredActions>: <b><#items as reqActionItem>${kcSanitize(msg("requiredAction.${reqActionItem}"))?no_esc}<#sep>, </#items></b></#list><#else></#if></p> <#if skipLink??> <#else> <#if pageRedirectUri?has_content>
themes/src/main/resources/theme/base/login/messages/messages_en.properties+2 −0 modified@@ -393,6 +393,8 @@ requiredAction.VERIFY_EMAIL=Verify Email requiredAction.CONFIGURE_RECOVERY_AUTHN_CODES=Generate Recovery Codes requiredAction.webauthn-register-passwordless=Webauthn Register Passwordless +invalidTokenRequiredActions=Required actions included in the link are not valid + doX509Login=You will be logged in as\: clientCertificate=X509 client certificate\: noCertificate=[No Certificate]
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
8- github.com/advisories/GHSA-m4fv-gm5m-4725ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-1274ghsaADVISORY
- bugzilla.redhat.com/show_bug.cgighsaWEB
- github.com/keycloak/keycloak/commit/fc3c61235fa30132123c17ed8702ff7b3a672fe9ghsaWEB
- github.com/keycloak/keycloak/pull/16764ghsaWEB
- github.com/keycloak/keycloak/security/advisories/GHSA-m4fv-gm5m-4725ghsaWEB
- herolab.usd.de/security-advisories/usd-2021-0033ghsaWEB
- herolab.usd.de/security-advisories/usd-2021-0033/mitre
News mentions
0No linked articles in our index yet.