VYPR
High severityNVD Advisory· Published Sep 25, 2023· Updated Aug 3, 2024

Keycloak: reflected xss attack

CVE-2022-4137

Description

A reflected cross-site scripting (XSS) vulnerability was found in the 'oob' OAuth endpoint due to incorrect null-byte handling. This issue allows a malicious link to insert an arbitrary URI into a Keycloak error page. This flaw requires a user or administrator to interact with a link in order to be vulnerable. This may compromise user details, allowing it to be changed or collected by an attacker.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A reflected XSS vulnerability in Keycloak's OAuth endpoint allows an attacker to inject arbitrary JavaScript via a crafted link, potentially compromising user data.

Vulnerability

Description

CVE-2022-4137 is a reflected cross-site scripting (XSS) vulnerability found in the 'oob' OAuth endpoint of Keycloak. The root cause is incorrect null-byte handling, which allows an attacker to insert an arbitrary URI into a Keycloak error page. This flaw is present in the way the endpoint processes crafted input and renders error messages without proper sanitization [1].

Exploitation

To exploit this vulnerability, an attacker must create a malicious link that includes a specially crafted parameter. The link, when clicked by a user or administrator, will cause the Keycloak server to reflect the injected JavaScript in the error page response. No authentication is required for the attacker to prepare the malicious link, but the victim must be logged into Keycloak for the attack to succeed in stealing session-related data [1].

Impact

Successful exploitation allows the attacker to execute arbitrary JavaScript in the context of the victim's Keycloak session. This can lead to the compromise of user details, such as session tokens or credentials, enabling the attacker to change or collect user information. The impact is limited to the actions a victim user can perform, but it could lead to account takeover or privilege escalation if an administrator is targeted [1].

Mitigation

Red Hat has released security updates to address this vulnerability in Red Hat Single Sign-On 7.6.3 (via RHSA-2023:1043, RHSA-2023:1044, and RHSA-2023:1045) for various Red Hat Enterprise Linux and RHEL-based products [2][3][4]. Users are strongly advised to apply these patches as soon as possible. There are no known workarounds; upgrading to the fixed version is the only reliable mitigation.

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.keycloak:keycloak-parentMaven
< 20.0.520.0.5

Affected products

5
  • Red Hat/Red Hat Single Sign-On 7v5
    cpe:/a:redhat:red_hat_single_sign_on:7.6
  • Red Hat/Red Hat Single Sign-On 7.6 for RHEL 7v5
    cpe:/a:redhat:red_hat_single_sign_on:7.6::el7
    Range: 0:18.0.6-1.redhat_00001.1.el7sso
  • Red Hat/Red Hat Single Sign-On 7.6 for RHEL 8v5
    cpe:/a:redhat:red_hat_single_sign_on:7.6::el8
    Range: 0:18.0.6-1.redhat_00001.1.el8sso
  • Red Hat/Red Hat Single Sign-On 7.6 for RHEL 9v5
    cpe:/a:redhat:red_hat_single_sign_on:7.6::el9
    Range: 0:18.0.6-1.redhat_00001.1.el9sso
  • ghsa-coords
    Range: < 20.0.5

Patches

1
30d0e9d22dae

