VYPR
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.

PackageAffected versionsPatched versions
org.keycloak:keycloak-quarkus-serverMaven
< 26.0.826.0.8

Patches

1
7a76858fe4aa

Restrict access to environment variables when at the server runtime

https://github.com/keycloak/keycloakPedro IgorDec 11, 2024via ghsa
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

News mentions

0

No linked articles in our index yet.