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 8a45d15bfcc..2e95c58fd5b 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,12 +19,14 @@ 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.jspecify.annotations.Nullable; 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. */ @SuppressWarnings("removal") public static void assertResourceLocation(@Nullable Resource location) { @@ -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(); @@ -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 3704dcb8d09..bec8e49980b 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 @@ -153,10 +153,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); } } @@ -373,6 +370,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 8717cfefcc9..5b1a133c8fa 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,12 +19,14 @@ 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.jspecify.annotations.Nullable; 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. */ @SuppressWarnings("removal") public static void assertResourceLocation(@Nullable Resource location) { @@ -67,6 +70,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 72e04b077d7..1689bce5184 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 @@ -170,10 +170,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); } /** @@ -471,6 +468,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(); }