diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java index f436bc2e96e3..feee146a02d7 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java @@ -908,16 +908,20 @@ public PathComponent build() { } private static String getSanitizedPath(final StringBuilder path) { - int index = path.indexOf("//"); - if (index >= 0) { - StringBuilder sanitized = new StringBuilder(path); - while (index != -1) { - sanitized.deleteCharAt(index); - index = sanitized.indexOf("//", index); - } - return sanitized.toString(); + String pathString = path.toString(); + int protocolIndex = pathString.indexOf("://"); + boolean hasProtocol = protocolIndex != -1; + + if (hasProtocol) { + String protocol = path.substring(0, protocolIndex + 3); + String restOfPath = path.substring(protocolIndex + 3); + + String sanitizedRestPath = restOfPath.replaceAll("/{2,}", "/"); + + return protocol + sanitizedRestPath; } - return path.toString(); + + return pathString.replaceAll("/{2,}", "/"); } public void removeTrailingSlash() { diff --git a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java index ce22e8e4c885..0fb56b0c4359 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java @@ -16,6 +16,7 @@ package org.springframework.web.util; +import java.lang.reflect.Method; import java.net.URI; import java.util.Arrays; import java.util.Collection; @@ -25,10 +26,13 @@ import java.util.Map; import java.util.Optional; import java.util.function.BiConsumer; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -890,4 +894,37 @@ void expandPortAndPathWithoutSeparator(ParserType parserType) { assertThat(uri.toString()).isEqualTo("ws://localhost:7777/test"); } + @ParameterizedTest + @MethodSource("providePathsForSanitization") + void verifySanitizedPath(StringBuilder inputPath, String expected) { + assertThat(invokeGetSanitizedPath(inputPath)).isEqualTo(expected); + } + + private static Stream providePathsForSanitization() { + return Stream.of( + Arguments.of(new StringBuilder("https://example.com//path/to//resource"), "https://example.com/path/to/resource"), + Arguments.of(new StringBuilder("https://example.com//path"), "https://example.com/path"), + Arguments.of(new StringBuilder("example.com//path"), "example.com/path"), + Arguments.of(new StringBuilder("example.com//////////////path//to"), "example.com/path/to") + ); + } + + private String invokeGetSanitizedPath(StringBuilder path) { + try { + Class outerClass = Class.forName("org.springframework.web.util.UriComponentsBuilder"); + + Class innerClass = Arrays.stream(outerClass.getDeclaredClasses()) + .filter(clazz -> clazz.getSimpleName().equals("FullPathComponentBuilder")) + .findFirst() + .orElseThrow(() -> new ClassNotFoundException("FullPathComponentBuilder not found")); + + Method method = innerClass.getDeclaredMethod("getSanitizedPath", StringBuilder.class); + method.setAccessible(true); + return (String) method.invoke(null, path); + } + catch (Exception e) { + throw new RuntimeException("Failed to invoke getSanitizedPath", e); + } + } + }