diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyName.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyName.java index 7dc8cf2f93f5..ba1053debded 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyName.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyName.java @@ -672,6 +672,19 @@ private static Elements probablySingleElementOf(CharSequence name) { } throw new InvalidConfigurationPropertyNameException(name, Collections.singletonList('.')); } + for (int i = 0; i < name.length() - 1; i++) { + if (name.charAt(i) == '.' && name.charAt(i + 1) == '.') { + // A repeated separator produces an empty element between the two dots. + // ElementsParser.add silently drops empty elements, which would + // otherwise make this name indistinguishable from the same name with + // the repeated separator collapsed to one (e.g. "foo..bar" parsing as + // identical to "foo.bar"), silently merging two distinct names. + if (returnNullIfInvalid) { + return null; + } + throw new InvalidConfigurationPropertyNameException(name, Collections.singletonList('.')); + } + } if (name.charAt(name.length() - 1) == '[') { if (returnNullIfInvalid) { return null; diff --git a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameTests.java b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameTests.java index b102541b991d..fa730479ab7a 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameTests.java @@ -54,6 +54,22 @@ void ofNameShouldNotEndWithDot() { .withMessageContaining("is not valid"); } + @Test + void ofNameShouldNotContainDoubleDot() { + // A repeated separator produces an empty element, which ElementsParser.add + // silently drops. Without this check, "foo..bar" parses as identical to + // "foo.bar" (of("foo..bar").equals(of("foo.bar")) is true and + // isValid("foo..bar") is true), so two distinct property-source keys that + // differ only by a repeated separator silently collide on the same + // ConfigurationPropertyName. + assertThatExceptionOfType(InvalidConfigurationPropertyNameException.class) + .isThrownBy(() -> ConfigurationPropertyName.of("foo..bar")) + .withMessageContaining("is not valid"); + assertThatExceptionOfType(InvalidConfigurationPropertyNameException.class) + .isThrownBy(() -> ConfigurationPropertyName.of("foo...bar")) + .withMessageContaining("is not valid"); + } + @Test void ofNameShouldNotContainUppercase() { assertThatExceptionOfType(InvalidConfigurationPropertyNameException.class) @@ -757,6 +773,8 @@ void isValidWhenNotValidShouldReturnFalse() { assertThat(ConfigurationPropertyName.isValid("-foo")).isFalse(); assertThat(ConfigurationPropertyName.isValid("FooBar")).isFalse(); assertThat(ConfigurationPropertyName.isValid("foo!bar")).isFalse(); + assertThat(ConfigurationPropertyName.isValid("foo..bar")).isFalse(); + assertThat(ConfigurationPropertyName.isValid("foo...bar")).isFalse(); assertThat(ConfigurationPropertyName.isValid("foo[")).isFalse(); }