From 12bf5e79ae856bdd1f3ae021b7f9eaca9f739c2d Mon Sep 17 00:00:00 2001 From: vishnugt Date: Sat, 11 Jan 2025 14:04:21 +0530 Subject: [PATCH] Make multiple slash sanitization optional in web util Closes gh-34076 Signed-off-by: vishnugt --- .../web/util/UriComponentsBuilder.java | 42 ++++++++++++++----- .../web/util/UrlPathHelper.java | 26 +++++++++++- .../web/util/UriComponentsBuilderTests.java | 14 ++++++- .../web/util/UrlPathHelperTests.java | 14 +++++++ 4 files changed, 82 insertions(+), 14 deletions(-) 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 c1d9930043ac..7236e2a36aaa 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 @@ -284,7 +284,7 @@ public UriComponentsBuilder encode(Charset charset) { * @return the URI components */ public UriComponents build() { - return build(false); + return build(false, true); } /** @@ -297,11 +297,31 @@ public UriComponents build() { * characters that should have been encoded. */ public UriComponents build(boolean encoded) { - return buildInternal(encoded ? EncodingHint.FULLY_ENCODED : - (this.encodeTemplate ? EncodingHint.ENCODE_TEMPLATE : EncodingHint.NONE)); + return build(encoded, true); + } + + /** + * Variant of {@link #build()} to create a {@link UriComponents} instance + * when components are already fully encoded. In addition, this method allows + * to control whether the double slashes in the path should be replaced with + * a single slash. + * @param encoded whether the components in this builder are already encoded + * @param sanitizePath whether the double slashes should be replaced with single slash + * @return the URI components + * @throws IllegalArgumentException if any of the components contain illegal + * characters that should have been encoded. + */ + public UriComponents build(boolean encoded, boolean sanitizePath) { + EncodingHint hint = encoded ? EncodingHint.FULLY_ENCODED : + (this.encodeTemplate ? EncodingHint.ENCODE_TEMPLATE : EncodingHint.NONE); + return buildInternal(hint, sanitizePath); } private UriComponents buildInternal(EncodingHint hint) { + return buildInternal(hint, true); + } + + private UriComponents buildInternal(EncodingHint hint, boolean sanitizePath) { UriComponents result; if (this.ssp != null) { result = new OpaqueUriComponents(this.scheme, this.ssp, this.fragment); @@ -309,7 +329,7 @@ private UriComponents buildInternal(EncodingHint hint) { else { MultiValueMap queryParams = new LinkedMultiValueMap<>(this.queryParams); HierarchicalUriComponents uric = new HierarchicalUriComponents(this.scheme, this.fragment, - this.userInfo, this.host, this.port, this.pathBuilder.build(), queryParams, + this.userInfo, this.host, this.port, this.pathBuilder.build(sanitizePath), queryParams, hint == EncodingHint.FULLY_ENCODED); result = (hint == EncodingHint.ENCODE_TEMPLATE ? uric.encodeTemplate(this.charset) : uric); } @@ -771,7 +791,7 @@ public enum ParserType { private interface PathComponentBuilder { - @Nullable PathComponent build(); + @Nullable PathComponent build(boolean sanitizePath); PathComponentBuilder cloneBuilder(); } @@ -823,11 +843,11 @@ public void addPath(String path) { } @Override - public PathComponent build() { + public PathComponent build(boolean sanitizePath) { int size = this.builders.size(); List components = new ArrayList<>(size); for (PathComponentBuilder componentBuilder : this.builders) { - PathComponent pathComponent = componentBuilder.build(); + PathComponent pathComponent = componentBuilder.build(sanitizePath); if (pathComponent != null) { components.add(pathComponent); } @@ -861,12 +881,12 @@ public void append(String path) { } @Override - public @Nullable PathComponent build() { + public @Nullable PathComponent build(boolean sanitizePath) { if (this.path.isEmpty()) { return null; } - String sanitized = getSanitizedPath(this.path); - return new HierarchicalUriComponents.FullPathComponent(sanitized); + String path = sanitizePath ? getSanitizedPath(this.path) : this.path.toString(); + return new HierarchicalUriComponents.FullPathComponent(path); } private static String getSanitizedPath(final StringBuilder path) { @@ -911,7 +931,7 @@ public void append(String... pathSegments) { } @Override - public @Nullable PathComponent build() { + public @Nullable PathComponent build(boolean sanitizePath) { return (this.pathSegments.isEmpty() ? null : new HierarchicalUriComponents.PathSegmentComponent(this.pathSegments)); } diff --git a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java index a7035825155e..27f3561d6c97 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java +++ b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java @@ -79,6 +79,8 @@ public class UrlPathHelper { private boolean removeSemicolonContent = true; + private boolean sanitizePath = true; + private String defaultEncoding = WebUtils.DEFAULT_CHARACTER_ENCODING; private boolean readOnly = false; @@ -145,6 +147,22 @@ public boolean shouldRemoveSemicolonContent() { return this.removeSemicolonContent; } + /** + * Set if double slashes to be replaced by single slash in the request URI. + *

Default is "true". + */ + public void setSanitizePath(boolean sanitizePath) { + checkReadOnly(); + this.sanitizePath = sanitizePath; + } + + /** + * Whether configured to replace double slashes with single slash from the request URI. + */ + public boolean shouldSanitizePath() { + return this.sanitizePath; + } + /** * Set the default character encoding to use for URL decoding. * Default is ISO-8859-1, according to the Servlet spec. @@ -392,12 +410,16 @@ else if (index1 == requestUri.length()) { } /** - * Sanitize the given path. Uses the following rules: + * Sanitize the given path if {code shouldSanitizePath()} is true. + * Uses the following rules: *

    *
  • replace all "//" by "/"
  • *
*/ - private static String getSanitizedPath(final String path) { + private String getSanitizedPath(final String path) { + if (!shouldSanitizePath()) { + return path; + } int start = path.indexOf("//"); if (start == -1) { return path; 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..cb3613b48b98 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 @@ -849,11 +849,23 @@ void toUriStringWithCurlyBraces(ParserType parserType) { } @Test // gh-26012 - void verifyDoubleSlashReplacedWithSingleOne() { + void verifyDoubleSlashReplacedWithSingleDefault() { String path = UriComponentsBuilder.fromPath("/home/").path("/path").build().getPath(); assertThat(path).isEqualTo("/home/path"); } + @Test // gh-34076 + void verifyDoubleSlashNotReplacedAsSingleSlash() { + String path = UriComponentsBuilder.fromPath("/home/").path("/path").build(true, false).getPath(); + assertThat(path).isEqualTo("/home//path"); + } + + @Test // gh-34076 + void verifyDoubleSlashReplacedAsSingleSlashWithConfig() { + String path = UriComponentsBuilder.fromPath("/home/").path("/path").build(true, true).getPath(); + assertThat(path).isEqualTo("/home/path"); + } + @ParameterizedTest @EnumSource void validPort(ParserType parserType) { diff --git a/spring-web/src/test/java/org/springframework/web/util/UrlPathHelperTests.java b/spring-web/src/test/java/org/springframework/web/util/UrlPathHelperTests.java index f5db19c830a7..36398d80cc67 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UrlPathHelperTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UrlPathHelperTests.java @@ -110,6 +110,20 @@ void getRequestUri() { assertThat(helper.getRequestUri(request)).isEqualTo("/home/path"); } + @Test // gh-34076 + void getRequestUriWithSanitizingDisabled() { + helper.setSanitizePath(false); + request.setRequestURI("/home/" + "/path"); + assertThat(helper.getRequestUri(request)).isEqualTo("/home//path"); + } + + @Test // gh-34076 + void getRequestUriWithSanitizingEnabled() { + helper.setSanitizePath(true); + request.setRequestURI("/home/" + "/path"); + assertThat(helper.getRequestUri(request)).isEqualTo("/home/path"); + } + @Test void getRequestRemoveSemicolonContent() { helper.setRemoveSemicolonContent(true);