Skip to content

Commit 7a76858

Browse files
pedroigorabstractj
authored andcommitted
Restrict access to environment variables when at the server runtime
Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
1 parent edbf75e commit 7a76858

File tree

26 files changed

+269
-159
lines changed

26 files changed

+269
-159
lines changed

adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/KeyParser.java

+8-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import org.keycloak.adapters.saml.config.Key;
2121
import org.keycloak.common.util.StringPropertyReplacer;
22+
import org.keycloak.common.util.SystemEnvProperties;
2223
import org.keycloak.saml.common.exceptions.ParsingException;
2324
import org.keycloak.saml.common.util.StaxParserUtil;
2425

@@ -60,20 +61,24 @@ protected void processSubElement(XMLEventReader xmlEventReader, Key target, Keyc
6061
case CERTIFICATE_PEM:
6162
StaxParserUtil.advance(xmlEventReader);
6263
value = StaxParserUtil.getElementText(xmlEventReader);
63-
target.setCertificatePem(StringPropertyReplacer.replaceProperties(value));
64+
target.setCertificatePem(replaceProperties(value));
6465
break;
6566

6667
case PUBLIC_KEY_PEM:
6768
StaxParserUtil.advance(xmlEventReader);
6869
value = StaxParserUtil.getElementText(xmlEventReader);
69-
target.setPublicKeyPem(StringPropertyReplacer.replaceProperties(value));
70+
target.setPublicKeyPem(replaceProperties(value));
7071
break;
7172

7273
case PRIVATE_KEY_PEM:
7374
StaxParserUtil.advance(xmlEventReader);
7475
value = StaxParserUtil.getElementText(xmlEventReader);
75-
target.setPrivateKeyPem(StringPropertyReplacer.replaceProperties(value));
76+
target.setPrivateKeyPem(replaceProperties(value));
7677
break;
7778
}
7879
}
80+
81+
private String replaceProperties(String value) {
82+
return StringPropertyReplacer.replaceProperties(value, SystemEnvProperties.UNFILTERED::getProperty);
83+
}
7984
}

authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java

-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
import java.io.IOException;
2323
import java.io.InputStream;
24-
import java.util.Objects;
2524

2625
import com.fasterxml.jackson.annotation.JsonInclude;
2726
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -33,7 +32,6 @@
3332
import org.keycloak.common.crypto.CryptoIntegration;
3433
import org.keycloak.common.util.KeycloakUriBuilder;
3534
import org.keycloak.representations.AccessTokenResponse;
36-
import org.keycloak.util.SystemPropertiesJsonParserFactory;
3735

3836
/**
3937
* <p>This is class serves as an entry point for clients looking for access to Keycloak Authorization Services.

core/src/main/java/org/keycloak/util/SystemPropertiesJsonParserFactory.java authz/client/src/main/java/org/keycloak/authorization/client/SystemPropertiesJsonParserFactory.java

+7-10
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
* limitations under the License.
1616
*/
1717

18-
package org.keycloak.util;
18+
package org.keycloak.authorization.client;
19+
20+
import java.io.IOException;
21+
import java.io.InputStream;
22+
import java.io.Reader;
1923

2024
import com.fasterxml.jackson.core.JsonParser;
2125
import com.fasterxml.jackson.core.io.IOContext;
@@ -24,19 +28,12 @@
2428
import org.keycloak.common.util.StringPropertyReplacer;
2529
import org.keycloak.common.util.SystemEnvProperties;
2630

