Skip to content

Commit 9961647

Browse files
committed
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
1 parent 674f2f5 commit 9961647

File tree

6 files changed

+47
-3
lines changed

6 files changed

+47
-3
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818

1919
import java.io.File;
2020
import java.io.IOException;
21-
import java.net.MalformedURLException;
2221
import java.net.URL;
22+
import java.net.URLEncoder;
2323
import java.time.Duration;
2424
import java.util.ArrayList;
2525
import java.util.Arrays;
@@ -32,6 +32,7 @@
3232
import java.util.Map;
3333
import java.util.Set;
3434
import java.util.concurrent.TimeUnit;
35+
import java.util.regex.Pattern;
3536

3637
import javax.servlet.ServletContainerInitializer;
3738
import javax.servlet.ServletContext;
@@ -94,6 +95,8 @@
9495
public class UndertowServletWebServerFactory extends AbstractServletWebServerFactory
9596
implements ConfigurableUndertowWebServerFactory, ResourceLoaderAware {
9697

98+
private static final Pattern ENCODED_SLASH = Pattern.compile("%2F", Pattern.LITERAL);
99+
97100
private static final Set<Class<?>> NO_CLASSES = Collections.emptySet();
98101

99102
private List<UndertowBuilderCustomizer> builderCustomizers = new ArrayList<>();
@@ -578,14 +581,15 @@ public void removeResourceChangeListener(ResourceChangeListener listener) {
578581

579582
private URLResource getMetaInfResource(URL resourceJar, String path) {
580583
try {
581-
URL resourceUrl = new URL(resourceJar + "META-INF/resources" + path);
584+
String urlPath = URLEncoder.encode(ENCODED_SLASH.matcher(path).replaceAll("/"), "UTF-8");
585+
URL resourceUrl = new URL(resourceJar + "META-INF/resources" + urlPath);
582586
URLResource resource = new URLResource(resourceUrl, path);
583587
if (resource.getContentLength() < 0) {
584588
return null;
585589
}
586590
return resource;
587591
}
588-
catch (MalformedURLException ex) {
592+
catch (Exception ex) {
589593
return null;
590594
}
591595
}

spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/ApplicationBuilder.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ private File createResourcesJar() throws IOException {
9090
resourcesJarStream.putNextEntry(new ZipEntry("META-INF/resources/nested-meta-inf-resource.txt"));
9191
resourcesJarStream.write("nested".getBytes());
9292
resourcesJarStream.closeEntry();
93+
resourcesJarStream.putNextEntry(
94+
new ZipEntry("META-INF/resources/nested-reserved-!#$%&()*+,:=?@[]-meta-inf-resource.txt"));
95+
resourcesJarStream.write("encoded-name".getBytes());
96+
resourcesJarStream.closeEntry();
9397
return resourcesJar;
9498
}
9599
}

spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarDevelopmentIntegrationTests.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ public void metaInfResourceFromDependencyIsAvailableViaHttp() {
5454
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
5555
}
5656

57+
@Test
58+
public void metaInfResourceFromDependencyWithNameThatContainsReservedCharactersIsAvailableViaHttp() {
59+
ResponseEntity<String> entity = this.rest.getForEntity(
60+
"/nested-reserved-%21%23%24%25%26%28%29%2A%2B%2C%3A%3D%3F%40%5B%5D-meta-inf-resource.txt",
61+
String.class);
62+
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
63+
assertThat(entity.getBody()).isEqualTo("encoded-name");
64+
}
65+
5766
@Test
5867
public void metaInfResourceFromDependencyIsAvailableViaServletContext() {
5968
ResponseEntity<String> entity = this.rest.getForEntity("/servletContext?/nested-meta-inf-resource.txt",

spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarPackagingIntegrationTests.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ public void nestedMetaInfResourceIsAvailableViaHttp() {
5454
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
5555
}
5656

57+
@Test
58+
public void nestedMetaInfResourceWithNameThatContainsReservedCharactersIsAvailableViaHttp() {
59+
ResponseEntity<String> entity = this.rest.getForEntity(
60+
"/nested-reserved-%21%23%24%25%26%28%29%2A%2B%2C%3A%3D%3F%40%5B%5D-meta-inf-resource.txt",
61+
String.class);
62+
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
63+
assertThat(entity.getBody()).isEqualTo("encoded-name");
64+
}
65+
5766
@Test
5867
public void nestedMetaInfResourceIsAvailableViaServletContext() {
5968
ResponseEntity<String> entity = this.rest.getForEntity("/servletContext?/nested-meta-inf-resource.txt",

spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarDevelopmentIntegrationTests.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ public void metaInfResourceFromDependencyIsAvailableViaHttp() {
6060
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
6161
}
6262

63+
@Test
64+
public void metaInfResourceFromDependencyWithNameThatContainsReservedCharactersIsAvailableViaHttp() {
65+
ResponseEntity<String> entity = this.rest.getForEntity(
66+
"/nested-reserved-%21%23%24%25%26%28%29%2A%2B%2C%3A%3D%3F%40%5B%5D-meta-inf-resource.txt",
67+
String.class);
68+
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
69+
assertThat(entity.getBody()).isEqualTo("encoded-name");
70+
}
71+
6372
@Test
6473
public void metaInfResourceFromDependencyIsAvailableViaServletContext() {
6574
ResponseEntity<String> entity = this.rest.getForEntity("/servletContext?/nested-meta-inf-resource.txt",

spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerWarPackagingIntegrationTests.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ public void nestedMetaInfResourceIsAvailableViaHttp() {
6060
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
6161
}
6262

63+
@Test
64+
public void nestedMetaInfResourceWithNameThatContainsReservedCharactersIsAvailableViaHttp() {
65+
ResponseEntity<String> entity = this.rest.getForEntity(
66+
"/nested-reserved-%21%23%24%25%26%28%29%2A%2B%2C%3A%3D%3F%40%5B%5D-meta-inf-resource.txt",
67+
String.class);
68+
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
69+
assertThat(entity.getBody()).isEqualTo("encoded-name");
70+
}
71+
6372
@Test
6473
public void nestedMetaInfResourceIsAvailableViaServletContext() {
6574
ResponseEntity<String> entity = this.rest.getForEntity("/servletContext?/nested-meta-inf-resource.txt",

0 commit comments

Comments
 (0)