Skip to content

Commit 7da70a5

Browse files
committed
Mask sensitive placeholders in env endpoint
Closes gh-8282
1 parent 703b7d9 commit 7da70a5

File tree

4 files changed

+126
-18
lines changed

4 files changed

+126
-18
lines changed

spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/EnvironmentEndpoint.java

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
import org.springframework.core.env.EnumerablePropertySource;
2727
import org.springframework.core.env.Environment;
2828
import org.springframework.core.env.MutablePropertySources;
29+
import org.springframework.core.env.PropertyResolver;
2930
import org.springframework.core.env.PropertySource;
31+
import org.springframework.core.env.PropertySources;
32+
import org.springframework.core.env.PropertySourcesPropertyResolver;
3033
import org.springframework.core.env.StandardEnvironment;
3134

3235
/**
@@ -35,6 +38,7 @@
3538
* @author Dave Syer
3639
* @author Phillip Webb
3740
* @author Christian Dupuis
41+
* @author Madhura Bhave
3842
*/
3943
@ConfigurationProperties(prefix = "endpoints.env")
4044
public class EnvironmentEndpoint extends AbstractEndpoint<Map<String, Object>> {
@@ -56,14 +60,15 @@ public void setKeysToSanitize(String... keysToSanitize) {
5660
public Map<String, Object> invoke() {
5761
Map<String, Object> result = new LinkedHashMap<String, Object>();
5862
result.put("profiles", getEnvironment().getActiveProfiles());
59-
for (Entry<String, PropertySource<?>> entry : getPropertySources().entrySet()) {
63+
PropertyResolver resolver = getResolver();
64+
for (Entry<String, PropertySource<?>> entry : getPropertySourcesAsMap().entrySet()) {
6065
PropertySource<?> source = entry.getValue();
6166
String sourceName = entry.getKey();
6267
if (source instanceof EnumerablePropertySource) {
6368
EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) source;
6469
Map<String, Object> properties = new LinkedHashMap<String, Object>();
6570
for (String name : enumerable.getPropertyNames()) {
66-
properties.put(name, sanitize(name, enumerable.getProperty(name)));
71+
properties.put(name, sanitize(name, resolver.getProperty(name)));
6772
}
6873
properties = postProcessSourceProperties(sourceName, properties);
6974
if (properties != null) {
@@ -74,20 +79,32 @@ public Map<String, Object> invoke() {
7479
return result;
7580
}
7681

77-
private Map<String, PropertySource<?>> getPropertySources() {
82+
public PropertyResolver getResolver() {
83+
PlaceholderSanitizingPropertyResolver resolver = new PlaceholderSanitizingPropertyResolver(
84+
getPropertySources(), this.sanitizer);
85+
resolver.setIgnoreUnresolvableNestedPlaceholders(true);
86+
return resolver;
87+
}
88+
89+
private Map<String, PropertySource<?>> getPropertySourcesAsMap() {
7890
Map<String, PropertySource<?>> map = new LinkedHashMap<String, PropertySource<?>>();
79-
MutablePropertySources sources = null;
91+
MutablePropertySources sources = getPropertySources();
92+
for (PropertySource<?> source : sources) {
93+
extract("", map, source);
94+
}
95+
return map;
96+
}
97+
98+
private MutablePropertySources getPropertySources() {
99+
MutablePropertySources sources;
80100
Environment environment = getEnvironment();
81101
if (environment != null && environment instanceof ConfigurableEnvironment) {
82102
sources = ((ConfigurableEnvironment) environment).getPropertySources();
83103
}
84104
else {
85105
sources = new StandardEnvironment().getPropertySources();
86106
}
87-
for (PropertySource<?> source : sources) {
88-
extract("", map, source);
89-
}
90-
return map;
107+
return sources;
91108
}
92109

93110
private void extract(String root, Map<String, PropertySource<?>> map,
@@ -120,4 +137,32 @@ protected Map<String, Object> postProcessSourceProperties(String sourceName,
120137
return properties;
121138
}
122139

140+
/**
141+
* {@link PropertySourcesPropertyResolver} that sanitizes sensitive placeholders
142+
* if present.
143+
*
144+
* @author Madhura Bhave
145+
*/
146+
private class PlaceholderSanitizingPropertyResolver extends PropertySourcesPropertyResolver {
147+
148+
private final Sanitizer sanitizer;
149+
150+
/**
151+
* Create a new resolver against the given property sources.
152+
* @param propertySources the set of {@link PropertySource} objects to use
153+
* @param sanitizer the sanitizer used to sanitize sensitive values
154+
*/
155+
PlaceholderSanitizingPropertyResolver(PropertySources
156+
propertySources, Sanitizer sanitizer) {
157+
super(propertySources);
158+
this.sanitizer = sanitizer;
159+
}
160+
161+
@Override
162+
protected String getPropertyAsRawString(String key) {
163+
String value = super.getPropertyAsRawString(key);
164+
return (String) this.sanitizer.sanitize(key, value);
165+
}
166+
}
167+
123168
}

spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpoint.java

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,8 @@
2222
import org.springframework.core.env.ConfigurableEnvironment;
2323
import org.springframework.core.env.EnumerablePropertySource;
2424
import org.springframework.core.env.Environment;
25-
import org.springframework.core.env.PropertyResolver;
2625
import org.springframework.core.env.PropertySource;
2726
import org.springframework.core.env.PropertySources;
28-
import org.springframework.core.env.PropertySourcesPropertyResolver;
2927
import org.springframework.http.HttpStatus;
3028
import org.springframework.web.bind.annotation.PathVariable;
3129
import org.springframework.web.bind.annotation.ResponseBody;
@@ -95,14 +93,7 @@ private void getNames(PropertySources propertySources, NameCallback callback) {
9593

9694
@Override
9795
protected Object getOptionalValue(Environment source, String name) {
98-
PropertyResolver resolver = source;
99-
if (source instanceof ConfigurableEnvironment) {
100-
resolver = new PropertySourcesPropertyResolver(
101-
((ConfigurableEnvironment) source).getPropertySources());
102-
((PropertySourcesPropertyResolver) resolver)
103-
.setIgnoreUnresolvableNestedPlaceholders(true);
104-
}
105-
Object result = resolver.getProperty(name);
96+
Object result = ((EnvironmentEndpoint) getDelegate()).getResolver().getProperty(name);
10697
if (result != null) {
10798
result = ((EnvironmentEndpoint) getDelegate()).sanitize(name, result);
10899
}

spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/EnvironmentEndpointTests.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
* @author Christian Dupuis
4040
* @author Nicolas Lejeune
4141
* @author Stephane Nicoll
42+
* @author Madhura Bhave
4243
*/
4344
public class EnvironmentEndpointTests extends AbstractEndpointTests<EnvironmentEndpoint> {
4445

@@ -194,6 +195,66 @@ public void testKeySanitizationWithCustomPatternAndKeyByEnvironment()
194195
assertThat(systemProperties.get("apiKey")).isEqualTo("******");
195196
}
196197

198+
@SuppressWarnings("unchecked")
199+
@Test
200+
public void propertyWithPlaceholderResolved() throws Exception {
201+
this.context = new AnnotationConfigApplicationContext();
202+
EnvironmentTestUtils.addEnvironment(this.context,
203+
"my.foo: ${bar.blah}", "bar.blah: hello");
204+
this.context.register(Config.class);
205+
this.context.refresh();
206+
EnvironmentEndpoint report = getEndpointBean();
207+
Map<String, Object> env = report.invoke();
208+
Map<String, Object> testProperties = (Map<String, Object>) env
209+
.get("test");
210+
assertThat(testProperties.get("my.foo")).isEqualTo("hello");
211+
}
212+
213+
@SuppressWarnings("unchecked")
214+
@Test
215+
public void propertyWithPlaceholderNotResolved() throws Exception {
216+
this.context = new AnnotationConfigApplicationContext();
217+
EnvironmentTestUtils.addEnvironment(this.context,
218+
"my.foo: ${bar.blah}");
219+
this.context.register(Config.class);
220+
this.context.refresh();
221+
EnvironmentEndpoint report = getEndpointBean();
222+
Map<String, Object> env = report.invoke();
223+
Map<String, Object> testProperties = (Map<String, Object>) env
224+
.get("test");
225+
assertThat(testProperties.get("my.foo")).isEqualTo("${bar.blah}");
226+
}
227+
228+
@SuppressWarnings("unchecked")
229+
@Test
230+
public void propertyWithSensitivePlaceholderResolved() throws Exception {
231+
this.context = new AnnotationConfigApplicationContext();
232+
EnvironmentTestUtils.addEnvironment(this.context,
233+
"my.foo: http://${bar.password}://hello", "bar.password: hello");
234+
this.context.register(Config.class);
235+
this.context.refresh();
236+
EnvironmentEndpoint report = getEndpointBean();
237+
Map<String, Object> env = report.invoke();
238+
Map<String, Object> testProperties = (Map<String, Object>) env
239+
.get("test");
240+
assertThat(testProperties.get("my.foo")).isEqualTo("http://******://hello");
241+
}
242+
243+
@SuppressWarnings("unchecked")
244+
@Test
245+
public void propertyWithSensitivePlaceholderNotResolved() throws Exception {
246+
this.context = new AnnotationConfigApplicationContext();
247+
EnvironmentTestUtils.addEnvironment(this.context,
248+
"my.foo: http://${bar.password}://hello");
249+
this.context.register(Config.class);
250+
this.context.refresh();
251+
EnvironmentEndpoint report = getEndpointBean();
252+
Map<String, Object> env = report.invoke();
253+
Map<String, Object> testProperties = (Map<String, Object>) env
254+
.get("test");
255+
assertThat(testProperties.get("my.foo")).isEqualTo("http://${bar.password}://hello");
256+
}
257+
197258
@Configuration
198259
@EnableConfigurationProperties
199260
public static class Config {

spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpointTests.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,17 @@ public void nestedPathWhenPlaceholderCannotBeResolvedShouldReturnUnresolvedPrope
148148
.andExpect(content().string(containsString("\"my.foo\":\"${my.bar}\"")));
149149
}
150150

151+
@Test
152+
public void nestedPathWithSensitivePlaceholderShouldSanitize() throws Exception {
153+
Map<String, Object> map = new HashMap<String, Object>();
154+
map.put("my.foo", "${my.password}");
155+
map.put("my.password", "hello");
156+
((ConfigurableEnvironment) this.context.getEnvironment()).getPropertySources()
157+
.addFirst(new MapPropertySource("placeholder", map));
158+
this.mvc.perform(get("/env/my.*")).andExpect(status().isOk())
159+
.andExpect(content().string(containsString("\"my.foo\":\"******\"")));
160+
}
161+
151162
@Configuration
152163
@Import({ JacksonAutoConfiguration.class,
153164
HttpMessageConvertersAutoConfiguration.class, WebMvcAutoConfiguration.class,

0 commit comments

Comments
 (0)