From 9961647c7f3df95d9d5f24fccf138914992763a5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 29 Aug 2019 11:58:21 +0100 Subject: [PATCH] Improve handling of reserved characters in MetaInfResourceManager Previously, MetaInfResourceManager that we use with Undertow to serve static resources from jar's META-INF/resources did not correctly handle characters in the path that should be percent-encoded when used in a URL. This commit updates MetaInfResourceManager to encode the path before it is used to create a URL. Prior to this encoding, encoded slashes (%2F) are decoded as, unlike other encoded characters in the request's URL, encoded slashes are not decoded prior to calling the ResourceManager. Fixes gh-17853 --- .../undertow/UndertowServletWebServerFactory.java | 10 +++++++--- .../boot/context/embedded/ApplicationBuilder.java | 4 ++++ ...ServletContainerJarDevelopmentIntegrationTests.java | 9 +++++++++ ...edServletContainerJarPackagingIntegrationTests.java | 9 +++++++++ ...ServletContainerWarDevelopmentIntegrationTests.java | 9 +++++++++ ...edServletContainerWarPackagingIntegrationTests.java | 9 +++++++++ 6 files changed, 47 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java index 3bf1bbabf11..7ec70ee5446 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java @@ -18,8 +18,8 @@ package org.springframework.boot.web.embedded.undertow; import java.io.File; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URL; +import java.net.URLEncoder; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -32,6 +32,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; import javax.servlet.ServletContainerInitializer; import javax.servlet.ServletContext; @@ -94,6 +95,8 @@ import org.springframework.util.Assert; public class UndertowServletWebServerFactory extends AbstractServletWebServerFactory implements ConfigurableUndertowWebServerFactory, ResourceLoaderAware { + private static final Pattern ENCODED_SLASH = Pattern.compile("%2F", Pattern.LITERAL); + private static final Set> NO_CLASSES = Collections.emptySet(); private List builderCustomizers = new ArrayList<>(); @@ -578,14 +581,15 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac private URLResource getMetaInfResource(URL resourceJar, String path) { try { - URL resourceUrl = new URL(resourceJar + "META-INF/resources" + path); + String urlPath = URLEncoder.encode(ENCODED_SLASH.matcher(path).replaceAll("/"), "UTF-8"); + URL resourceUrl = new URL(resourceJar + "META-INF/resources" + urlPath); URLResource resource = new URLResource(resourceUrl, path); if (resource.getContentLength() < 0) { return null; } return resource; } - catch (MalformedURLException ex) { + catch (Exception ex) { return null; } } diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/ApplicationBuilder.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/ApplicationBuilder.java index 32792d04bcd..dd3f30a3129 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/ApplicationBuilder.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/ApplicationBuilder.java @@ -90,6 +90,10 @@ class ApplicationBuilder { resourcesJarStream.putNextEntry(new ZipEntry("META-INF/resources/nested-meta-inf-resource.txt")); resourcesJarStream.write("nested".getBytes()); resourcesJarStream.closeEntry(); + resourcesJarStream.putNextEntry( + new ZipEntry("META-INF/resources/nested-reserved-!#$%&()*+,:=?@[]-meta-inf-resource.txt")); + resourcesJarStream.write("encoded-name".getBytes()); + resourcesJarStream.closeEntry(); return resourcesJar; } } diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarDevelopmentIntegrationTests.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarDevelopmentIntegrationTests.java index 6b61c999862..a632c58bec2 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarDevelopmentIntegrationTests.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarDevelopmentIntegrationTests.java @@ -54,6 +54,15 @@ public class EmbeddedServletContainerJarDevelopmentIntegrationTests assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); } + @Test + public void metaInfResourceFromDependencyWithNameThatContainsReservedCharactersIsAvailableViaHttp() { + ResponseEntity entity = this.rest.getForEntity( + "/nested-reserved-%21%23%24%25%26%28%29%2A%2B%2C%3A%3D%3F%40%5B%5D-meta-inf-resource.txt", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).isEqualTo("encoded-name"); + } + @Test public void metaInfResourceFromDependencyIsAvailableViaServletContext() { ResponseEntity entity = this.rest.getForEntity("/servletContext?/nested-meta-inf-resource.txt", diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarPackagingIntegrationTests.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarPackagingIntegrationTests.java index 06900c056e6..268b27db9ce 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarPackagingIntegrationTests.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarPackagingIntegrationTests.java @@ -54,6 +54,15 @@ public class EmbeddedServletContainerJarPackagingIntegrationTests assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); } + @Test + public void nestedMetaInfResourceWithNameThatContainsReservedCharactersIsAvailableViaHttp() { + ResponseEntity entity = this.rest.getForEntity( + "/nested-reserved-%21%23%24%25%26%28%29%2A%2B%2C%3A%3D%3F%40%5B%5D-meta-inf-resource.txt", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).isEqualTo("encoded-name"); + } + @Test public void nestedMetaInfResourceIsAvailableViaServletContext() { ResponseEntity entity = this.rest.getForEntity("/servletContext?/nested-meta-inf-resource.txt", diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarDevelopmentIntegrationTests.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarDevelopmentIntegrationTests.java index 98c4258c989..f2e5d3bd0ac 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarDevelopmentIntegrationTests.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarDevelopmentIntegrationTests.java @@ -60,6 +60,15 @@ public class EmbeddedServletContainerWarDevelopmentIntegrationTests assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); } + @Test + public void metaInfResourceFromDependencyWithNameThatContainsReservedCharactersIsAvailableViaHttp() { + ResponseEntity entity = this.rest.getForEntity( + "/nested-reserved-%21%23%24%25%26%28%29%2A%2B%2C%3A%3D%3F%40%5B%5D-meta-inf-resource.txt", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).isEqualTo("encoded-name"); + } + @Test public void metaInfResourceFromDependencyIsAvailableViaServletContext() { ResponseEntity entity = this.rest.getForEntity("/servletContext?/nested-meta-inf-resource.txt", diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarPackagingIntegrationTests.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarPackagingIntegrationTests.java index 69dc3703eed..f9081f7dceb 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarPackagingIntegrationTests.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarPackagingIntegrationTests.java @@ -60,6 +60,15 @@ public class EmbeddedServletContainerWarPackagingIntegrationTests assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); } + @Test + public void nestedMetaInfResourceWithNameThatContainsReservedCharactersIsAvailableViaHttp() { + ResponseEntity entity = this.rest.getForEntity( + "/nested-reserved-%21%23%24%25%26%28%29%2A%2B%2C%3A%3D%3F%40%5B%5D-meta-inf-resource.txt", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).isEqualTo("encoded-name"); + } + @Test public void nestedMetaInfResourceIsAvailableViaServletContext() { ResponseEntity entity = this.rest.getForEntity("/servletContext?/nested-meta-inf-resource.txt",