Fixes for OOB endpoint and KeycloakSanitizer (#16774)

https://github.com/keycloak/keycloakMarek PosoldaFeb 2, 2023via ghsa
7 files changed · +176 9
  • services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java+1 0 modified
    @@ -275,6 +275,7 @@ protected Response createResponse(LoginFormsPages page) {
                             new OAuthGrantBean(accessCode, client, clientScopesRequested));
                     break;
                 case CODE:
    +                attributes.remove("message"); // No need to include "message" attribute as error is included in separate field anyway
                     attributes.put(OAuth2Constants.CODE, new CodeBean(accessCode, messageType == MessageType.ERROR ? getFirstMessageUnformatted() : null));
                     break;
                 case X509_CONFIRM:
    
  • services/src/main/java/org/keycloak/theme/KeycloakSanitizerMethod.java+26 0 modified
    @@ -19,6 +19,7 @@
     
     import freemarker.template.TemplateMethodModelEx;
     import freemarker.template.TemplateModelException;
    +import org.owasp.html.Encoding;
     
     import java.util.List;
     import java.util.regex.Matcher;
    @@ -40,11 +41,36 @@ public Object exec(List list) throws TemplateModelException {
             }
             
             String html = list.get(0).toString();
    +
    +        html = decodeHtmlFull(html);
    +
             String sanitized = KeycloakSanitizerPolicy.POLICY_DEFINITION.sanitize(html);
             
             return fixURLs(sanitized);
         }
     
    +
    +    // Fully decode HTML. Assume it can be encoded multiple times
    +    private String decodeHtmlFull(String html) {
    +        if (html == null) return null;
    +
    +        int MAX_DECODING_COUNT = 5; // Max count of attempts for decoding HTML (in case it was encoded multiple times)
    +        String decodedHtml;
    +
    +        for (int i = 0; i < MAX_DECODING_COUNT; i++) {
    +            decodedHtml = Encoding.decodeHtml(html);
    +            if (decodedHtml.equals(html)) {
    +                // HTML is decoded. We can return it
    +                return html;
    +            } else {
    +                // Next attempt
    +                html = decodedHtml;
    +            }
    +        }
    +
    +        return "";
    +    }
    +
         private String fixURLs(String msg) {
             Matcher matcher = HREF_PATTERN.matcher(msg);
             if (matcher.find()) {
    
  • services/src/test/java/org/keycloak/theme/KeycloakSanitizerTest.java+16 0 modified
    @@ -73,6 +73,22 @@ public void testUrls() throws Exception {
             html.set(0, "<p><a href=\"javascript:alert('hello!');\">link</a></p>");
             assertResult("<p>link</p>", html);
     
    +        html.set(0, "<p><a href=\"javascript:alert(document.domain);\">link</a></p>");
    +        assertResult("<p>link</p>", html);
    +
    +        html.set(0, "<p><a href=\"javascript&colon;alert(document.domain);\">link</a></p>");
    +        assertResult("<p>link</p>", html);
    +
    +        // Effectively same as previous case, but with \0 character added
    +        html.set(0, "<p><a href=\"javascript&\0colon;alert(document.domain);\">link</a></p>");
    +        assertResult("<p>link</p>", html);
    +
    +        html.set(0, "<p><a href=\"javascript&amp;amp;\0colon;alert(document.domain);\">link</a></p>");
    +        assertResult("<p>link</p>", html);
    +
    +        html.set(0, "<p><a href=\"javascript&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;\0colon;alert(document.domain);\">link</a></p>");
    +        assertResult("", html);
    +
             html.set(0, "<p><a href=\"https://localhost?key=123&msg=abc\">link</a></p>");
             assertResult("<p><a href=\"https://localhost?key=123&msg=abc\" rel=\"nofollow\">link</a></p>", html);
     
    
  • testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/InstalledAppRedirectPage.java+107 0 added
    @@ -0,0 +1,107 @@
    +/*
    + * 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.testsuite.pages;
    +
    +import java.net.URI;
    +import java.net.URISyntaxException;
    +
    +import org.junit.Assert;
    +import org.keycloak.OAuth2Constants;
    +import org.keycloak.common.util.KeycloakUriBuilder;
    +import org.keycloak.services.Urls;
    +import org.openqa.selenium.By;
    +import org.openqa.selenium.NoSuchElementException;
    +import org.openqa.selenium.WebElement;
    +import org.openqa.selenium.support.FindBy;
    +
    +/**
    + * Page represented by code.ftl. It is used by "Installed applications" (KeycloakInstalled)
    + *
    + * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
    + */
    +public class InstalledAppRedirectPage extends AbstractPage {
    +
    +    @FindBy(id = "code")
    +    private WebElement code;
    +
    +    @FindBy(id = "kc-page-title")
    +    private WebElement pageTitle;
    +
    +    @FindBy(className = "alert-error")
    +    private WebElement errorBox;
    +
    +    @Override
    +    public void open() {
    +        throw new UnsupportedOperationException("Use method: open(code, error, errorDescription)");
    +    }
    +
    +
    +    public void open(String realmName, String code, String error, String errorDescription) {
    +        try {
    +            KeycloakUriBuilder kcUriBuilder = KeycloakUriBuilder.fromUri(Urls.realmInstalledAppUrnCallback(new URI(oauth.AUTH_SERVER_ROOT), realmName));
    +            if (code != null) {
    +                kcUriBuilder.queryParam(OAuth2Constants.CODE, code);
    +            }
    +            if (error != null) {
    +                kcUriBuilder.queryParam(OAuth2Constants.ERROR, error);
    +            }
    +            if (errorDescription != null) {
    +                kcUriBuilder.queryParam(OAuth2Constants.ERROR_DESCRIPTION, errorDescription);
    +            }
    +            String oobEndpointUri = kcUriBuilder.build().toString();
    +            driver.navigate().to(oobEndpointUri);
    +        } catch (URISyntaxException use) {
    +            throw new IllegalArgumentException(use);
    +        }
    +    }
    +
    +    @Override
    +    public boolean isCurrent() {
    +        throw new UnsupportedOperationException("Use method 'isCurrentExpectSuccess' or 'isCurrentExpectError'");
    +    }
    +
    +
    +    public String getSuccessCode() {
    +        Assert.assertEquals("Success code", getPageTitleText());
    +        return code.getAttribute("value");
    +    }
    +
    +    public String getPageTitleText() {
    +        return pageTitle.getText();
    +    }
    +
    +    // Check if link is present inside title or error box
    +    public void assertLinkBackToApplicationNotPresent() {
    +        try {
    +            pageTitle.findElement(By.tagName("a"));
    +            throw new AssertionError("Link was present inside title");
    +        } catch (NoSuchElementException nsee) {
    +            // Ignore
    +        }
    +
    +        try {
    +            errorBox.findElement(By.tagName("a"));
    +            throw new AssertionError("Link was present inside error box");
    +        } catch (NoSuchElementException nsee) {
    +            // Ignore
    +        }
    +
    +    }
    +}
    
  • testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/error/EscapeErrorPageTest.java+2 2 modified
    @@ -56,12 +56,12 @@ public void URL() {
     
         @Test
         public void ampersandEscape() {
    -        checkMessage("&lt;img src=&quot;something&quot;&gt;", "<img src=\"something\">");
    +        checkMessage("&lt;img src=&quot;something&quot;&gt;", "");
         }
     
         @Test
         public void hexEscape() {
    -        checkMessage("&#x3C;img src&#61;something&#x2F;&#x3E;", "<img src=something/>");
    +        checkMessage("&#x3C;img src&#61;something&#x2F;&#x3E;", "");
         }
     
         @Test
    
  • testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java+22 5 modified
    @@ -31,7 +31,7 @@
     import org.keycloak.testsuite.AbstractKeycloakTest;
     import org.keycloak.testsuite.AssertEvents;
     import org.keycloak.testsuite.pages.ErrorPage;
    -import org.keycloak.testsuite.pages.PageUtils;
    +import org.keycloak.testsuite.pages.InstalledAppRedirectPage;
     import org.keycloak.testsuite.util.ClientManager;
     import org.keycloak.testsuite.util.OAuthClient;
     import org.openqa.selenium.By;
    @@ -58,6 +58,9 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
         @Page
         private ErrorPage errorPage;
     
    +    @Page
    +    private InstalledAppRedirectPage installedAppPage;
    +
         @Override
         public void addTestRealms(List<RealmRepresentation> testRealms) {
             RealmRepresentation realmRepresentation = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
    @@ -92,16 +95,30 @@ public void authorizationRequestInstalledApp() throws IOException {
     
             oauth.doLogin("test-user@localhost", "password");
     
    -        String title = PageUtils.getPageTitle(driver);
    -        Assert.assertEquals("Success code", title);
    -
    -        driver.findElement(By.id(OAuth2Constants.CODE)).getAttribute("value");
    +        installedAppPage.getSuccessCode();
     
             events.expectLogin().detail(Details.REDIRECT_URI, oauth.AUTH_SERVER_ROOT + "/realms/test/protocol/openid-connect/oauth/oob").assertEvent().getDetails().get(Details.CODE_ID);
     
             ClientManager.realm(adminClient.realm("test")).clientId("test-app").removeRedirectUris(Constants.INSTALLED_APP_URN);
         }
     
    +    @Test
    +    public void authorizationRequestInstalledAppErrors() throws IOException {
    +        String error = "<p><a href=\"javascript&amp;colon;alert(document.domain);\">Back to application</a></p>";
    +        installedAppPage.open("test", null, error, null);
    +
    +        // Assert text escaped and not "a" link present
    +        installedAppPage.assertLinkBackToApplicationNotPresent();
    +        Assert.assertEquals("Error code: <p>Back to application</p>", installedAppPage.getPageTitleText());
    +
    +        error = "<p><a href=\"http://foo.com\">Back to application</a></p>";
    +        installedAppPage.open("test", null, error, null);
    +
    +        // In this case, link is not sanitized as it is valid link, however it is escaped and not shown as a link
    +        installedAppPage.assertLinkBackToApplicationNotPresent();
    +        Assert.assertEquals("Error code: <p><a href=\"http://foo.com\" rel=\"nofollow\">Back to application</a></p>", installedAppPage.getPageTitleText());
    +    }
    +
         @Test
         public void authorizationValidRedirectUri() throws IOException {
             ClientManager.realm(adminClient.realm("test")).clientId("test-app").addRedirectUris(oauth.getRedirectUri());
    
  • themes/src/main/resources/theme/base/login/code.ftl+2 2 modified
    @@ -4,15 +4,15 @@
             <#if code.success>
                 ${msg("codeSuccessTitle")}
             <#else>
    -            ${msg("codeErrorTitle", code.error)}
    +            ${kcSanitize(msg("codeErrorTitle", code.error))}
             </#if>
         <#elseif section = "form">
             <div id="kc-code">
                 <#if code.success>
                     <p>${msg("copyCodeInstruction")}</p>
                     <input id="code" class="${properties.kcTextareaClass!}" value="${code.code}"/>
                 <#else>
    -                <p id="error">${code.error}</p>
    +                <p id="error">${kcSanitize(code.error)}</p>
                 </#if>
             </div>
         </#if>
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

11

News mentions

0

No linked articles in our index yet.