Medium severity4.9NVD Advisory· Published Jan 14, 2025· Updated Apr 15, 2026
CVE-2024-11736
CVE-2024-11736
Description
A vulnerability was found in Keycloak. Admin users may have to access sensitive server environment variables and system properties through user-configurable URLs. When configuring backchannel logout URLs or admin URLs, admin users can include placeholders like ${env.VARNAME} or ${PROPNAME}. The server replaces these placeholders with the actual values of environment variables or system properties during URL processing.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.keycloak:keycloak-quarkus-serverMaven | < 26.0.8 | 26.0.8 |
Patches
17a76858fe4aaRestrict access to environment variables when at the server runtime
26 files changed · +269 −159
adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/KeyParser.java+8 −3 modified@@ -19,6 +19,7 @@ import org.keycloak.adapters.saml.config.Key; import org.keycloak.common.util.StringPropertyReplacer; +import org.keycloak.common.util.SystemEnvProperties; import org.keycloak.saml.common.exceptions.ParsingException; import org.keycloak.saml.common.util.StaxParserUtil; @@ -60,20 +61,24 @@ protected void processSubElement(XMLEventReader xmlEventReader, Key target, Keyc case CERTIFICATE_PEM: StaxParserUtil.advance(xmlEventReader); value = StaxParserUtil.getElementText(xmlEventReader); - target.setCertificatePem(StringPropertyReplacer.replaceProperties(value)); + target.setCertificatePem(replaceProperties(value)); break; case PUBLIC_KEY_PEM: StaxParserUtil.advance(xmlEventReader); value = StaxParserUtil.getElementText(xmlEventReader); - target.setPublicKeyPem(StringPropertyReplacer.replaceProperties(value)); + target.setPublicKeyPem(replaceProperties(value)); break; case PRIVATE_KEY_PEM: StaxParserUtil.advance(xmlEventReader); value = StaxParserUtil.getElementText(xmlEventReader); - target.setPrivateKeyPem(StringPropertyReplacer.replaceProperties(value)); + target.setPrivateKeyPem(replaceProperties(value)); break; } } + + private String replaceProperties(String value) { + return StringPropertyReplacer.replaceProperties(value, SystemEnvProperties.UNFILTERED::getProperty); + } }
authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java+0 −2 modified@@ -21,7 +21,6 @@ import java.io.IOException; import java.io.InputStream; -import java.util.Objects; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; @@ -33,7 +32,6 @@ import org.keycloak.common.crypto.CryptoIntegration; import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.representations.AccessTokenResponse; -import org.keycloak.util.SystemPropertiesJsonParserFactory; /** * <p>This is class serves as an entry point for clients looking for access to Keycloak Authorization Services.
authz/client/src/main/java/org/keycloak/authorization/client/SystemPropertiesJsonParserFactory.java+7 −10 renamed@@ -15,7 +15,11 @@ * limitations under the License. */ -package org.keycloak.util; +package org.keycloak.authorization.client; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.io.IOContext; @@ -24,19 +28,12 @@ import org.keycloak.common.util.StringPropertyReplacer; import org.keycloak.common.util.SystemEnvProperties; -import java.io.IOException; -import java.io.InputStream; -import java.io.Reader; -import java.util.Properties; - /** * Provides replacing of system properties for parsed values * * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> */ -public class SystemPropertiesJsonParserFactory extends MappingJsonFactory { - - private static final Properties properties = new SystemEnvProperties(); +class SystemPropertiesJsonParserFactory extends MappingJsonFactory { @Override protected JsonParser _createParser(InputStream in, IOContext ctxt) throws IOException { @@ -71,7 +68,7 @@ public SystemPropertiesAwareJsonParser(JsonParser d) { @Override public String getText() throws IOException { String orig = super.getText(); - return StringPropertyReplacer.replaceProperties(orig, properties); + return StringPropertyReplacer.replaceProperties(orig, SystemEnvProperties.UNFILTERED::getProperty); } } }
authz/client/src/test/java/org/keycloak/authorization/client/JsonParserTest.java+57 −0 added@@ -0,0 +1,57 @@ +/* + * Copyright 2024 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.authorization.client; + +import java.io.IOException; +import java.io.InputStream; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.representations.adapters.config.AdapterConfig; + +/** + * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> + */ +public class JsonParserTest { + + @Test + public void testParsingSystemProps() throws IOException { + System.setProperty("my.host", "foo"); + System.setProperty("con.pool.size", "200"); + System.setProperty("allow.any.hostname", "true"); + System.setProperty("socket.timeout.millis", "6000"); + System.setProperty("connection.timeout.millis", "7000"); + System.setProperty("connection.ttl.millis", "500"); + + InputStream is = getClass().getClassLoader().getResourceAsStream("keycloak.json"); + + ObjectMapper mapper = new ObjectMapper(new SystemPropertiesJsonParserFactory()); + AdapterConfig config = mapper.readValue(is, AdapterConfig.class); + Assert.assertEquals("http://foo:8080/auth", config.getAuthServerUrl()); + Assert.assertEquals("external", config.getSslRequired()); + Assert.assertEquals("angular-product${non.existing}", config.getResource()); + Assert.assertTrue(config.isPublicClient()); + Assert.assertTrue(config.isAllowAnyHostname()); + Assert.assertEquals(100, config.getCorsMaxAge()); + Assert.assertEquals(200, config.getConnectionPoolSize()); + Assert.assertEquals(6000L, config.getSocketTimeout()); + Assert.assertEquals(7000L, config.getConnectionTimeout()); + Assert.assertEquals(500L, config.getConnectionTTL()); + } +}
authz/client/src/test/resources/keycloak.json+12 −0 added@@ -0,0 +1,12 @@ +{ + "auth-server-url" : "http://${my.host}:8080/auth", + "ssl-required" : "external", + "resource" : "angular-product${non.existing}", + "public-client" : true, + "allow-any-hostname": "${allow.any.hostname}", + "cors-max-age": 100, + "connection-pool-size": "${con.pool.size}", + "socket-timeout-millis": "${socket.timeout.millis}", + "connection-timeout-millis": "${connection.timeout.millis}", + "connection-ttl-millis": "${connection.ttl.millis}" +} \ No newline at end of file
common/src/main/java/org/keycloak/common/util/StringPropertyReplacer.java+31 −36 modified@@ -17,22 +17,20 @@ package org.keycloak.common.util; import java.io.File; -import java.util.Properties; +import java.util.Optional; /** - * A utility class for replacing properties in strings. + * A utility class for replacing properties in strings. * * @author <a href="mailto:jason@planet57.com">Jason Dillon</a> * @author <a href="Scott.Stark@jboss.org">Scott Stark</a> * @author <a href="claudio.vesco@previnet.it">Claudio Vesco</a> * @author <a href="mailto:adrian@jboss.com">Adrian Brock</a> * @author <a href="mailto:dimitris@jboss.org">Dimitris Andreadis</a> - * @version <tt>$Revision: 2898 $</tt> + * @version <tt>$Revision: 2898 $</tt> */ public final class StringPropertyReplacer { - /** New line string constant */ - public static final String NEWLINE = System.getProperty("line.separator", "\n"); /** File separator value */ private static final String FILE_SEPARATOR = File.separator; @@ -51,7 +49,12 @@ public final class StringPropertyReplacer private static final int SEEN_DOLLAR = 1; private static final int IN_BRACKET = 2; - private static final Properties systemEnvProperties = new SystemEnvProperties(); + private static final PropertyResolver NULL_RESOLVER = property -> null; + private static PropertyResolver DEFAULT_PROPERTY_RESOLVER; + + public static void setDefaultPropertyResolver(PropertyResolver systemVariables) { + DEFAULT_PROPERTY_RESOLVER = systemVariables; + } /** * Go through the input string and replace any occurrence of ${p} with @@ -72,14 +75,13 @@ public final class StringPropertyReplacer * @return the input string with all property references replaced if any. * If there are no valid references the input string will be returned. */ - public static String replaceProperties(final String string) - { - return replaceProperties(string, (Properties) null); + public static String replaceProperties(final String string) { + return replaceProperties(string, getDefaultPropertyResolver()); } /** * Go through the input string and replace any occurrence of ${p} with - * the props.getProperty(p) value. If there is no such property p defined, + * the value resolves from {@code resolver}. If there is no such property p defined, * then the ${p} reference will remain unchanged. * * If the property reference is of the form ${p:v} and there is no such property p, @@ -93,17 +95,10 @@ public static String replaceProperties(final String string) * value and the property ${:} is replaced with System.getProperty("path.separator"). * * @param string - the string with possible ${} references - * @param props - the source for ${x} property ref values, null means use System.getProperty() + * @param resolver - the property resolver * @return the input string with all property references replaced if any. * If there are no valid references the input string will be returned. */ - public static String replaceProperties(final String string, final Properties props) { - if (props == null) { - return replaceProperties(string, (PropertyResolver) null); - } - return replaceProperties(string, props::getProperty); - } - public static String replaceProperties(final String string, PropertyResolver resolver) { if(string == null) { @@ -171,10 +166,7 @@ else if (PATH_SEPARATOR_ALIAS.equals(key)) else { // check from the properties - if (resolver != null) - value = resolver.resolve(key); - else - value = systemEnvProperties.getProperty(key); + value = resolveValue(resolver, key); if (value == null) { @@ -183,10 +175,7 @@ else if (PATH_SEPARATOR_ALIAS.equals(key)) if (colon > 0) { String realKey = key.substring(0, colon); - if (resolver != null) - value = resolver.resolve(realKey); - else - value = systemEnvProperties.getProperty(realKey); + value = resolveValue(resolver, realKey); if (value == null) { @@ -239,7 +228,7 @@ else if (PATH_SEPARATOR_ALIAS.equals(key)) throw new IllegalStateException("Infinite recursion happening when replacing properties on '" + buffer + "'"); } } - + // Done return buffer.toString(); } @@ -257,26 +246,32 @@ private static String resolveCompositeKey(String key, PropertyResolver resolver) { // Check the first part String key1 = key.substring(0, comma); - if (resolver != null) - value = resolver.resolve(key1); - else - value = systemEnvProperties.getProperty(key1); + value = resolveValue(resolver, key1); } // Check the second part, if there is one and first lookup failed if (value == null && comma < key.length() - 1) { String key2 = key.substring(comma + 1); - if (resolver != null) - value = resolver.resolve(key2); - else - value = systemEnvProperties.getProperty(key2); + value = resolveValue(resolver, key2); } } // Return whatever we've found or null return value; } - + public interface PropertyResolver { String resolve(String property); } + + private static String resolveValue(PropertyResolver resolver, String key) { + if (resolver == null) { + return getDefaultPropertyResolver().resolve(key); + } + + return resolver.resolve(key); + } + + private static PropertyResolver getDefaultPropertyResolver() { + return Optional.ofNullable(DEFAULT_PROPERTY_RESOLVER).orElse(NULL_RESOLVER); + } }
common/src/main/java/org/keycloak/common/util/SystemEnvProperties.java+39 −2 modified@@ -17,19 +17,53 @@ package org.keycloak.common.util; +import java.util.Collections; +import java.util.Optional; import java.util.Properties; +import java.util.Set; /** + * <p>An utility class to resolve the value of a key based on the environment variables + * and system properties available at runtime. In most cases, you do not want to resolve whatever system variable is available at runtime but specify which ones + * can be used when resolving placeholders. + * + * <p>To resolve to an environment variable, the key must have a format like {@code env.<key>} where {@code key} is the name of an environment variable. + * For system properties, there is no specific format and the value is resolved from a system property that matches the key. + * * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> */ public class SystemEnvProperties extends Properties { + /** + * <p>An variation of {@link SystemEnvProperties} that gives unrestricted access to any system variable available at runtime. + * Most of the time you don't want to use this class but favor creating a {@link SystemEnvProperties} instance that + * filters which system variables should be available at runtime. + */ + public static final SystemEnvProperties UNFILTERED = new SystemEnvProperties(Collections.emptySet()) { + @Override + protected boolean isAllowed(String key) { + return true; + } + }; + + private final Set<String> allowedSystemVariables; + + /** + * Creates a new instance where system variables where only specific keys can be resolved from system variables. + * + * @param allowedSystemVariables the keys of system variables that should be available at runtime + */ + public SystemEnvProperties(Set<String> allowedSystemVariables) { + this.allowedSystemVariables = Optional.ofNullable(allowedSystemVariables).orElse(Collections.emptySet()); + } + @Override public String getProperty(String key) { if (key.startsWith("env.")) { - return System.getenv().get(key.substring(4)); + String envKey = key.substring(4); + return isAllowed(envKey) ? System.getenv().get(envKey) : null; } else { - return System.getProperty(key); + return isAllowed(key) ? System.getProperty(key) : null; } } @@ -39,4 +73,7 @@ public String getProperty(String key, String defaultValue) { return value != null ? value : defaultValue; } + protected boolean isAllowed(String key) { + return allowedSystemVariables.contains(key); + } }
common/src/test/java/org/keycloak/common/util/StringPropertyReplacerTest.java+16 −12 modified@@ -33,35 +33,35 @@ public class StringPropertyReplacerTest { @Test public void testSystemProperties() throws NoSuchAlgorithmException { System.setProperty("prop1", "val1"); - Assert.assertEquals("foo-val1", StringPropertyReplacer.replaceProperties("foo-${prop1}")); + Assert.assertEquals("foo-val1", replaceProperties("foo-${prop1}")); - Assert.assertEquals("foo-def", StringPropertyReplacer.replaceProperties("foo-${prop2:def}")); + Assert.assertEquals("foo-def", replaceProperties("foo-${prop2:def}")); System.setProperty("prop2", "val2"); - Assert.assertEquals("foo-val2", StringPropertyReplacer.replaceProperties("foo-${prop2:def}")); + Assert.assertEquals("foo-val2", replaceProperties("foo-${prop2:def}")); // It looks for the property "prop3", then fallback to "prop4", then fallback to "prop5" and finally default value. // This syntax is supported by Quarkus (and underlying Microprofile) - Assert.assertEquals("foo-def", StringPropertyReplacer.replaceProperties("foo-${prop3:${prop4:${prop5:def}}}")); + Assert.assertEquals("foo-def", replaceProperties("foo-${prop3:${prop4:${prop5:def}}}")); System.setProperty("prop5", "val5"); - Assert.assertEquals("foo-val5", StringPropertyReplacer.replaceProperties("foo-${prop3:${prop4:${prop5:def}}}")); + Assert.assertEquals("foo-val5", replaceProperties("foo-${prop3:${prop4:${prop5:def}}}")); System.setProperty("prop4", "val4"); - Assert.assertEquals("foo-val4", StringPropertyReplacer.replaceProperties("foo-${prop3:${prop4:${prop5:def}}}")); + Assert.assertEquals("foo-val4", replaceProperties("foo-${prop3:${prop4:${prop5:def}}}")); System.setProperty("prop3", "val3"); - Assert.assertEquals("foo-val3", StringPropertyReplacer.replaceProperties("foo-${prop3:${prop4:${prop5:def}}}")); + Assert.assertEquals("foo-val3", replaceProperties("foo-${prop3:${prop4:${prop5:def}}}")); // It looks for the property "prop6", then fallback to "prop7" then fallback to value "def" . // This syntax is not supported by Quarkus (microprofile), however Wildfly probably supports this - Assert.assertEquals("foo-def", StringPropertyReplacer.replaceProperties("foo-${prop6,prop7:def}")); + Assert.assertEquals("foo-def", replaceProperties("foo-${prop6,prop7:def}")); System.setProperty("prop7", "val7"); - Assert.assertEquals("foo-val7", StringPropertyReplacer.replaceProperties("foo-${prop6,prop7:def}")); + Assert.assertEquals("foo-val7", replaceProperties("foo-${prop6,prop7:def}")); System.setProperty("prop6", "val6"); - Assert.assertEquals("foo-val6", StringPropertyReplacer.replaceProperties("foo-${prop6,prop7:def}")); + Assert.assertEquals("foo-val6", replaceProperties("foo-${prop6,prop7:def}")); } @Test public void testStackOverflow() { System.setProperty("prop", "${prop}"); - IllegalStateException ise = Assert.assertThrows(IllegalStateException.class, () -> StringPropertyReplacer.replaceProperties("${prop}")); + IllegalStateException ise = Assert.assertThrows(IllegalStateException.class, () -> replaceProperties("${prop}")); Assert.assertEquals("Infinite recursion happening when replacing properties on '${prop}'", ise.getMessage()); } @@ -72,9 +72,13 @@ public void testEnvironmentVariables() throws NoSuchAlgorithmException { for (String key : env.keySet()) { String value = env.get(key); if ( !(value == null || "".equals(value)) ) { - Assert.assertEquals("foo-" + value, StringPropertyReplacer.replaceProperties("foo-${env." + key + "}")); + Assert.assertEquals("foo-" + value, replaceProperties("foo-${env." + key + "}")); break; } } } + + private String replaceProperties(String key) { + return StringPropertyReplacer.replaceProperties(key, SystemEnvProperties.UNFILTERED::getProperty); + } }
core/src/main/java/org/keycloak/Config.java+31 −0 modified@@ -17,8 +17,15 @@ package org.keycloak; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.Set; +import org.keycloak.common.util.StringPropertyReplacer; +import org.keycloak.common.util.StringPropertyReplacer.PropertyResolver; +import org.keycloak.common.util.SystemEnvProperties; + /** * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> */ @@ -28,6 +35,14 @@ public class Config { public static void init(ConfigProvider configProvider) { Config.configProvider = configProvider; + StringPropertyReplacer.setDefaultPropertyResolver(new PropertyResolver() { + SystemEnvProperties systemVariables = new SystemEnvProperties(Config.getAllowedSystemVariables()); + + @Override + public String resolve(String property) { + return systemVariables.getProperty(property); + } + }); } public static String getAdminRealm() { @@ -56,6 +71,22 @@ public static Scope scope(String... scope) { return configProvider.scope(scope); } + private static Set<String> getAllowedSystemVariables() { + Scope adminScope = configProvider.scope("admin"); + + if (adminScope == null) { + return Collections.emptySet(); + } + + String[] allowedSystemVariables = adminScope.getArray("allowed-system-variables"); + + if (allowedSystemVariables == null) { + return Collections.emptySet(); + } + + return new HashSet<>(Arrays.asList(allowedSystemVariables)); + } + public static interface ConfigProvider { String getProvider(String spi);
core/src/main/java/org/keycloak/util/JsonSerialization.java+1 −10 modified@@ -40,7 +40,6 @@ public class JsonSerialization { public static final ObjectMapper mapper = new ObjectMapper(); public static final ObjectMapper prettyMapper = new ObjectMapper(); - public static final ObjectMapper sysPropertiesAwareMapper = new ObjectMapper(new SystemPropertiesJsonParserFactory()); static { mapper.registerModule(new Jdk8Module()); @@ -80,7 +79,7 @@ public static <T> T readValue(String bytes, Class<T> type) throws IOException { } public static <T> T readValue(InputStream bytes, Class<T> type) throws IOException { - return readValue(bytes, type, false); + return mapper.readValue(bytes, type); } public static <T> T readValue(String string, TypeReference<T> type) throws IOException { @@ -91,14 +90,6 @@ public static <T> T readValue(InputStream bytes, TypeReference<T> type) throws I return mapper.readValue(bytes, type); } - public static <T> T readValue(InputStream bytes, Class<T> type, boolean replaceSystemProperties) throws IOException { - if (replaceSystemProperties) { - return sysPropertiesAwareMapper.readValue(bytes, type); - } else { - return mapper.readValue(bytes, type); - } - } - /** * Creates an {@link ObjectNode} based on the given {@code pojo}, copying all its properties to the resulting {@link ObjectNode}. *
core/src/test/java/org/keycloak/JsonParserTest.java+1 −26 modified@@ -23,7 +23,6 @@ import org.keycloak.representations.ClaimsRepresentation; import org.keycloak.representations.IDToken; import org.keycloak.representations.JsonWebToken; -import org.keycloak.representations.adapters.config.AdapterConfig; import org.keycloak.representations.idm.ClientPoliciesRepresentation; import org.keycloak.representations.idm.ClientPolicyConditionConfigurationRepresentation; import org.keycloak.representations.idm.ClientPolicyConditionRepresentation; @@ -97,30 +96,6 @@ public void testUnwrap() throws Exception { Assert.assertNotNull(nested.get("foo")); } - @Test - public void testParsingSystemProps() throws IOException { - System.setProperty("my.host", "foo"); - System.setProperty("con.pool.size", "200"); - System.setProperty("allow.any.hostname", "true"); - System.setProperty("socket.timeout.millis", "6000"); - System.setProperty("connection.timeout.millis", "7000"); - System.setProperty("connection.ttl.millis", "500"); - - InputStream is = getClass().getClassLoader().getResourceAsStream("keycloak.json"); - - AdapterConfig config = JsonSerialization.readValue(is, AdapterConfig.class, true); - Assert.assertEquals("http://foo:8080/auth", config.getAuthServerUrl()); - Assert.assertEquals("external", config.getSslRequired()); - Assert.assertEquals("angular-product${non.existing}", config.getResource()); - Assert.assertTrue(config.isPublicClient()); - Assert.assertTrue(config.isAllowAnyHostname()); - Assert.assertEquals(100, config.getCorsMaxAge()); - Assert.assertEquals(200, config.getConnectionPoolSize()); - Assert.assertEquals(6000L, config.getSocketTimeout()); - Assert.assertEquals(7000L, config.getConnectionTimeout()); - Assert.assertEquals(500L, config.getConnectionTTL()); - } - static Pattern substitution = Pattern.compile("\\$\\{([^}]+)\\}"); @Test @@ -263,4 +238,4 @@ private <T> void assertClaimValue(ClaimsRepresentation.ClaimValue<T> claimVal, B } } -} \ No newline at end of file +}
docs/documentation/upgrading/topics/changes/changes-26_0_8.adoc+7 −0 added@@ -0,0 +1,7 @@ += Deprecating using system variables in the realm configuration + +To favor a more secure server runtime and avoid to accidentally expose system variables, you are now forced to specify +which system variables you want to expose by using the `spi-admin-allowed-system-variables` configuration option when +starting the server. + +In future releases, this capability will be removed in favor of preventing any usage of system variables in the realm configuration.
docs/guides/server/configuration.adoc+14 −0 modified@@ -266,6 +266,20 @@ https-certificate-file You can achieve most optimizations to startup and runtime behavior by using the `build` command. Also, by using the `keycloak.conf` file as a configuration source, you avoid some steps at startup that would otherwise require command line parameters, such as initializing the CLI itself. As a result, the server starts up even faster. +== Using system variables in the realm configuration + +Some of the realm capabilities allow administrators to reference system variables such as environment variables and system properties when configuring +the realm and its components. + +By default, {project_name} disallow using system variables but only those explicitly specified through the `spi-admin-allowed-system-variables` configuration +option. This option allows you to specify a comma-separated list of keys that will eventually resolve to values from system variables with the same key. + +. Start the server and expose a set of system variables to the server runtime ++ + <@kc.start parameters="--spi-admin-allowed-system-variables=FOO,BAR"/> + +In future releases, this capability will be removed in favor of preventing any usage of system variables in the realm configuration. + == Underlying concepts This section gives an overview of the underlying concepts {project_name} uses, especially when it comes to optimizing the startup.
model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java+1 −1 modified@@ -377,7 +377,7 @@ public Connection getConnection() { url = addH2NonKeywords(url); } Class.forName(driver); - return DriverManager.getConnection(StringPropertyReplacer.replaceProperties(url, System.getProperties()), config.get("user"), config.get("password")); + return DriverManager.getConnection(StringPropertyReplacer.replaceProperties(url, System.getProperties()::getProperty), config.get("user"), config.get("password")); } } catch (Exception e) { throw new RuntimeException("Failed to connect to database", e);
saml-core/src/main/java/org/keycloak/saml/common/util/StaxParserUtil.java+4 −3 modified@@ -17,6 +17,7 @@ package org.keycloak.saml.common.util; import org.keycloak.common.util.StringPropertyReplacer; +import org.keycloak.common.util.SystemEnvProperties; import org.keycloak.saml.common.ErrorCodes; import org.keycloak.saml.common.PicketLinkLogger; import org.keycloak.saml.common.PicketLinkLoggerFactory; @@ -210,7 +211,7 @@ public static String getAttributeValueRP(Attribute attribute) { final String value = attribute.getValue(); - return value == null ? null : trim(StringPropertyReplacer.replaceProperties(value)); + return value == null ? null : trim(StringPropertyReplacer.replaceProperties(value, SystemEnvProperties.UNFILTERED::getProperty)); } /** @@ -250,7 +251,7 @@ public static String getAttributeValue(StartElement startElement, HasQName attrN */ public static String getAttributeValueRP(StartElement startElement, HasQName attrName) { final String value = getAttributeValue(startElement, attrName.getQName()); - return value == null ? null : StringPropertyReplacer.replaceProperties(value); + return value == null ? null : StringPropertyReplacer.replaceProperties(value, SystemEnvProperties.UNFILTERED::getProperty); } /** @@ -535,7 +536,7 @@ public static String getElementText(XMLEventReader xmlEventReader) throws Parsin */ public static String getElementTextRP(XMLEventReader xmlEventReader) throws ParsingException { try { - return trim(StringPropertyReplacer.replaceProperties(xmlEventReader.getElementText())); + return trim(StringPropertyReplacer.replaceProperties(xmlEventReader.getElementText(), SystemEnvProperties.UNFILTERED::getProperty)); } catch (XMLStreamException e) { throw logger.parserException(e); }
services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java+3 −12 modified@@ -26,7 +26,6 @@ import org.keycloak.TokenIdGenerator; import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.common.util.MultivaluedHashMap; -import org.keycloak.common.util.StringPropertyReplacer; import org.keycloak.common.util.Time; import org.keycloak.connections.httpclient.HttpClientProvider; import org.keycloak.constants.AdapterConstants; @@ -58,7 +57,6 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Stream; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.impl.client.CloseableHttpClient; @@ -77,8 +75,7 @@ public ResourceAdminManager(KeycloakSession session) { } public static String resolveUri(KeycloakSession session, String rootUrl, String uri) { - String absoluteURI = ResolveRelative.resolveRelativeUri(session, rootUrl, uri); - return StringPropertyReplacer.replaceProperties(absoluteURI); + return ResolveRelative.resolveRelativeUri(session, rootUrl, uri); } @@ -88,10 +85,7 @@ public static String getManagementUrl(KeycloakSession session, ClientModel clien return null; } - String absoluteURI = ResolveRelative.resolveRelativeUri(session, client.getRootUrl(), mgmtUrl); - - // this is for resolving URI like "http://${jboss.host.name}:8080/..." in order to send request to same machine and avoid request to LB in cluster environment - return StringPropertyReplacer.replaceProperties(absoluteURI); + return ResolveRelative.resolveRelativeUri(session, client.getRootUrl(), mgmtUrl); } // For non-cluster setup, return just single configured managementUrls @@ -192,10 +186,7 @@ public static String getBackchannelLogoutUrl(KeycloakSession session, ClientMode return null; } - String absoluteURI = ResolveRelative.resolveRelativeUri(session, client.getRootUrl(), backchannelLogoutUrl); - // this is for resolving URI like "http://${jboss.host.name}:8080/..." in order to send request to same machine - // and avoid request to LB in cluster environment - return StringPropertyReplacer.replaceProperties(absoluteURI); + return ResolveRelative.resolveRelativeUri(session, client.getRootUrl(), backchannelLogoutUrl); } protected Response sendBackChannelLogoutRequestToClientUri(ClientModel resource,
services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java+1 −1 modified@@ -264,7 +264,7 @@ private ClientRepresentation modelToRepresentation(ClientModel model, List<Strin private ConsentRepresentation modelToRepresentation(UserConsentModel model) { List<ConsentScopeRepresentation> grantedScopes = model.getGrantedClientScopes().stream() - .map(m -> new ConsentScopeRepresentation(m.getId(), m.getConsentScreenText()!= null ? m.getConsentScreenText() : m.getName(), StringPropertyReplacer.replaceProperties(m.getConsentScreenText(), getProperties()))) + .map(m -> new ConsentScopeRepresentation(m.getId(), m.getConsentScreenText()!= null ? m.getConsentScreenText() : m.getName(), StringPropertyReplacer.replaceProperties(m.getConsentScreenText(), getProperties()::getProperty))) .collect(Collectors.toList()); return new ConsentRepresentation(grantedScopes, model.getCreatedDate(), model.getLastUpdatedDate()); }
services/src/main/java/org/keycloak/services/util/ResolveRelative.java+9 −3 modified@@ -17,6 +17,7 @@ package org.keycloak.services.util; +import org.keycloak.common.util.StringPropertyReplacer; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.urls.UrlType; @@ -36,14 +37,19 @@ public static String resolveRelativeUri(KeycloakSession session, String rootUrl, } public static String resolveRelativeUri(String frontendUrl, String adminUrl, String rootUrl, String url) { + String finalUrl; + if (url == null || !url.startsWith("/")) { - return url; + finalUrl = url; } else if (rootUrl != null && !rootUrl.isEmpty()) { - return resolveRootUrl(frontendUrl, adminUrl, rootUrl) + url; + finalUrl = resolveRootUrl(frontendUrl, adminUrl, rootUrl) + url; } else { - return UriBuilder.fromUri(frontendUrl).replacePath(url).build().toString(); + finalUrl = UriBuilder.fromUri(frontendUrl).replacePath(url).build().toString(); } + + return StringPropertyReplacer.replaceProperties(finalUrl); } + public static String resolveRootUrl(KeycloakSession session, String rootUrl) { String frontendUrl = session.getContext().getUri(UrlType.FRONTEND).getBaseUri().toString(); String adminUrl = session.getContext().getUri(UrlType.ADMIN).getBaseUri().toString();
services/src/main/java/org/keycloak/theme/DefaultThemeManager.java+1 −1 modified@@ -352,7 +352,7 @@ public Properties getProperties() throws IOException { */ private void substituteProperties(final Properties properties) { for (final String propertyName : properties.stringPropertyNames()) { - properties.setProperty(propertyName, StringPropertyReplacer.replaceProperties(properties.getProperty(propertyName), new SystemEnvProperties())); + properties.setProperty(propertyName, StringPropertyReplacer.replaceProperties(properties.getProperty(propertyName), SystemEnvProperties.UNFILTERED::getProperty)); } } }
services/src/test/java/org/keycloak/utils/JsonConfigProvider.java+6 −9 renamed@@ -15,27 +15,24 @@ * limitations under the License. */ -package org.keycloak.services.util; +package org.keycloak.utils; + +import java.util.Set; import com.fasterxml.jackson.databind.JsonNode; import org.keycloak.Config; import org.keycloak.common.util.StringPropertyReplacer; - -import java.util.Properties; -import java.util.Set; +import org.keycloak.common.util.SystemEnvProperties; /** * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> */ public class JsonConfigProvider implements Config.ConfigProvider { - private Properties properties; - private JsonNode config; - public JsonConfigProvider(JsonNode config, Properties properties) { + public JsonConfigProvider(JsonNode config) { this.config = config; - this.properties = properties; } @Override @@ -70,7 +67,7 @@ private static JsonNode getNode(JsonNode root, String... path) { } private String replaceProperties(String value) { - return StringPropertyReplacer.replaceProperties(value, properties); + return StringPropertyReplacer.replaceProperties(value, SystemEnvProperties.UNFILTERED::getProperty); } public class JsonScope implements Config.Scope {
services/src/test/java/org/keycloak/utils/ScopeUtil.java+2 −4 modified@@ -18,23 +18,21 @@ package org.keycloak.utils; import org.junit.Assert; -import org.keycloak.services.util.JsonConfigProvider; -import org.keycloak.services.util.JsonConfigProvider.JsonScope; import java.io.IOException; import java.util.Map; -import java.util.Properties; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import org.keycloak.utils.JsonConfigProvider.JsonScope; public class ScopeUtil { public static JsonScope createScope(Map<String, String> properties) { ObjectMapper mapper = new ObjectMapper(); try { JsonNode config = mapper.readTree(json(properties)); - return new JsonConfigProvider(config, new Properties()).new JsonScope(config); + return new JsonConfigProvider(config).new JsonScope(config); } catch (IOException e) { Assert.fail("Could not parse json"); }
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java+2 −1 modified@@ -42,6 +42,7 @@ import org.keycloak.admin.client.Keycloak; import org.keycloak.common.crypto.FipsMode; import org.keycloak.common.util.StringPropertyReplacer; +import org.keycloak.common.util.SystemEnvProperties; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.services.error.KeycloakErrorHandler; import org.keycloak.testsuite.ProfileAssume; @@ -436,7 +437,7 @@ private boolean handleManualMigration() { log.infof("Running SQL script created by liquibase during manual migration flow", sqlScriptPath); String prefix = "keycloak.connectionsJpa."; String jdbcDriver = System.getProperty(prefix + "driver"); - String dbUrl = StringPropertyReplacer.replaceProperties(System.getProperty(prefix + "url")); + String dbUrl = StringPropertyReplacer.replaceProperties(System.getProperty(prefix + "url"), SystemEnvProperties.UNFILTERED::getProperty); String dbUser = System.getProperty(prefix + "user"); String dbPassword = System.getProperty(prefix + "password");
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentTargetModifier.java+2 −1 modified@@ -29,6 +29,7 @@ import org.jboss.arquillian.test.spi.TestClass; import org.jboss.logging.Logger; import org.keycloak.common.util.StringPropertyReplacer; +import org.keycloak.common.util.SystemEnvProperties; import org.keycloak.testsuite.utils.arquillian.ContainerConstants; import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.getAppServerQualifiers; @@ -100,7 +101,7 @@ private void checkTestDeployments(List<DeploymentDescription> descriptions, Test String newAppServerQualifier = ContainerConstants.APP_SERVER_PREFIX + AppServerTestEnricher.CURRENT_APP_SERVER + "-" + suffix; updateServerQualifier(deployment, testClass, newAppServerQualifier); } else { - String newServerQualifier = StringPropertyReplacer.replaceProperties(containerQualifier); + String newServerQualifier = StringPropertyReplacer.replaceProperties(containerQualifier, SystemEnvProperties.UNFILTERED::getProperty); if (!newServerQualifier.equals(containerQualifier)) { updateServerQualifier(deployment, testClass, newServerQualifier); }
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlIdPInitiatedSsoTest.java+10 −9 modified@@ -39,6 +39,7 @@ import java.io.InputStream; import java.net.URI; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.List; import java.util.Map; @@ -97,8 +98,8 @@ private RealmRepresentation loadFromClasspath(String fileName, Properties proper InputStream is = KcSamlIdPInitiatedSsoTest.class.getResourceAsStream(fileName); try { String template = StreamUtil.readString(is, Charset.defaultCharset()); - String realmString = StringPropertyReplacer.replaceProperties(template, properties); - return IOUtil.loadRealm(new ByteArrayInputStream(realmString.getBytes("UTF-8"))); + String realmString = StringPropertyReplacer.replaceProperties(template, properties::getProperty); + return IOUtil.loadRealm(new ByteArrayInputStream(realmString.getBytes(StandardCharsets.UTF_8))); } catch (IOException ex) { throw new RuntimeException(ex); } @@ -139,7 +140,7 @@ public void addTestRealms(List<RealmRepresentation> testRealms) { p.put("url.realm.provider", urlRealmProvider); p.put("url.realm.consumer", urlRealmConsumer); p.put("url.realm.consumer-2", urlRealmConsumer2); - + testRealms.add(loadFromClasspath("kc3731-provider-realm.json", p)); testRealms.add(loadFromClasspath("kc3731-broker-realm.json", p)); } @@ -399,7 +400,7 @@ public void testProviderIdpInitiatedLoginWithPrincipalAttribute() throws Excepti assertThat(fed.getUserId(), is(PROVIDER_REALM_USER_NAME)); assertThat(fed.getUserName(), is(PROVIDER_REALM_USER_NAME)); } - + @Test public void testProviderTransientIdpInitiatedLogin() throws Exception { IdentityProviderResource idp = adminClient.realm(REALM_CONS_NAME).identityProviders().get("saml-leaf"); @@ -426,7 +427,7 @@ public void testProviderTransientIdpInitiatedLogin() throws Exception { nameId.setFormat(URI.create(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get())); nameId.setValue("subjectId1" ); resp.getAssertions().get(0).getAssertion().getSubject().getSubType().addBaseID(nameId); - + Set<StatementAbstractType> statements = resp.getAssertions().get(0).getAssertion().getStatements(); AttributeStatementType attributeType = (AttributeStatementType) statements.stream() @@ -448,7 +449,7 @@ public void testProviderTransientIdpInitiatedLogin() throws Exception { // Login in provider realm .login().sso(true).build() - + .processSamlResponse(Binding.POST) .transformObject(ob -> { assertThat(ob, Matchers.isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); @@ -460,7 +461,7 @@ public void testProviderTransientIdpInitiatedLogin() throws Exception { nameId.setFormat(URI.create(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get())); nameId.setValue("subjectId2" ); resp.getAssertions().get(0).getAssertion().getSubject().getSubType().addBaseID(nameId); - + Set<StatementAbstractType> statements = resp.getAssertions().get(0).getAssertion().getStatements(); AttributeStatementType attributeType = (AttributeStatementType) statements.stream() @@ -487,15 +488,15 @@ public void testProviderTransientIdpInitiatedLogin() throws Exception { ResponseType resp = (ResponseType) samlResponse.getSamlObject(); assertThat(resp.getDestination(), is(urlRealmConsumer + "/app/auth2/saml")); assertAudience(resp, urlRealmConsumer + "/app/auth2"); - + UsersResource users = adminClient.realm(REALM_CONS_NAME).users(); List<UserRepresentation> userList= users.search(CONSUMER_CHOSEN_USERNAME); assertEquals(1, userList.size()); String id = userList.get(0).getId(); FederatedIdentityRepresentation fed = users.get(id).getFederatedIdentity().get(0); assertThat(fed.getUserId(), is(PROVIDER_REALM_USER_NAME)); assertThat(fed.getUserName(), is(PROVIDER_REALM_USER_NAME)); - + //check that no user with sent subject-id was sent userList = users.search("subjectId1"); assertTrue(userList.isEmpty());
testsuite/model/src/main/java/org/keycloak/testsuite/model/Config.java+2 −4 modified@@ -21,9 +21,9 @@ import org.keycloak.Config.SystemPropertiesScope; import org.keycloak.common.util.StringPropertyReplacer; import org.keycloak.common.util.SystemEnvProperties; + import java.util.HashMap; import java.util.Map; -import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BooleanSupplier; import java.util.stream.Collectors; @@ -34,8 +34,6 @@ */ public class Config implements ConfigProvider { - private final Properties systemProperties = new SystemEnvProperties(); - private final Map<String, String> defaultProperties = new ConcurrentHashMap<>(); private final ThreadLocal<Map<String, String>> properties = new ThreadLocal<Map<String, String>>() { @Override @@ -157,7 +155,7 @@ public Map<String, String> getConfig() { } private String replaceProperties(String value) { - return StringPropertyReplacer.replaceProperties(value, systemProperties); + return StringPropertyReplacer.replaceProperties(value, SystemEnvProperties.UNFILTERED::getProperty); } @Override
testsuite/utils/src/main/java/org/keycloak/testsuite/JsonConfigProviderFactory.java+2 −9 modified@@ -23,13 +23,11 @@ import java.io.IOException; import java.net.URL; import java.util.Optional; -import java.util.Properties; import org.jboss.logging.Logger; import org.keycloak.Config; -import org.keycloak.common.util.SystemEnvProperties; import org.keycloak.services.ServicesLogger; -import org.keycloak.services.util.JsonConfigProvider; import org.keycloak.util.JsonSerialization; +import org.keycloak.utils.JsonConfigProvider; public class JsonConfigProviderFactory implements ConfigProviderFactory { @@ -66,11 +64,6 @@ public Optional<Config.ConfigProvider> create() { } protected Optional<Config.ConfigProvider> createJsonProvider(JsonNode node) { - return Optional.ofNullable(node).map(n -> new JsonConfigProvider(n, getProperties())); + return Optional.ofNullable(node).map(n -> new JsonConfigProvider(n)); } - - protected Properties getProperties() { - return new SystemEnvProperties(); - } - }
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-f4v7-3mww-9gc2ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-11736ghsaADVISORY
- access.redhat.com/errata/RHSA-2025:0299nvdWEB
- access.redhat.com/errata/RHSA-2025:0300nvdWEB
- access.redhat.com/security/cve/CVE-2024-11736nvdWEB
- bugzilla.redhat.com/show_bug.cginvdWEB
- github.com/keycloak/keycloak/commit/7a76858fe4aa39a39fb6b86dd3d2c113d9c59854ghsaWEB
- github.com/keycloak/keycloak/security/advisories/GHSA-f4v7-3mww-9gc2ghsaWEB
News mentions
0No linked articles in our index yet.