mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Reject unsafe resource handling locations
As of gh-36692, Spring logs a WARN message when an unsafe resource handling location is configured. This change now rejects entirely such setups by failing before the application starts up. Closes gh-36695
This commit is contained in:
+6
-18
@@ -19,7 +19,6 @@ 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;
|
||||
@@ -69,17 +68,15 @@ 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.");
|
||||
}
|
||||
Assert.isTrue(!path.isEmpty() && !"/".equals(path),
|
||||
() -> "Resource location '" + location + "' is considered unsafe " +
|
||||
"and cannot 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.");
|
||||
}
|
||||
Assert.isTrue(!"/".equals(path),
|
||||
() -> "Resource location '" + location + "' is considered unsafe " +
|
||||
"and cannot be used as it provides access to the root servlet context.");
|
||||
}
|
||||
else if (location instanceof UrlResource) {
|
||||
path = location.getURL().toExternalForm();
|
||||
@@ -176,7 +173,6 @@ public abstract class ResourceHandlerUtils {
|
||||
/**
|
||||
* Checks for invalid resource input paths rejecting the following:
|
||||
* <ul>
|
||||
* <li>Paths that contain "WEB-INF" or "META-INF"
|
||||
* <li>Paths that contain "../" after a call to
|
||||
* {@link StringUtils#cleanPath}.
|
||||
* <li>Paths that represent a {@link ResourceUtils#isUrl
|
||||
@@ -189,14 +185,6 @@ public abstract class ResourceHandlerUtils {
|
||||
* @return {@code true} if the path is invalid, {@code false} otherwise
|
||||
*/
|
||||
public static boolean isInvalidPath(String path) {
|
||||
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));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (path.contains(":/")) {
|
||||
String relativePath = (path.charAt(0) == '/' ? path.substring(1) : path);
|
||||
if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) {
|
||||
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.reactive.resource;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.ContextResource;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.PathResource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
/**
|
||||
* Tests for {@link ResourceHandlerUtils}.
|
||||
*/
|
||||
class ResourceHandlerUtilsTests {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("removal")
|
||||
void assertResourceLocation() throws Exception {
|
||||
assertThatNoException().isThrownBy(() ->
|
||||
ResourceHandlerUtils.assertResourceLocation(new ClassPathResource("test/")));
|
||||
|
||||
assertThatNoException().isThrownBy(() ->
|
||||
ResourceHandlerUtils.assertResourceLocation(new FileSystemResource("test/")));
|
||||
|
||||
assertThatNoException().isThrownBy(() ->
|
||||
ResourceHandlerUtils.assertResourceLocation(new PathResource("test/")));
|
||||
|
||||
assertThatNoException().isThrownBy(() ->
|
||||
ResourceHandlerUtils.assertResourceLocation(new UrlResource("file:/test/")));
|
||||
|
||||
assertThatNoException().isThrownBy(() ->
|
||||
ResourceHandlerUtils.assertResourceLocation(new TestContextResource("/test/")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void assertResourceLocationShouldRejectNull() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
ResourceHandlerUtils.assertResourceLocation(null))
|
||||
.withMessage("Resource location must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void assertResourceLocationShouldRejectNotEndWithSlash() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
ResourceHandlerUtils.assertResourceLocation(new ClassPathResource("test")))
|
||||
.withMessageContaining("Resource location does not end with slash");
|
||||
}
|
||||
|
||||
@Test
|
||||
void assertResourceLocationShouldRejectUnsafeClassPathResource() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
ResourceHandlerUtils.assertResourceLocation(new ClassPathResource("")))
|
||||
.withMessageContaining("is considered unsafe");
|
||||
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
ResourceHandlerUtils.assertResourceLocation(new ClassPathResource("/")))
|
||||
.withMessageContaining("is considered unsafe");
|
||||
}
|
||||
|
||||
@Test
|
||||
void assertResourceLocationShouldRejectUnsafeContextResource() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
ResourceHandlerUtils.assertResourceLocation(new TestContextResource("/")))
|
||||
.withMessageContaining("is considered unsafe");
|
||||
}
|
||||
|
||||
private static class TestContextResource implements ContextResource {
|
||||
|
||||
private final String path;
|
||||
|
||||
TestContextResource(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPathWithinContext() {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getURL() throws IOException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getURI() throws IOException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getFile() throws IOException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() throws IOException {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long lastModified() throws IOException {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.springframework.core.io.Resource createRelative(String relativePath) throws IOException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "TestContextResource";
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+6
-18
@@ -19,7 +19,6 @@ 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;
|
||||
@@ -70,17 +69,15 @@ 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.");
|
||||
}
|
||||
Assert.isTrue(!path.isEmpty() && !"/".equals(path),
|
||||
() -> "Resource location '" + location + "' is considered unsafe " +
|
||||
"and cannot 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.");
|
||||
}
|
||||
Assert.isTrue(!"/".equals(path),
|
||||
() -> "Resource location '" + location + "' is considered unsafe " +
|
||||
"and cannot be used as it provides access to the root servlet context.");
|
||||
}
|
||||
else if (location instanceof UrlResource) {
|
||||
path = location.getURL().toExternalForm();
|
||||
@@ -177,7 +174,6 @@ public abstract class ResourceHandlerUtils {
|
||||
/**
|
||||
* Checks for invalid resource input paths rejecting the following:
|
||||
* <ul>
|
||||
* <li>Paths that contain "WEB-INF" or "META-INF"
|
||||
* <li>Paths that contain "../" after a call to
|
||||
* {@link org.springframework.util.StringUtils#cleanPath}.
|
||||
* <li>Paths that represent a {@link org.springframework.util.ResourceUtils#isUrl
|
||||
@@ -190,14 +186,6 @@ public abstract class ResourceHandlerUtils {
|
||||
* @return {@code true} if the path is invalid, {@code false} otherwise
|
||||
*/
|
||||
public static boolean isInvalidPath(String path) {
|
||||
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));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (path.contains(":/")) {
|
||||
String relativePath = (path.charAt(0) == '/' ? path.substring(1) : path);
|
||||
if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) {
|
||||
|
||||
+2
-2
@@ -239,7 +239,7 @@ class DelegatingWebMvcConfigurationTests {
|
||||
}
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry.addResourceHandler("/resources/**").addResourceLocations("/");
|
||||
registry.addResourceHandler("/resources/**").addResourceLocations("/static");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -304,7 +304,7 @@ class DelegatingWebMvcConfigurationTests {
|
||||
}
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry.addResourceHandler("/resources/**").addResourceLocations("/");
|
||||
registry.addResourceHandler("/resources/**").addResourceLocations("/static");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.servlet.resource;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.PathResource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.web.context.support.ServletContextResource;
|
||||
import org.springframework.web.testfixture.servlet.MockServletContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
/**
|
||||
* Tests for {@link ResourceHandlerUtils}.
|
||||
*/
|
||||
class ResourceHandlerUtilsTests {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("removal")
|
||||
void assertResourceLocation() throws Exception {
|
||||
assertThatNoException().isThrownBy(() ->
|
||||
ResourceHandlerUtils.assertResourceLocation(new ClassPathResource("test/")));
|
||||
|
||||
assertThatNoException().isThrownBy(() ->
|
||||
ResourceHandlerUtils.assertResourceLocation(new FileSystemResource("test/")));
|
||||
|
||||
assertThatNoException().isThrownBy(() ->
|
||||
ResourceHandlerUtils.assertResourceLocation(new PathResource("test/")));
|
||||
|
||||
assertThatNoException().isThrownBy(() ->
|
||||
ResourceHandlerUtils.assertResourceLocation(new UrlResource("file:/test/")));
|
||||
|
||||
assertThatNoException().isThrownBy(() ->
|
||||
ResourceHandlerUtils.assertResourceLocation(new ServletContextResource(new MockServletContext(), "/test/")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void assertResourceLocationShouldRejectNull() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
ResourceHandlerUtils.assertResourceLocation(null))
|
||||
.withMessage("Resource location must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void assertResourceLocationShouldRejectNotEndWithSlash() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
ResourceHandlerUtils.assertResourceLocation(new ClassPathResource("test")))
|
||||
.withMessageContaining("Resource location does not end with slash");
|
||||
}
|
||||
|
||||
@Test
|
||||
void assertResourceLocationShouldRejectUnsafeClassPathResource() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
ResourceHandlerUtils.assertResourceLocation(new ClassPathResource("")))
|
||||
.withMessageContaining("is considered unsafe");
|
||||
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
ResourceHandlerUtils.assertResourceLocation(new ClassPathResource("/")))
|
||||
.withMessageContaining("is considered unsafe");
|
||||
}
|
||||
|
||||
@Test
|
||||
void assertResourceLocationShouldRejectUnsafeContextResource() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
ResourceHandlerUtils.assertResourceLocation(new ServletContextResource(new MockServletContext(), "/")))
|
||||
.withMessageContaining("is considered unsafe");
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -12,6 +12,6 @@
|
||||
</mvc:annotation-driven>
|
||||
|
||||
<mvc:view-controller path="/foo"/>
|
||||
<mvc:resources mapping="/resources/**" location="/, classpath:/META-INF/"/>
|
||||
<mvc:resources mapping="/resources/**" location="/static, classpath:/META-INF/"/>
|
||||
|
||||
</beans>
|
||||
|
||||
+1
-1
@@ -12,6 +12,6 @@
|
||||
</mvc:annotation-driven>
|
||||
|
||||
<mvc:view-controller path="/foo"/>
|
||||
<mvc:resources mapping="/resources/**" location="/, classpath:/META-INF/"/>
|
||||
<mvc:resources mapping="/resources/**" location="/static, classpath:/META-INF/"/>
|
||||
|
||||
</beans>
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
|
||||
|
||||
<mvc:resources mapping="/resources/**" location="/, classpath:/META-INF/" />
|
||||
<mvc:resources mapping="/resources/**" location="/static, classpath:/META-INF/" />
|
||||
|
||||
<mvc:annotation-driven>
|
||||
<mvc:path-matching
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
|
||||
|
||||
<mvc:resources mapping="/resources/**" location="/, classpath:/META-INF/">
|
||||
<mvc:resources mapping="/resources/**" location="/static, classpath:/META-INF/">
|
||||
<mvc:cache-control max-age="3600" s-maxage="1800" cache-public="true"/>
|
||||
<mvc:resource-chain resource-cache="false" auto-registration="false">
|
||||
<mvc:resolvers>
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
<mvc:resources mapping="/resources/**" location="/, classpath:/META-INF/, [charset=ISO-8859-1]${location}">
|
||||
<mvc:resources mapping="/resources/**" location="/static, classpath:/META-INF/, [charset=ISO-8859-1]${location}">
|
||||
<mvc:resource-chain resource-cache="true" cache-manager="resourceCache" cache-name="test-resource-cache">
|
||||
<mvc:resolvers>
|
||||
<mvc:version-resolver>
|
||||
|
||||
+1
-1
@@ -5,6 +5,6 @@
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
|
||||
|
||||
<mvc:resources mapping="/resources/**" location="/, classpath:/META-INF/" cache-period="3600" order="5"/>
|
||||
<mvc:resources mapping="/resources/**" location="/static, classpath:/META-INF/" cache-period="3600" order="5"/>
|
||||
|
||||
</beans>
|
||||
|
||||
+1
-1
@@ -6,6 +6,6 @@
|
||||
http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
|
||||
|
||||
<mvc:annotation-driven />
|
||||
<mvc:resources mapping="/resources/**" location="/, classpath:/META-INF/" />
|
||||
<mvc:resources mapping="/resources/**" location="/static, classpath:/META-INF/" />
|
||||
|
||||
</beans>
|
||||
|
||||
Reference in New Issue
Block a user