Warn against unsafe static resource locations

Prior to this commit, `ResourceHandlerUtils` would perform resource
location checks to ensure that the configured location is valid. This
commit also ensures that we log a WARN message if the application
chooses a well-known unsafe location like "classpath:" or the root
Servlet context for serving static resources.

Closes gh-36692
This commit is contained in:
Brian Clozel
2026-04-23 10:45:15 +02:00
parent 8965d9bccf
commit 0725bf4941
4 changed files with 44 additions and 12 deletions
@@ -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));
@@ -153,10 +153,7 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
public void setLocations(@Nullable List<Resource> 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();
}
@@ -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));
@@ -170,10 +170,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
public void setLocations(List<Resource> 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();
}