27-
import java.io.IOException;
28-
import java.io.InputStream;
29-
import java.io.Reader;
30-
import java.util.Properties;
31-
3231
/**
3332
* Provides replacing of system properties for parsed values
3433
*
3534
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
3635
*/
37-
public class SystemPropertiesJsonParserFactory extends MappingJsonFactory {
38-
39-
private static final Properties properties = new SystemEnvProperties();
36+
class SystemPropertiesJsonParserFactory extends MappingJsonFactory {
4037

4138
@Override
4239
protected JsonParser _createParser(InputStream in, IOContext ctxt) throws IOException {
@@ -71,7 +68,7 @@ public SystemPropertiesAwareJsonParser(JsonParser d) {
7168
@Override
7269
public String getText() throws IOException {
7370
String orig = super.getText();
74-
return StringPropertyReplacer.replaceProperties(orig, properties);
71+
return StringPropertyReplacer.replaceProperties(orig, SystemEnvProperties.UNFILTERED::getProperty);
7572
}
7673
}
7774
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2024 Red Hat, Inc. and/or its affiliates
3+
* and other contributors as indicated by the @author tags.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.keycloak.authorization.client;
19+
20+
import java.io.IOException;
21+
import java.io.InputStream;
22+
23+
import com.fasterxml.jackson.databind.ObjectMapper;
24+
import org.junit.Assert;
25+
import org.junit.Test;
26+
import org.keycloak.representations.adapters.config.AdapterConfig;
27+
28+
/**
29+
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
30+
*/
31+
public class JsonParserTest {
32+
33+
@Test
34+
public void testParsingSystemProps() throws IOException {
35+
System.setProperty("my.host", "foo");
36+
System.setProperty("con.pool.size", "200");
37+
System.setProperty("allow.any.hostname", "true");
38+
System.setProperty("socket.timeout.millis", "6000");
39+
System.setProperty("connection.timeout.millis", "7000");
40+
System.setProperty("connection.ttl.millis", "500");
41+
42+
InputStream is = getClass().getClassLoader().getResourceAsStream("keycloak.json");
43+
44+
ObjectMapper mapper = new ObjectMapper(new SystemPropertiesJsonParserFactory());
45+
AdapterConfig config = mapper.readValue(is, AdapterConfig.class);
46+
Assert.assertEquals("http://foo:8080/auth", config.getAuthServerUrl());
47+
Assert.assertEquals("external", config.getSslRequired());
48+
Assert.assertEquals("angular-product${non.existing}", config.getResource());
49+
Assert.assertTrue(config.isPublicClient());
50+
Assert.assertTrue(config.isAllowAnyHostname());
51+
Assert.assertEquals(100, config.getCorsMaxAge());
52+
Assert.assertEquals(200, config.getConnectionPoolSize());
53+
Assert.assertEquals(6000L, config.getSocketTimeout());
54+
Assert.assertEquals(7000L, config.getConnectionTimeout());
55+
Assert.assertEquals(500L, config.getConnectionTTL());
56+
}
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"auth-server-url" : "http://${my.host}:8080/auth",
3+
"ssl-required" : "external",
4+
"resource" : "angular-product${non.existing}",
5+
"public-client" : true,
6+
"allow-any-hostname": "${allow.any.hostname}",
7+
"cors-max-age": 100,
8+
"connection-pool-size": "${con.pool.size}",
9+
"socket-timeout-millis": "${socket.timeout.millis}",
10+
"connection-timeout-millis": "${connection.timeout.millis}",
11+
"connection-ttl-millis": "${connection.ttl.millis}"
12+
}

common/src/main/java/org/keycloak/common/util/StringPropertyReplacer.java

+31-36
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,20 @@
1717
package org.keycloak.common.util;
1818

1919
import java.io.File;
20-
import java.util.Properties;
20+
import java.util.Optional;
2121

2222
/**
23-
* A utility class for replacing properties in strings.
23+
* A utility class for replacing properties in strings.
2424
*
2525
* @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
2626
* @author <a href="Scott.Stark@jboss.org">Scott Stark</a>
2727
* @author <a href="claudio.vesco@previnet.it">Claudio Vesco</a>
2828
* @author <a href="mailto:adrian@jboss.com">Adrian Brock</a>
2929
* @author <a href="mailto:dimitris@jboss.org">Dimitris Andreadis</a>
30-
* @version <tt>$Revision: 2898 $</tt>
30+
* @version <tt>$Revision: 2898 $</tt>
3131
*/
3232
public final class StringPropertyReplacer
3333
{
34-
/** New line string constant */
35-
public static final String NEWLINE = System.getProperty("line.separator", "\n");
3634

3735
/** File separator value */
3836
private static final String FILE_SEPARATOR = File.separator;
@@ -51,7 +49,12 @@ public final class StringPropertyReplacer
5149
private static final int SEEN_DOLLAR = 1;
5250
private static final int IN_BRACKET = 2;
5351

54-
private static final Properties systemEnvProperties = new SystemEnvProperties();
52+
private static final PropertyResolver NULL_RESOLVER = property -> null;
53+
private static PropertyResolver DEFAULT_PROPERTY_RESOLVER;
54+
55+
public static void setDefaultPropertyResolver(PropertyResolver systemVariables) {
56+
DEFAULT_PROPERTY_RESOLVER = systemVariables;
57+
}
5558

5659
/**
5760
* Go through the input string and replace any occurrence of ${p} with
@@ -72,14 +75,13 @@ public final class StringPropertyReplacer
7275
* @return the input string with all property references replaced if any.
7376
* If there are no valid references the input string will be returned.
7477
*/
75-
public static String replaceProperties(final String string)
76-
{
77-
return replaceProperties(string, (Properties) null);
78+
public static String replaceProperties(final String string) {
79+
return replaceProperties(string, getDefaultPropertyResolver());
7880
}
7981

8082
/**
8183
* Go through the input string and replace any occurrence of ${p} with
82-
* the props.getProperty(p) value. If there is no such property p defined,
84+
* the value resolves from {@code resolver}. If there is no such property p defined,
8385
* then the ${p} reference will remain unchanged.
8486
*
8587
* 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)
9395
* value and the property ${:} is replaced with System.getProperty("path.separator").
9496
*
9597
* @param string - the string with possible ${} references
96-
* @param props - the source for ${x} property ref values, null means use System.getProperty()
98+
* @param resolver - the property resolver
9799
* @return the input string with all property references replaced if any.
98100
* If there are no valid references the input string will be returned.
99101
*/
100-
public static String replaceProperties(final String string, final Properties props) {
101-
if (props == null) {
102-
return replaceProperties(string, (PropertyResolver) null);
103-
}
104-
return replaceProperties(string, props::getProperty);
105-
}
106-
107102
public static String replaceProperties(final String string, PropertyResolver resolver)
108103
{
109104
if(string == null) {
@@ -171,10 +166,7 @@ else if (PATH_SEPARATOR_ALIAS.equals(key))
171166
else
172167
{
173168
// check from the properties
174-
if (resolver != null)
175-
value = resolver.resolve(key);
176-
else
177-
value = systemEnvProperties.getProperty(key);
169+
value = resolveValue(resolver, key);
178170

179171
if (value == null)
180172
{
@@ -183,10 +175,7 @@ else if (PATH_SEPARATOR_ALIAS.equals(key))
183175
if (colon > 0)
184176
{
185177
String realKey = key.substring(0, colon);
186-
if (resolver != null)
187-
value = resolver.resolve(realKey);
188-
else
189-
value = systemEnvProperties.getProperty(realKey);
178+
value = resolveValue(resolver, realKey);
190179

191180
if (value == null)
192181
{
@@ -239,7 +228,7 @@ else if (PATH_SEPARATOR_ALIAS.equals(key))
239228
throw new IllegalStateException("Infinite recursion happening when replacing properties on '" + buffer + "'");
240229
}
241230
}
242-
231+
243232
// Done
244233
return buffer.toString();
245234
}
@@ -257,26 +246,32 @@ private static String resolveCompositeKey(String key, PropertyResolver resolver)
257246
{
258247
// Check the first part
259248
String key1 = key.substring(0, comma);
260-
if (resolver != null)
261-
value = resolver.resolve(key1);
262-
else
263-
value = systemEnvProperties.getProperty(key1);
249+
value = resolveValue(resolver, key1);
264250
}
265251
// Check the second part, if there is one and first lookup failed
266252
if (value == null && comma < key.length() - 1)
267253
{
268254
String key2 = key.substring(comma + 1);
269-
if (resolver != null)
270-
value = resolver.resolve(key2);
271-
else
272-
value = systemEnvProperties.getProperty(key2);
255+
value = resolveValue(resolver, key2);
273256
}
274257
}
275258
// Return whatever we've found or null
276259
return value;
277260
}
278-
261+
279262
public interface PropertyResolver {
280263
String resolve(String property);
281264
}
265+
266+
private static String resolveValue(PropertyResolver resolver, String key) {
267+
if (resolver == null) {
268+
return getDefaultPropertyResolver().resolve(key);
269+
}
270+
271+
return resolver.resolve(key);
272+
}
273+
274+
private static PropertyResolver getDefaultPropertyResolver() {
275+
return Optional.ofNullable(DEFAULT_PROPERTY_RESOLVER).orElse(NULL_RESOLVER);
276+
}
282277
}

common/src/main/java/org/keycloak/common/util/SystemEnvProperties.java

+39-2
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,53 @@
1717

1818
package org.keycloak.common.util;
1919

20+
import java.util.Collections;
21+
import java.util.Optional;
2022
import java.util.Properties;
23+
import java.util.Set;
2124

2225
/**
26+
* <p>An utility class to resolve the value of a key based on the environment variables
27+
* 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
28+
* can be used when resolving placeholders.
29+
*
30+
* <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.
31+
* For system properties, there is no specific format and the value is resolved from a system property that matches the key.
32+
*
2333
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
2434
*/
2535
public class SystemEnvProperties extends Properties {
2636

37+
/**
38+
* <p>An variation of {@link SystemEnvProperties} that gives unrestricted access to any system variable available at runtime.
39+
* Most of the time you don't want to use this class but favor creating a {@link SystemEnvProperties} instance that
40+
* filters which system variables should be available at runtime.
41+
*/
42+
public static final SystemEnvProperties UNFILTERED = new SystemEnvProperties(Collections.emptySet()) {
43+
@Override
44+
protected boolean isAllowed(String key) {
45+
return true;
46+
}
47+
};
48+
49+
private final Set<String> allowedSystemVariables;
50+
51+
/**
52+
* Creates a new instance where system variables where only specific keys can be resolved from system variables.
53+
*
54+
* @param allowedSystemVariables the keys of system variables that should be available at runtime
55+
*/
56+
public SystemEnvProperties(Set<String> allowedSystemVariables) {
57+
this.allowedSystemVariables = Optional.ofNullable(allowedSystemVariables).orElse(Collections.emptySet());
58+
}
59+
2760
@Override
2861
public String getProperty(String key) {
2962
if (key.startsWith("env.")) {
30-
return System.getenv().get(key.substring(4));
63+
String envKey = key.substring(4);
64+
return isAllowed(envKey) ? System.getenv().get(envKey) : null;
3165
} else {
32-
return System.getProperty(key);
66+
return isAllowed(key) ? System.getProperty(key) : null;
3367
}
3468
}
3569

@@ -39,4 +73,7 @@ public String getProperty(String key, String defaultValue) {
3973
return value != null ? value : defaultValue;
4074
}
4175

76+
protected boolean isAllowed(String key) {
77+
return allowedSystemVariables.contains(key);
78+
}
4279
}

0 commit comments

Comments
 (0)