diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Sanitizer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Sanitizer.java index 386b916df9..6a72d25d8c 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Sanitizer.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/Sanitizer.java @@ -16,9 +16,11 @@ package org.springframework.boot.actuate.endpoint; +import java.util.regex.Matcher; import java.util.regex.Pattern; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Strategy that should be used by endpoint implementations to sanitize potentially @@ -29,16 +31,19 @@ import org.springframework.util.Assert; * @author Phillip Webb * @author Nicolas Lejeune * @author Stephane Nicoll + * @author HaiTao Zhang * @since 2.0.0 */ public class Sanitizer { private static final String[] REGEX_PARTS = { "*", "$", "^", "+" }; + private static final Pattern URI_USERINFO_PATTERN = Pattern.compile("[A-Za-z]+://.+:(.*)@.+$"); + private Pattern[] keysToSanitize; public Sanitizer() { - this("password", "secret", "key", "token", ".*credentials.*", "vcap_services", "sun.java.command"); + this("password", "secret", "key", "token", ".*credentials.*", "vcap_services", "sun.java.command", "uri"); } public Sanitizer(String... keysToSanitize) { @@ -86,10 +91,22 @@ public class Sanitizer { } for (Pattern pattern : this.keysToSanitize) { if (pattern.matcher(key).matches()) { + if (pattern.matcher("uri").matches()) { + return sanitizeUri(value); + } return "******"; } } return value; } + private Object sanitizeUri(Object value) { + Matcher matcher = URI_USERINFO_PATTERN.matcher(value.toString()); + String password = matcher.matches() ? matcher.group(1) : null; + if (password != null) { + return StringUtils.replace(value.toString(), ":" + password + "@", ":******@"); + } + return value; + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointTests.java index 292de006e0..cd9e316bf5 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.context.properties; +import java.net.URI; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -46,6 +47,7 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Dave Syer * @author Andy Wilkinson * @author Stephane Nicoll + * @author HaiTao Zhang */ class ConfigurationPropertiesReportEndpointTests { @@ -174,6 +176,22 @@ class ConfigurationPropertiesReportEndpointTests { }); } + @Test + void sanitizedUriWithSensitiveInfo() { + load((context, properties) -> { + Map nestedProperties = properties.getBeans().get("testProperties").getProperties(); + assertThat(nestedProperties.get("sensitiveUri")).isEqualTo("http://user:******@localhost:8080"); + }); + } + + @Test + void sanitizedUriWithNoPassword() { + load((context, properties) -> { + Map nestedProperties = properties.getBeans().get("testProperties").getProperties(); + assertThat(nestedProperties.get("noPasswordUri")).isEqualTo("http://user:******@localhost:8080"); + }); + } + @Test @SuppressWarnings("unchecked") void listsOfListsAreSanitized() { @@ -266,6 +284,10 @@ class ConfigurationPropertiesReportEndpointTests { private Duration duration = Duration.ofSeconds(10); + private URI sensitiveUri = URI.create("http://user:password@localhost:8080"); + + private URI noPasswordUri = URI.create("http://user:@localhost:8080"); + TestProperties() { this.secrets.put("mine", "myPrivateThing"); this.secrets.put("yours", "yourPrivateThing"); @@ -377,6 +399,22 @@ class ConfigurationPropertiesReportEndpointTests { this.duration = duration; } + public void setSensitiveUri(URI sensitiveUri) { + this.sensitiveUri = sensitiveUri; + } + + public URI getSensitiveUri() { + return this.sensitiveUri; + } + + public void setNoPasswordUri(URI noPasswordUri) { + this.noPasswordUri = noPasswordUri; + } + + public URI getNoPasswordUri() { + return this.noPasswordUri; + } + public static class Hidden { private String mine = "mySecret"; diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/SanitizerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/SanitizerTests.java index 1677cde539..deeab48b57 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/SanitizerTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/SanitizerTests.java @@ -40,6 +40,21 @@ class SanitizerTests { assertThat(sanitizer.sanitize("sometoken", "secret")).isEqualTo("******"); assertThat(sanitizer.sanitize("find", "secret")).isEqualTo("secret"); assertThat(sanitizer.sanitize("sun.java.command", "--spring.redis.password=pa55w0rd")).isEqualTo("******"); + assertThat(sanitizer.sanitize("my.uri", "http://user:password@localhost:8080")) + .isEqualTo("http://user:******@localhost:8080"); + } + + @Test + void uriWithNoPasswordShouldNotBeSanitized() { + Sanitizer sanitizer = new Sanitizer(); + assertThat(sanitizer.sanitize("my.uri", "http://localhost:8080")).isEqualTo("http://localhost:8080"); + } + + @Test + void uriWithPasswordMatchingOtherPartsOfString() { + Sanitizer sanitizer = new Sanitizer(); + assertThat(sanitizer.sanitize("my.uri", "http://user://@localhost:8080")) + .isEqualTo("http://user:******@localhost:8080"); } @Test diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointTests.java index 5b0c75e379..ecaa47813c 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointTests.java @@ -49,6 +49,7 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Stephane Nicoll * @author Madhura Bhave * @author Andy Wilkinson + * @author HaiTao Zhang */ class EnvironmentEndpointTests { @@ -244,6 +245,14 @@ class EnvironmentEndpointTests { assertThat(sources.get("two").getProperties().get("a").getValue()).isEqualTo("apple"); } + @Test + void uriPropertryWithSensitiveInfo() { + ConfigurableEnvironment environment = new StandardEnvironment(); + TestPropertyValues.of("sensitive.uri=http://user:password@localhost:8080").applyTo(environment); + EnvironmentEntryDescriptor descriptor = new EnvironmentEndpoint(environment).environmentEntry("sensitive.uri"); + assertThat(descriptor.getProperty().getValue()).isEqualTo("http://user:******@localhost:8080"); + } + private static ConfigurableEnvironment emptyEnvironment() { StandardEnvironment environment = new StandardEnvironment(); environment.getPropertySources().remove(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME);