CVE-2025-11537
Description
A flaw was found in Keycloak. When the logging format is configured to a verbose, user-supplied pattern (such as the pre-defined 'long' pattern), sensitive headers including Authorization and Cookie are disclosed to the logs in cleartext. An attacker with read access to the log files can extract these credentials (e.g., bearer tokens, session cookies) and use them to impersonate users, leading to a full account compromise.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.keycloak:keycloak-quarkus-serverMaven | < 26.5.6 | 26.5.6 |
Patches
39622f550a6e5Mask certain HTTP headers in the HTTP access log
5 files changed · +111 −2
docs/documentation/upgrading/topics/changes/changes-26_5_5.adoc+8 −0 added@@ -0,0 +1,8 @@ +== Notable changes + +Notable changes may include internal behavior changes that prevent common misconfigurations, bugs that are fixed, or changes to simplify running {project_name}. +It also lists significant changes to internal APIs. + +=== HTTP Access log does not contain specific sensitive information + +Specific sensitive information is omitted from the HTTP Access log, such as the value of the `Authorization` HTTP header. Moreover, values of specific sensitive {project_name} cookies, such as `KEYCLOAK_SESSION`, `KEYCLOAK_IDENTITY`, or `AUTH_SESSION_ID`, are also omitted.
docs/guides/server/logging.adoc+19 −1 modified@@ -286,10 +286,28 @@ You can even specify your own pattern with your required data to be logged, such <@kc.start parameters="--http-access-log-pattern='%A %{METHOD} %{REQUEST_URL} %{i,User-Agent}'"/> WARNING: HTTP Access logs may contain sensitive HTTP headers like `Authorization`, `Cookie`, or external API keys references. -Be careful with using the `long` pattern or printing the headers by the custom format - you should use it only for development purposes. +The `Authorization` header and selected sensitive cookies are automatically masked in the HTTP Access log. However, the list of masked items might not be complete. Be careful with using the `long` pattern or printing the headers by the custom format - you should use it only for development purposes. Consult the https://quarkus.io/guides/http-reference#configuring-http-access-logs[Quarkus documentation] for the full list of variables that can be used. +=== Masked specific HTTP headers and cookies + +Selected sensitive HTTP headers and cookies are automatically masked in the HTTP Access log. + +Masked sensitive HTTP headers: + +* `Authorization` + +Masked sensitive {project_name} cookies: + +* `AUTH_SESSION_ID` +* `KC_AUTH_SESSION_HASH` +* `KEYCLOAK_IDENTITY` +* `KEYCLOAK_SESSION` +* `AUTH_SESSION_ID_LEGACY` +* `KEYCLOAK_IDENTITY_LEGACY` +* `KEYCLOAK_SESSION_LEGACY` + === Exclude specific URL paths It is possible to exclude specific URL paths from the HTTP access logging, so they will not be recorded.
quarkus/runtime/src/main/resources/application.properties+3 −0 modified@@ -33,6 +33,9 @@ quarkus.log.category."io.quarkus.arc.processor.IndexClassLookupUtils".level=off quarkus.log.category."io.quarkus.hibernate.orm.deployment.HibernateOrmProcessor".level=warn quarkus.log.category."io.quarkus.deployment.steps.ReflectiveHierarchyStep".level=error +# Hide/mask sensitive Keycloak cookies from the HTTP Access log - see https://github.com/keycloak/keycloak/issues/43811 +quarkus.http.access-log.masked-cookies=AUTH_SESSION_ID,KC_AUTH_SESSION_HASH,KEYCLOAK_IDENTITY,KEYCLOAK_SESSION,AUTH_SESSION_ID_LEGACY,KEYCLOAK_IDENTITY_LEGACY,KEYCLOAK_SESSION_LEGACY + # SqlExceptionHelper will log-and-throw error messages. # As those messages might later be caught and handled, this is an antipattern so we prevent logging them # https://hibernate.zulipchat.com/#narrow/channel/132096-hibernate-user/topic/Feature.20Request.3A.20Disable.20logging.20of.20SqlExceptionHelper.20for
quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/cli/PicocliTest.java+6 −0 modified@@ -982,6 +982,12 @@ public void httpAccessLog() { nonRunningPicocli = pseudoLaunch("start-dev", "--http-access-log-enabled=true", "--http-access-log-exclude='/realms/my-realm/.*"); assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode); + onAfter(); + + // masked headers and cookies - default + nonRunningPicocli = pseudoLaunch("start-dev", "--http-access-log-enabled=true"); + assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode); + assertExternalConfig("quarkus.http.access-log.masked-cookies", "AUTH_SESSION_ID,KC_AUTH_SESSION_HASH,KEYCLOAK_IDENTITY,KEYCLOAK_SESSION,AUTH_SESSION_ID_LEGACY,KEYCLOAK_IDENTITY_LEGACY,KEYCLOAK_SESSION_LEGACY"); } @Test
quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/LoggingDistTest.java+75 −1 modified@@ -22,8 +22,13 @@ import java.nio.charset.Charset; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import org.keycloak.config.LoggingOptions; +import org.keycloak.connections.httpclient.HttpClientBuilder; +import org.keycloak.cookie.CookieType; import org.keycloak.it.junit5.extension.CLIResult; import org.keycloak.it.junit5.extension.DistributionTest; import org.keycloak.it.junit5.extension.DryRun; @@ -34,10 +39,16 @@ import io.quarkus.deployment.util.FileUtil; import io.quarkus.test.junit.main.Launch; +import org.apache.commons.io.FileUtils; +import org.apache.http.HttpHeaders; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.util.EntityUtils; +import org.hamcrest.Matchers; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; -import org.testcontainers.shaded.org.apache.commons.io.FileUtils; +import static org.keycloak.OAuth2Constants.DPOP_HTTP_HEADER; import static org.keycloak.quarkus.runtime.cli.command.Main.CONFIG_FILE_LONG_NAME; import static io.restassured.RestAssured.when; @@ -313,4 +324,67 @@ void telemetryLogsEnabled(CLIResult cliResult) { cliResult.assertMessage("service.name=\"keycloak\""); cliResult.assertMessage("Failed to export LogsRequestMarshaler."); } + + @Test + @Launch({"start-dev", "--http-access-log-enabled=true", "--http-access-log-pattern=long"}) + void httpAccessLogMaskedCookies(CLIResult cliResult) { + assertHttpAccessLogMaskedCookies(cliResult); + } + + @Test + @Launch({"start-dev", "--http-access-log-enabled=true", "--http-access-log-pattern='%{ALL_REQUEST_HEADERS}'"}) + void httpAccessLogMaskedCookiesDiffFormat(CLIResult cliResult) { + assertHttpAccessLogMaskedCookies(cliResult); + } + + private void assertHttpAccessLogMaskedCookies(CLIResult cliResult) { + var defaultMaskedCookies = new ArrayList<>(List.of(CookieType.OLD_UNUSED_COOKIES)); + defaultMaskedCookies.add(CookieType.AUTH_SESSION_ID); + defaultMaskedCookies.add(CookieType.AUTH_SESSION_ID_HASH); + defaultMaskedCookies.add(CookieType.IDENTITY); + defaultMaskedCookies.add(CookieType.SESSION); + + var hiddenCookiesFromApplicationProps = Arrays.stream("AUTH_SESSION_ID,KC_AUTH_SESSION_HASH,KEYCLOAK_IDENTITY,KEYCLOAK_SESSION,AUTH_SESSION_ID_LEGACY,KEYCLOAK_IDENTITY_LEGACY,KEYCLOAK_SESSION_LEGACY".split(",")).toList(); + + assertThat(hiddenCookiesFromApplicationProps, Matchers.containsInAnyOrder( + defaultMaskedCookies.stream() + .map(CookieType::getName) + .toArray(String[]::new)) + ); + + cliResult.assertStartedDevMode(); + + try (var httpClient = new HttpClientBuilder().build()) { + var baseRequest = RequestBuilder.post().setUri("http://localhost:8080/realms/master"); + + var sensitiveCookiesRequest = baseRequest.addHeader(HttpHeaders.AUTHORIZATION, "Bearer something-that-should-be-hidden"); + hiddenCookiesFromApplicationProps.forEach(cookie -> sensitiveCookiesRequest.addHeader("Cookie", cookie + "=something-that-should-be-hidden")); + + try (CloseableHttpResponse response = httpClient.execute(sensitiveCookiesRequest.build())) { + assertThat(response, notNullValue()); + EntityUtils.consumeQuietly(response.getEntity()); + } + + var differentAuthorizationHeader = baseRequest + .addHeader(HttpHeaders.AUTHORIZATION, DPOP_HTTP_HEADER + " something-that-should-be-hidden") + .addHeader(HttpHeaders.CONTENT_LANGUAGE, "cs") + .addHeader("Cookie", "SOMETHING=something-not-sensitive") + .build(); + + try (CloseableHttpResponse response = httpClient.execute(differentAuthorizationHeader)) { + assertThat(response, notNullValue()); + EntityUtils.consumeQuietly(response.getEntity()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + // Verify that sensitive cookie values are masked in the access log + cliResult.assertMessage("[org.keycloak.http.access-log]"); + cliResult.assertMessage("Authorization: Bearer ..."); + cliResult.assertMessage("Authorization: DPoP ..."); + cliResult.assertMessage("Cookie: SOMETHING=something-not-sensitive"); + cliResult.assertMessage("Content-Language: cs"); + hiddenCookiesFromApplicationProps.forEach(cookie -> cliResult.assertMessage("Cookie: %s=...".formatted(cookie))); + } }
5a3cdb7c4ccbMask certain HTTP headers in the HTTP access log
5 files changed · +108 −2
docs/documentation/upgrading/topics/changes/changes-26_4_10.adoc+4 −0 modified@@ -27,3 +27,7 @@ bin/kc.[sh|bat] --spi-login-protocol--saml--max-inflating-size=524288 ---- The same restriction is applied for the link:{saml_galleon_layers_link}[{project_name} SAML Galleon feature pack]. Although, in this case, you need to add a system property to the Wildfly/EAP server to change the default maximum size: `-Dorg.keycloak.adapters.saml.maxInflatingSize=524288`. + +=== HTTP Access log does not contain specific sensitive information + +Specific sensitive information is omitted from the HTTP Access log, such as the value of the `Authorization` HTTP header. Moreover, values of specific sensitive {project_name} cookies, such as `KEYCLOAK_SESSION`, `KEYCLOAK_IDENTITY`, or `AUTH_SESSION_ID`, are also omitted.
docs/guides/server/logging.adoc+20 −2 modified@@ -284,11 +284,29 @@ You can even specify your own pattern with your required data to be logged, such <@kc.start parameters="--http-access-log-pattern='%A %{METHOD} %{REQUEST_URL} %{i,User-Agent}'"/> WARNING: HTTP Access logs may contain sensitive HTTP headers like `Authorization`, `Cookie`, or external API keys references. -Be careful with using the `long` pattern or printing the headers by the custom format - you should use it only for development purposes. +The `Authorization` header and selected sensitive cookies are automatically masked in the HTTP Access log. However, the list of masked items might not be complete. Be careful with using the `long` pattern or printing the headers by the custom format - you should use it only for development purposes. Consult the https://quarkus.io/guides/http-reference#configuring-http-access-logs[Quarkus documentation] for the full list of variables that can be used. -==== Exclude specific URL paths +=== Masked specific HTTP headers and cookies + +Selected sensitive HTTP headers and cookies are automatically masked in the HTTP Access log. + +Masked sensitive HTTP headers: + +* `Authorization` + +Masked sensitive {project_name} cookies: + +* `AUTH_SESSION_ID` +* `KC_AUTH_SESSION_HASH` +* `KEYCLOAK_IDENTITY` +* `KEYCLOAK_SESSION` +* `AUTH_SESSION_ID_LEGACY` +* `KEYCLOAK_IDENTITY_LEGACY` +* `KEYCLOAK_SESSION_LEGACY` + +=== Exclude specific URL paths It is possible to exclude specific URL paths from the HTTP access logging, so they will not be recorded.
quarkus/runtime/src/main/resources/application.properties+3 −0 modified@@ -33,6 +33,9 @@ quarkus.log.category."io.quarkus.arc.processor.IndexClassLookupUtils".level=off quarkus.log.category."io.quarkus.hibernate.orm.deployment.HibernateOrmProcessor".level=warn quarkus.log.category."io.quarkus.deployment.steps.ReflectiveHierarchyStep".level=error +# Hide/mask sensitive Keycloak cookies from the HTTP Access log - see https://github.com/keycloak/keycloak/issues/43811 +quarkus.http.access-log.masked-cookies=AUTH_SESSION_ID,KC_AUTH_SESSION_HASH,KEYCLOAK_IDENTITY,KEYCLOAK_SESSION,AUTH_SESSION_ID_LEGACY,KEYCLOAK_IDENTITY_LEGACY,KEYCLOAK_SESSION_LEGACY + # SqlExceptionHelper will log-and-throw error messages. # As those messages might later be caught and handled, this is an antipattern so we prevent logging them # https://hibernate.zulipchat.com/#narrow/channel/132096-hibernate-user/topic/Feature.20Request.3A.20Disable.20logging.20of.20SqlExceptionHelper.20for
quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/cli/PicocliTest.java+6 −0 modified@@ -935,6 +935,12 @@ public void httpAccessLog() { nonRunningPicocli = pseudoLaunch("start-dev", "--http-access-log-enabled=true", "--http-access-log-exclude='/realms/my-realm/.*"); assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode); + onAfter(); + + // masked headers and cookies - default + nonRunningPicocli = pseudoLaunch("start-dev", "--http-access-log-enabled=true"); + assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode); + assertExternalConfig("quarkus.http.access-log.masked-cookies", "AUTH_SESSION_ID,KC_AUTH_SESSION_HASH,KEYCLOAK_IDENTITY,KEYCLOAK_SESSION,AUTH_SESSION_ID_LEGACY,KEYCLOAK_IDENTITY_LEGACY,KEYCLOAK_SESSION_LEGACY"); } @Test
quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/LoggingDistTest.java+75 −0 modified@@ -31,10 +31,15 @@ import java.nio.charset.Charset; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.keycloak.config.LoggingOptions; +import org.keycloak.connections.httpclient.HttpClientBuilder; +import org.keycloak.cookie.CookieType; import org.keycloak.it.junit5.extension.CLIResult; import org.keycloak.it.junit5.extension.DistributionTest; import org.keycloak.it.junit5.extension.DryRun; @@ -46,6 +51,13 @@ import io.quarkus.deployment.util.FileUtil; import io.quarkus.test.junit.main.Launch; +import org.apache.http.HttpHeaders; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.util.EntityUtils; +import org.hamcrest.Matchers; + +import static org.keycloak.OAuth2Constants.DPOP_HTTP_HEADER; @DistributionTest(keepAlive = true) @RawDistOnly(reason = "Too verbose for docker and enough to check raw dist") @@ -302,4 +314,67 @@ void httpAccessLogNotNamedPattern(CLIResult cliResult) { .statusCode(200); cliResult.assertNoMessage("http://127.0.0.1:8080/realms/master/clients/account/redirect"); } + + @Test + @Launch({"start-dev", "--http-access-log-enabled=true", "--http-access-log-pattern=long"}) + void httpAccessLogMaskedCookies(CLIResult cliResult) { + assertHttpAccessLogMaskedCookies(cliResult); + } + + @Test + @Launch({"start-dev", "--http-access-log-enabled=true", "--http-access-log-pattern='%{ALL_REQUEST_HEADERS}'"}) + void httpAccessLogMaskedCookiesDiffFormat(CLIResult cliResult) { + assertHttpAccessLogMaskedCookies(cliResult); + } + + private void assertHttpAccessLogMaskedCookies(CLIResult cliResult) { + var defaultMaskedCookies = new ArrayList<>(List.of(CookieType.OLD_UNUSED_COOKIES)); + defaultMaskedCookies.add(CookieType.AUTH_SESSION_ID); + defaultMaskedCookies.add(CookieType.AUTH_SESSION_ID_HASH); + defaultMaskedCookies.add(CookieType.IDENTITY); + defaultMaskedCookies.add(CookieType.SESSION); + + var hiddenCookiesFromApplicationProps = Arrays.stream("AUTH_SESSION_ID,KC_AUTH_SESSION_HASH,KEYCLOAK_IDENTITY,KEYCLOAK_SESSION,AUTH_SESSION_ID_LEGACY,KEYCLOAK_IDENTITY_LEGACY,KEYCLOAK_SESSION_LEGACY".split(",")).toList(); + + assertThat(hiddenCookiesFromApplicationProps, Matchers.containsInAnyOrder( + defaultMaskedCookies.stream() + .map(CookieType::getName) + .toArray(String[]::new)) + ); + + cliResult.assertStartedDevMode(); + + try (var httpClient = new HttpClientBuilder().build()) { + var baseRequest = RequestBuilder.post().setUri("http://localhost:8080/realms/master"); + + var sensitiveCookiesRequest = baseRequest.addHeader(HttpHeaders.AUTHORIZATION, "Bearer something-that-should-be-hidden"); + hiddenCookiesFromApplicationProps.forEach(cookie -> sensitiveCookiesRequest.addHeader("Cookie", cookie + "=something-that-should-be-hidden")); + + try (CloseableHttpResponse response = httpClient.execute(sensitiveCookiesRequest.build())) { + assertThat(response, notNullValue()); + EntityUtils.consumeQuietly(response.getEntity()); + } + + var differentAuthorizationHeader = baseRequest + .addHeader(HttpHeaders.AUTHORIZATION, DPOP_HTTP_HEADER + " something-that-should-be-hidden") + .addHeader(HttpHeaders.CONTENT_LANGUAGE, "cs") + .addHeader("Cookie", "SOMETHING=something-not-sensitive") + .build(); + + try (CloseableHttpResponse response = httpClient.execute(differentAuthorizationHeader)) { + assertThat(response, notNullValue()); + EntityUtils.consumeQuietly(response.getEntity()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + // Verify that sensitive cookie values are masked in the access log + cliResult.assertMessage("[org.keycloak.http.access-log]"); + cliResult.assertMessage("Authorization: Bearer ..."); + cliResult.assertMessage("Authorization: DPoP ..."); + cliResult.assertMessage("Cookie: SOMETHING=something-not-sensitive"); + cliResult.assertMessage("Content-Language: cs"); + hiddenCookiesFromApplicationProps.forEach(cookie -> cliResult.assertMessage("Cookie: %s=...".formatted(cookie))); + } }
137a35c1109fMask certain HTTP headers and cookies in the HTTP access log (#45400)
12 files changed · +253 −11
docs/documentation/release_notes/topics/26_6_0.adoc+9 −0 added@@ -0,0 +1,9 @@ +// Release notes should contain only headline-worthy new features, +// assuming that people who migrate will read the upgrading guide anyway. + += Sensitive Keycloak information is not displayed in the HTTP Access log + +If you are using the HTTP Access logging capability, sensitive information is omitted. +It means that tokens in the 'Authorization' HTTP header and specific sensitive Keycloak cookies are not shown. + +For more information, see https://www.keycloak.org/server/logging#http-access-logging[Configuring logging].
docs/documentation/upgrading/topics/changes/changes-26_6_0.adoc+4 −0 modified@@ -35,6 +35,10 @@ Previously virtual threads were used when at least two CPU cores were available. Starting with this version, virtual threads are only used when at least four CPU cores are available. This change should prevent deadlocks due to pinned virtual threads. +=== HTTP Access log does not contain specific sensitive information + +Specific sensitive information is omitted from the HTTP Access log, such as the value of the `Authorization` HTTP header. Moreover, values of specific sensitive {project_name} cookies, such as `KEYCLOAK_SESSION`, `KEYCLOAK_IDENTITY`, or `AUTH_SESSION_ID`, are also omitted. + // ------------------------ Deprecated features ------------------------ // == Deprecated features
docs/guides/server/logging.adoc+23 −1 modified@@ -286,10 +286,32 @@ You can even specify your own pattern with your required data to be logged, such <@kc.start parameters="--http-access-log-pattern='%A %{METHOD} %{REQUEST_URL} %{i,User-Agent}'"/> WARNING: HTTP Access logs may contain sensitive HTTP headers like `Authorization`, `Cookie`, or external API keys references. -Be careful with using the `long` pattern or printing the headers by the custom format - you should use it only for development purposes. +The `Authorization` header and selected sensitive cookies are automatically masked in the HTTP Access log. However, the list of masked items might not be complete. Be careful with using the `long` pattern or printing the headers by the custom format - you should use it only for development purposes. To extend the list of the masked items, see below. Consult the https://quarkus.io/guides/http-reference#configuring-http-access-logs[Quarkus documentation] for the full list of variables that can be used. +=== Mask specific HTTP headers and cookies + +Selected sensitive HTTP headers and cookies are automatically masked in the HTTP Access log. + +Masked sensitive HTTP headers: + +* `Authorization` + +Masked sensitive {project_name} cookies: + +* `AUTH_SESSION_ID` +* `KC_AUTH_SESSION_HASH` +* `KEYCLOAK_IDENTITY` +* `KEYCLOAK_SESSION` +* `AUTH_SESSION_ID_LEGACY` +* `KEYCLOAK_IDENTITY_LEGACY` +* `KEYCLOAK_SESSION_LEGACY` + +In order to extend the list of the masked items (e.g. to accommodate for headers and cookies used by your custom extensions), configure the `http-access-log-masked-headers` and `http-access-log-masked-cookies` options. + +<@kc.start parameters="--http-access-log-enabled=true --http-access-log-masked-headers=X-Custom,Y-Api-Token --http-access-log-masked-cookies=MY_COOKIE,MY_SECOND_COOKIE"/> + === Exclude specific URL paths It is possible to exclude specific URL paths from the HTTP access logging, so they will not be recorded.
quarkus/config-api/src/main/java/org/keycloak/config/HttpAccessLogOptions.java+27 −0 modified@@ -1,5 +1,7 @@ package org.keycloak.config; +import java.util.List; + public class HttpAccessLogOptions { public static final Option<Boolean> HTTP_ACCESS_LOG_ENABLED = new OptionBuilder<>("http-access-log-enabled", Boolean.class) @@ -20,4 +22,29 @@ public class HttpAccessLogOptions { .category(OptionCategory.HTTP_ACCESS_LOG) .description("A regular expression that can be used to exclude some paths from logging. For instance, '/realms/my-realm/.*' will exclude all subsequent endpoints for realm 'my-realm' from the log.") .build(); + + public static final List<String> DEFAULT_HIDDEN_HEADERS = List.of( + "Authorization" + ); + + // check the CookieType + public static final List<String> DEFAULT_HIDDEN_COOKIES = List.of( + "AUTH_SESSION_ID", + "KC_AUTH_SESSION_HASH", + "KEYCLOAK_IDENTITY", + "KEYCLOAK_SESSION", + "AUTH_SESSION_ID_LEGACY", + "KEYCLOAK_IDENTITY_LEGACY", + "KEYCLOAK_SESSION_LEGACY" + ); + + public static final Option<List<String>> HTTP_ACCESS_LOG_MASKED_HEADERS = OptionBuilder.listOptionBuilder("http-access-log-masked-headers", String.class) + .category(OptionCategory.HTTP_ACCESS_LOG) + .description("Set of HTTP headers whose values must be masked when the 'long' pattern or '%{ALL_REQUEST_HEADERS}' format is enabled with the 'http-access-log-pattern' option. Selected security sensitive headers are always masked.") + .build(); + + public static final Option<List<String>> HTTP_ACCESS_LOG_MASKED_COOKIES = OptionBuilder.listOptionBuilder("http-access-log-masked-cookies", String.class) + .category(OptionCategory.HTTP_ACCESS_LOG) + .description("Set of HTTP Cookie headers whose values must be masked when the 'long' pattern or '%{ALL_REQUEST_HEADERS}' format is enabled with the 'http-access-log-pattern' option. Selected security sensitive cookies are always masked.") + .build(); }
quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HttpAccessLogPropertyMappers.java+38 −0 modified@@ -1,9 +1,14 @@ package org.keycloak.quarkus.runtime.configuration.mappers; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.keycloak.config.HttpAccessLogOptions; import org.keycloak.quarkus.runtime.configuration.Configuration; +import org.keycloak.utils.StringUtil; + +import io.smallrye.config.ConfigSourceInterceptorContext; import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption; @@ -18,15 +23,48 @@ public List<PropertyMapper<?>> getPropertyMappers() { .build(), fromOption(HttpAccessLogOptions.HTTP_ACCESS_LOG_PATTERN) .isEnabled(HttpAccessLogPropertyMappers::isHttpAccessLogEnabled, ACCESS_LOG_ENABLED_MSG) + .paramLabel("<pattern>") .to("quarkus.http.access-log.pattern") .build(), fromOption(HttpAccessLogOptions.HTTP_ACCESS_LOG_EXCLUDE) .isEnabled(HttpAccessLogPropertyMappers::isHttpAccessLogEnabled, ACCESS_LOG_ENABLED_MSG) + .paramLabel("<exclusions>") .to("quarkus.http.access-log.exclude-pattern") + .build(), + fromOption(HttpAccessLogOptions.HTTP_ACCESS_LOG_MASKED_HEADERS) + .isEnabled(HttpAccessLogPropertyMappers::isHttpAccessLogEnabled, ACCESS_LOG_ENABLED_MSG) + .paramLabel("<headers>") + .to("quarkus.http.access-log.masked-headers") + .transformer(HttpAccessLogPropertyMappers::transformMaskedHeaders) + .build(), + fromOption(HttpAccessLogOptions.HTTP_ACCESS_LOG_MASKED_COOKIES) + .isEnabled(HttpAccessLogPropertyMappers::isHttpAccessLogEnabled, ACCESS_LOG_ENABLED_MSG) + .paramLabel("<cookies>") + .to("quarkus.http.access-log.masked-cookies") + .transformer(HttpAccessLogPropertyMappers::transformMaskedCookies) .build() ); } + private static String transformMaskedHeaders(String value, ConfigSourceInterceptorContext context) { + return transformMaskedElements(value, HttpAccessLogOptions.DEFAULT_HIDDEN_HEADERS); + } + + private static String transformMaskedCookies(String value, ConfigSourceInterceptorContext context) { + return transformMaskedElements(value, HttpAccessLogOptions.DEFAULT_HIDDEN_COOKIES); + } + + private static String transformMaskedElements(String value, List<String> defaultMaskedElements) { + var defaultMasked = new ArrayList<>(defaultMaskedElements); + if (StringUtil.isNotBlank(value)) { + Arrays.stream(value.split(",")) + .filter(f -> !defaultMasked.contains(f)) + .forEach(defaultMasked::add); + } + return String.join(",", defaultMasked); + } + + static boolean isHttpAccessLogEnabled() { return Configuration.isTrue(HttpAccessLogOptions.HTTP_ACCESS_LOG_ENABLED); }
quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/cli/PicocliTest.java+20 −0 modified@@ -30,6 +30,7 @@ import org.keycloak.common.Profile; import org.keycloak.config.LoggingOptions; +import org.keycloak.cookie.CookieType; import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.KeycloakMain; import org.keycloak.quarkus.runtime.cli.command.AbstractAutoBuildCommand; @@ -982,6 +983,25 @@ public void httpAccessLog() { nonRunningPicocli = pseudoLaunch("start-dev", "--http-access-log-enabled=true", "--http-access-log-exclude='/realms/my-realm/.*"); assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode); + onAfter(); + + // masked headers and cookies - default + nonRunningPicocli = pseudoLaunch("start-dev", "--http-access-log-enabled=true"); + assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode); + assertExternalConfig("quarkus.http.access-log.masked-headers", "Authorization"); + assertExternalConfig("quarkus.http.access-log.masked-cookies", "AUTH_SESSION_ID,KC_AUTH_SESSION_HASH,KEYCLOAK_IDENTITY,KEYCLOAK_SESSION,AUTH_SESSION_ID_LEGACY,KEYCLOAK_IDENTITY_LEGACY,KEYCLOAK_SESSION_LEGACY"); + onAfter(); + + // masked headers - custom + nonRunningPicocli = pseudoLaunch("start-dev", "--http-access-log-enabled=true", "--http-access-log-masked-headers=My-custom-header,Authorization"); + assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode); + assertExternalConfig("quarkus.http.access-log.masked-headers", "Authorization,My-custom-header"); + onAfter(); + + // masked cookies - custom + nonRunningPicocli = pseudoLaunch("start-dev", "--http-access-log-enabled=true", "--http-access-log-masked-cookies=MY_CUSTOM_COOKIE," + CookieType.AUTH_SESSION_ID.getName()); + assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode); + assertExternalConfig("quarkus.http.access-log.masked-cookies", "AUTH_SESSION_ID,KC_AUTH_SESSION_HASH,KEYCLOAK_IDENTITY,KEYCLOAK_SESSION,AUTH_SESSION_ID_LEGACY,KEYCLOAK_IDENTITY_LEGACY,KEYCLOAK_SESSION_LEGACY,MY_CUSTOM_COOKIE"); } @Test
quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/LoggingDistTest.java+72 −0 modified@@ -22,8 +22,13 @@ import java.nio.charset.Charset; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import org.keycloak.config.HttpAccessLogOptions; import org.keycloak.config.LoggingOptions; +import org.keycloak.connections.httpclient.HttpClientBuilder; +import org.keycloak.cookie.CookieType; import org.keycloak.it.junit5.extension.CLIResult; import org.keycloak.it.junit5.extension.DistributionTest; import org.keycloak.it.junit5.extension.DryRun; @@ -35,9 +40,15 @@ import io.quarkus.deployment.util.FileUtil; import io.quarkus.test.junit.main.Launch; import org.apache.commons.io.FileUtils; +import org.apache.http.HttpHeaders; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.util.EntityUtils; +import org.hamcrest.Matchers; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import static org.keycloak.OAuth2Constants.DPOP_HTTP_HEADER; import static org.keycloak.quarkus.runtime.cli.command.Main.CONFIG_FILE_LONG_NAME; import static io.restassured.RestAssured.when; @@ -313,4 +324,65 @@ void telemetryLogsEnabled(CLIResult cliResult) { cliResult.assertMessage("service.name=\"keycloak\""); cliResult.assertMessage("Failed to export LogsRequestMarshaler."); } + + @Test + @Launch({"start-dev", "--http-access-log-enabled=true", "--http-access-log-pattern=long"}) + void httpAccessLogMaskedCookies(CLIResult cliResult) { + assertHttpAccessLogMaskedCookies(cliResult); + } + + @Test + @Launch({"start-dev", "--http-access-log-enabled=true", "--http-access-log-pattern='%{ALL_REQUEST_HEADERS}'"}) + void httpAccessLogMaskedCookiesDiffFormat(CLIResult cliResult) { + assertHttpAccessLogMaskedCookies(cliResult); + } + + private void assertHttpAccessLogMaskedCookies(CLIResult cliResult) { + var defaultMaskedCookies = new ArrayList<>(List.of(CookieType.OLD_UNUSED_COOKIES)); + defaultMaskedCookies.add(CookieType.AUTH_SESSION_ID); + defaultMaskedCookies.add(CookieType.AUTH_SESSION_ID_HASH); + defaultMaskedCookies.add(CookieType.IDENTITY); + defaultMaskedCookies.add(CookieType.SESSION); + + assertThat(HttpAccessLogOptions.DEFAULT_HIDDEN_COOKIES, Matchers.containsInAnyOrder( + defaultMaskedCookies.stream() + .map(CookieType::getName) + .toArray(String[]::new)) + ); + + cliResult.assertStartedDevMode(); + + try (var httpClient = new HttpClientBuilder().build()) { + var baseRequest = RequestBuilder.post().setUri("http://localhost:8080/realms/master"); + + var sensitiveCookiesRequest = baseRequest.addHeader(HttpHeaders.AUTHORIZATION, "Bearer something-that-should-be-hidden"); + HttpAccessLogOptions.DEFAULT_HIDDEN_COOKIES.forEach(cookie -> sensitiveCookiesRequest.addHeader("Cookie", cookie + "=something-that-should-be-hidden")); + + try (CloseableHttpResponse response = httpClient.execute(sensitiveCookiesRequest.build())) { + assertThat(response, notNullValue()); + EntityUtils.consumeQuietly(response.getEntity()); + } + + var differentAuthorizationHeader = baseRequest + .addHeader(HttpHeaders.AUTHORIZATION, DPOP_HTTP_HEADER + " something-that-should-be-hidden") + .addHeader(HttpHeaders.CONTENT_LANGUAGE, "cs") + .addHeader("Cookie", "SOMETHING=something-not-sensitive") + .build(); + + try (CloseableHttpResponse response = httpClient.execute(differentAuthorizationHeader)) { + assertThat(response, notNullValue()); + EntityUtils.consumeQuietly(response.getEntity()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + // Verify that sensitive cookie values are masked in the access log + cliResult.assertMessage("[org.keycloak.http.access-log]"); + cliResult.assertMessage("Authorization: Bearer ..."); + cliResult.assertMessage("Authorization: DPoP ..."); + cliResult.assertMessage("Cookie: SOMETHING=something-not-sensitive"); + cliResult.assertMessage("Content-Language: cs"); + HttpAccessLogOptions.DEFAULT_HIDDEN_COOKIES.forEach(cookie -> cliResult.assertMessage("Cookie: %s=...".formatted(cookie))); + } }
quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.approved.txt+12 −2 modified@@ -366,12 +366,22 @@ HTTP Access log: --http-access-log-enabled <true|false> If HTTP access logging is enabled. By default this will log records in console. Default: false. ---http-access-log-exclude <PARAM> +--http-access-log-exclude <exclusions> A regular expression that can be used to exclude some paths from logging. For instance, '/realms/my-realm/.*' will exclude all subsequent endpoints for realm 'my-realm' from the log. Available only when HTTP Access log is enabled. ---http-access-log-pattern <PARAM> +--http-access-log-masked-cookies <cookies> + Set of HTTP Cookie headers whose values must be masked when the 'long' pattern + or '%{ALL_REQUEST_HEADERS}' format is enabled with the + 'http-access-log-pattern' option. Selected security sensitive cookies are + always masked. Available only when HTTP Access log is enabled. +--http-access-log-masked-headers <headers> + Set of HTTP headers whose values must be masked when the 'long' pattern or '% + {ALL_REQUEST_HEADERS}' format is enabled with the 'http-access-log-pattern' + option. Selected security sensitive headers are always masked. Available + only when HTTP Access log is enabled. +--http-access-log-pattern <pattern> The HTTP access log pattern. You can use the available named formats, or use custom format described in Quarkus documentation. Possible values are: common, combined, long, or a custom one. Default: common. Available only
quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.approved.txt+12 −2 modified@@ -367,12 +367,22 @@ HTTP Access log: --http-access-log-enabled <true|false> If HTTP access logging is enabled. By default this will log records in console. Default: false. ---http-access-log-exclude <PARAM> +--http-access-log-exclude <exclusions> A regular expression that can be used to exclude some paths from logging. For instance, '/realms/my-realm/.*' will exclude all subsequent endpoints for realm 'my-realm' from the log. Available only when HTTP Access log is enabled. ---http-access-log-pattern <PARAM> +--http-access-log-masked-cookies <cookies> + Set of HTTP Cookie headers whose values must be masked when the 'long' pattern + or '%{ALL_REQUEST_HEADERS}' format is enabled with the + 'http-access-log-pattern' option. Selected security sensitive cookies are + always masked. Available only when HTTP Access log is enabled. +--http-access-log-masked-headers <headers> + Set of HTTP headers whose values must be masked when the 'long' pattern or '% + {ALL_REQUEST_HEADERS}' format is enabled with the 'http-access-log-pattern' + option. Selected security sensitive headers are always masked. Available + only when HTTP Access log is enabled. +--http-access-log-pattern <pattern> The HTTP access log pattern. You can use the available named formats, or use custom format described in Quarkus documentation. Possible values are: common, combined, long, or a custom one. Default: common. Available only
quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelpAll.approved.txt+12 −2 modified@@ -329,12 +329,22 @@ HTTP Access log: --http-access-log-enabled <true|false> If HTTP access logging is enabled. By default this will log records in console. Default: false. ---http-access-log-exclude <PARAM> +--http-access-log-exclude <exclusions> A regular expression that can be used to exclude some paths from logging. For instance, '/realms/my-realm/.*' will exclude all subsequent endpoints for realm 'my-realm' from the log. Available only when HTTP Access log is enabled. ---http-access-log-pattern <PARAM> +--http-access-log-masked-cookies <cookies> + Set of HTTP Cookie headers whose values must be masked when the 'long' pattern + or '%{ALL_REQUEST_HEADERS}' format is enabled with the + 'http-access-log-pattern' option. Selected security sensitive cookies are + always masked. Available only when HTTP Access log is enabled. +--http-access-log-masked-headers <headers> + Set of HTTP headers whose values must be masked when the 'long' pattern or '% + {ALL_REQUEST_HEADERS}' format is enabled with the 'http-access-log-pattern' + option. Selected security sensitive headers are always masked. Available + only when HTTP Access log is enabled. +--http-access-log-pattern <pattern> The HTTP access log pattern. You can use the available named formats, or use custom format described in Quarkus documentation. Possible values are: common, combined, long, or a custom one. Default: common. Available only
quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelpAll.approved.txt+12 −2 modified@@ -366,12 +366,22 @@ HTTP Access log: --http-access-log-enabled <true|false> If HTTP access logging is enabled. By default this will log records in console. Default: false. ---http-access-log-exclude <PARAM> +--http-access-log-exclude <exclusions> A regular expression that can be used to exclude some paths from logging. For instance, '/realms/my-realm/.*' will exclude all subsequent endpoints for realm 'my-realm' from the log. Available only when HTTP Access log is enabled. ---http-access-log-pattern <PARAM> +--http-access-log-masked-cookies <cookies> + Set of HTTP Cookie headers whose values must be masked when the 'long' pattern + or '%{ALL_REQUEST_HEADERS}' format is enabled with the + 'http-access-log-pattern' option. Selected security sensitive cookies are + always masked. Available only when HTTP Access log is enabled. +--http-access-log-masked-headers <headers> + Set of HTTP headers whose values must be masked when the 'long' pattern or '% + {ALL_REQUEST_HEADERS}' format is enabled with the 'http-access-log-pattern' + option. Selected security sensitive headers are always masked. Available + only when HTTP Access log is enabled. +--http-access-log-pattern <pattern> The HTTP access log pattern. You can use the available named formats, or use custom format described in Quarkus documentation. Possible values are: common, combined, long, or a custom one. Default: common. Available only
quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelpAll.approved.txt+12 −2 modified@@ -364,12 +364,22 @@ HTTP Access log: --http-access-log-enabled <true|false> If HTTP access logging is enabled. By default this will log records in console. Default: false. ---http-access-log-exclude <PARAM> +--http-access-log-exclude <exclusions> A regular expression that can be used to exclude some paths from logging. For instance, '/realms/my-realm/.*' will exclude all subsequent endpoints for realm 'my-realm' from the log. Available only when HTTP Access log is enabled. ---http-access-log-pattern <PARAM> +--http-access-log-masked-cookies <cookies> + Set of HTTP Cookie headers whose values must be masked when the 'long' pattern + or '%{ALL_REQUEST_HEADERS}' format is enabled with the + 'http-access-log-pattern' option. Selected security sensitive cookies are + always masked. Available only when HTTP Access log is enabled. +--http-access-log-masked-headers <headers> + Set of HTTP headers whose values must be masked when the 'long' pattern or '% + {ALL_REQUEST_HEADERS}' format is enabled with the 'http-access-log-pattern' + option. Selected security sensitive headers are always masked. Available + only when HTTP Access log is enabled. +--http-access-log-pattern <pattern> The HTTP access log pattern. You can use the available named formats, or use custom format described in Quarkus documentation. Possible values are: common, combined, long, or a custom one. Default: common. Available only
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-gv3v-2cpp-3pmqghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-11537ghsaADVISORY
- access.redhat.com/security/cve/CVE-2025-11537nvdWEB
- bugzilla.redhat.com/show_bug.cginvdWEB
- github.com/keycloak/keycloak/commit/137a35c1109ff43a305f26264978a3ea21452373ghsaWEB
- github.com/keycloak/keycloak/commit/5a3cdb7c4ccbf83ffc926f70d655a60269d7207bghsaWEB
- github.com/keycloak/keycloak/commit/9622f550a6e565b29a3a37454421f08626791a6cghsaWEB
- www.keycloak.org/server/loggingghsaWEB
News mentions
0No linked articles in our index yet.