diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceHandlerUtils.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceHandlerUtils.java index 10da1a99874..3c33d7d7915 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceHandlerUtils.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceHandlerUtils.java @@ -19,11 +19,13 @@ package org.springframework.web.reactive.resource; import java.io.IOException; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; +import java.util.Locale; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.ContextResource; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; @@ -51,7 +53,8 @@ public abstract class ResourceHandlerUtils { /** - * Assert the given location is not null, and its path ends on slash. + * Assert the given location is valid. + * Location should not be null, its path ends with a slash, and not be an unsafe location. */ public static void assertResourceLocation(@Nullable Resource location) { Assert.notNull(location, "Resource location must not be null"); @@ -65,6 +68,17 @@ public abstract class ResourceHandlerUtils { } else if (location instanceof ClassPathResource classPathResource) { path = classPathResource.getPath(); + if (path.isEmpty() || "/".equals(path)) { + logger.warn("Resource location '" + location + "' is considered unsafe " + + "and should not be used as it provides access to the entire classpath."); + } + } + else if (location instanceof ContextResource contextResource) { + path = contextResource.getPathWithinContext(); + if ("/".equals(path)) { + logger.warn("Resource location '" + location + "' is considered unsafe " + + "and should not be used as it provides access to the root servlet context."); + } } else if (location instanceof UrlResource) { path = location.getURL().toExternalForm(); @@ -175,7 +189,8 @@ public abstract class ResourceHandlerUtils { * @return {@code true} if the path is invalid, {@code false} otherwise */ public static boolean isInvalidPath(String path) { - if (path.contains("WEB-INF") || path.contains("META-INF")) { + String pathLowerCase = path.toLowerCase(Locale.ROOT); + if (pathLowerCase.contains("web-inf") || pathLowerCase.contains("meta-inf")) { if (logger.isWarnEnabled()) { logger.warn(LogFormatUtils.formatValue( "Path with \"WEB-INF\" or \"META-INF\": [" + path + "]", -1, true)); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java index da9ec235a12..13447717b67 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java @@ -160,10 +160,7 @@ public class ResourceWebHandler implements WebHandler, InitializingBean { public void setLocations(@Nullable List locations) { this.locationResources.clear(); if (locations != null) { - for (Resource location : locations) { - ResourceHandlerUtils.assertResourceLocation(location); - this.locationResources.add(location); - } + this.locationResources.addAll(locations); } } @@ -384,6 +381,10 @@ public class ResourceWebHandler implements WebHandler, InitializingBean { } } + for (Resource location : result) { + ResourceHandlerUtils.assertResourceLocation(location); + } + if (isOptimizeLocations()) { result = result.stream().filter(Resource::exists).toList(); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHandlerUtils.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHandlerUtils.java index 8b953d62218..f576776e93c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHandlerUtils.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHandlerUtils.java @@ -19,11 +19,13 @@ package org.springframework.web.servlet.resource; import java.io.IOException; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; +import java.util.Locale; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.ContextResource; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; @@ -52,7 +54,8 @@ public abstract class ResourceHandlerUtils { /** - * Assert the given location is not null, and its path ends on slash. + * Assert the given location is valid. + * Location should not be null, its path ends with a slash, and not be an unsafe location. */ public static void assertResourceLocation(@Nullable Resource location) { Assert.notNull(location, "Resource location must not be null"); @@ -66,6 +69,17 @@ public abstract class ResourceHandlerUtils { } else if (location instanceof ClassPathResource classPathResource) { path = classPathResource.getPath(); + if (path.isEmpty() || "/".equals(path)) { + logger.warn("Resource location '" + location + "' is considered unsafe " + + "and should not be used as it provides access to the entire classpath."); + } + } + else if (location instanceof ContextResource contextResource) { + path = contextResource.getPathWithinContext(); + if ("/".equals(path)) { + logger.warn("Resource location '" + location + "' is considered unsafe " + + "and should not be used as it provides access to the root servlet context."); + } } else if (location instanceof UrlResource) { path = location.getURL().toExternalForm(); @@ -176,7 +190,8 @@ public abstract class ResourceHandlerUtils { * @return {@code true} if the path is invalid, {@code false} otherwise */ public static boolean isInvalidPath(String path) { - if (path.contains("WEB-INF") || path.contains("META-INF")) { + String pathLowerCase = path.toLowerCase(Locale.ROOT); + if (pathLowerCase.contains("web-inf") || pathLowerCase.contains("meta-inf")) { if (logger.isWarnEnabled()) { logger.warn(LogFormatUtils.formatValue( "Path with \"WEB-INF\" or \"META-INF\": [" + path + "]", -1, true)); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java index cd5414683cf..dca3cbc8148 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java @@ -180,10 +180,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator public void setLocations(List locations) { Assert.notNull(locations, "Locations list must not be null"); this.locationResources.clear(); - for (Resource location : locations) { - ResourceHandlerUtils.assertResourceLocation(location); - this.locationResources.add(location); - } + this.locationResources.addAll(locations); } /** @@ -515,6 +512,10 @@ public class ResourceHttpRequestHandler extends WebContentGenerator } result.addAll(this.locationResources); + for (Resource location : result) { + ResourceHandlerUtils.assertResourceLocation(location); + } + if (isOptimizeLocations()) { result = result.stream().filter(Resource::exists).toList(); }