Apply extra checks to static resource handling

- remove leading '/' and control chars
- improve url and relative path checks
- account for URL encoding
- add isResourceUnderLocation final verification

Issue: SPR-12354
This commit is contained in:
Rossen Stoyanchev
2014-11-11 07:12:44 +01:00
committed by Stephane Nicoll
parent a831ed524f
commit 9cef8e3001
7 changed files with 406 additions and 54 deletions

View File

@@ -17,6 +17,7 @@
package org.springframework.web.servlet.resource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
@@ -75,13 +76,13 @@ public class AppCacheManifestTransformerTests {
@Test
public void transformManifest() throws Exception {
VersionResourceResolver versionResourceResolver = new VersionResourceResolver();
versionResourceResolver
.setStrategyMap(Collections.singletonMap("/**", new ContentVersionStrategy()));
VersionResourceResolver versionResolver = new VersionResourceResolver();
versionResolver.setStrategyMap(Collections.singletonMap("/**", new ContentVersionStrategy()));
List<ResourceResolver> resolvers = new ArrayList<ResourceResolver>();
resolvers.add(versionResourceResolver);
resolvers.add(new PathResourceResolver());
PathResourceResolver pathResolver = new PathResourceResolver();
pathResolver.setAllowedLocations(new ClassPathResource("test/", getClass()));
List<ResourceResolver> resolvers = Arrays.asList(versionResolver, pathResolver);
ResourceResolverChain resolverChain = new DefaultResourceResolverChain(resolvers);
List<ResourceTransformer> transformers = new ArrayList<>();

View File

@@ -47,10 +47,13 @@ public class CssLinkResourceTransformerTests {
@Before
public void setUp() {
VersionResourceResolver resolver = new VersionResourceResolver();
resolver.setStrategyMap(Collections.singletonMap("/**", new ContentVersionStrategy()));
VersionResourceResolver versionResolver = new VersionResourceResolver();
versionResolver.setStrategyMap(Collections.singletonMap("/**", new ContentVersionStrategy()));
List<ResourceResolver> resolvers = Arrays.asList(resolver, new PathResourceResolver());
PathResourceResolver pathResolver = new PathResourceResolver();
pathResolver.setAllowedLocations(new ClassPathResource("test/", getClass()));
List<ResourceResolver> resolvers = Arrays.asList(versionResolver, pathResolver);
List<ResourceTransformer> transformers = Arrays.asList(new CssLinkResourceTransformer());
ResourceResolverChain resolverChain = new DefaultResourceResolverChain(resolvers);

View File

@@ -1,46 +1,102 @@
/*
* Copyright 2002-2014 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
*
* http://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 java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import static org.junit.Assert.*;
import org.springframework.core.io.UrlResource;
/**
* Unit tests for
* {@link org.springframework.web.servlet.resource.PathResourceResolver}.
*
* @author Brian Clozel
* @author Rossen Stoyanchev
*/
public class PathResourceResolverTests {
private ResourceResolverChain chain;
private PathResourceResolver resolver;
private List<Resource> locations;
@Before
public void setup() {
List<ResourceResolver> resolvers = new ArrayList<>();
resolvers.add(new PathResourceResolver());
this.chain = new DefaultResourceResolverChain(resolvers);
this.locations = new ArrayList<>();
this.locations.add(new ClassPathResource("test/", getClass()));
this.resolver = new PathResourceResolver();
}
@Test
public void resolveResourceInternal() {
String file = "bar.css";
Resource expected = new ClassPathResource("test/" + file, getClass());
Resource actual = this.chain.resolveResource(null, file, this.locations);
public void resolveFromClasspath() throws IOException {
Resource location = new ClassPathResource("test/", PathResourceResolver.class);
String requestPath = "bar.css";
Resource actual = this.resolver.resolveResource(null, requestPath, Arrays.asList(location), null);
assertEquals(location.createRelative(requestPath), actual);
}
assertEquals(expected, actual);
@Test
public void resolveFromClasspathRoot() throws IOException {
Resource location = new ClassPathResource("/");
String requestPath = "org/springframework/web/servlet/resource/test/bar.css";
Resource actual = this.resolver.resolveResource(null, requestPath, Arrays.asList(location), null);
assertNotNull(actual);
}
@Test
public void checkResource() throws IOException {
Resource location = new ClassPathResource("test/", PathResourceResolver.class);
testCheckResource(location, "../testsecret/secret.txt");
testCheckResource(location, "test/../../testsecret/secret.txt");
location = new UrlResource(getClass().getResource("./test/"));
String secretPath = new UrlResource(getClass().getResource("testsecret/secret.txt")).getURL().getPath();
testCheckResource(location, "file:" + secretPath);
testCheckResource(location, "/file:" + secretPath);
testCheckResource(location, "/" + secretPath);
testCheckResource(location, "////../.." + secretPath);
testCheckResource(location, "/%2E%2E/testsecret/secret.txt");
testCheckResource(location, "/%2e%2e/testsecret/secret.txt");
testCheckResource(location, " " + secretPath);
testCheckResource(location, "/ " + secretPath);
testCheckResource(location, "url:" + secretPath);
}
@Test
public void checkResourceWithAllowedLocations() {
this.resolver.setAllowedLocations(
new ClassPathResource("test/", PathResourceResolver.class),
new ClassPathResource("testalternatepath/", PathResourceResolver.class)
);
Resource location = new ClassPathResource("test/main.css", PathResourceResolver.class);
String actual = this.resolver.resolveUrlPath("../testalternatepath/bar.css", Arrays.asList(location), null);
assertEquals("../testalternatepath/bar.css", actual);
}
private void testCheckResource(Resource location, String requestPath) throws IOException {
Resource actual = this.resolver.resolveResource(null, requestPath, Arrays.asList(location), null);
assertTrue(location.createRelative(requestPath).exists());
assertNull(actual);
}
}

View File

@@ -16,25 +16,28 @@
package org.springframework.web.servlet.resource;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.mock.web.test.MockHttpServletResponse;
import org.springframework.mock.web.test.MockServletContext;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.servlet.HandlerMapping;
import static org.junit.Assert.*;
/**
* Unit tests for ResourceHttpRequestHandler.
*
@@ -126,23 +129,94 @@ public class ResourceHttpRequestHandlerTests {
}
@Test
public void getResourceViaDirectoryTraversal() throws Exception {
this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "../testsecret/secret.txt");
this.handler.handleRequest(this.request, this.response);
assertEquals(404, this.response.getStatus());
public void invalidPath() throws Exception {
this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "test/../../testsecret/secret.txt");
Resource location = new ClassPathResource("test/", getClass());
this.handler.setLocations(Arrays.asList(location));
testInvalidPath(location, "../testsecret/secret.txt");
testInvalidPath(location, "test/../../testsecret/secret.txt");
testInvalidPath(location, ":/../../testsecret/secret.txt");
location = new UrlResource(getClass().getResource("./test/"));
this.handler.setLocations(Arrays.asList(location));
Resource secretResource = new UrlResource(getClass().getResource("testsecret/secret.txt"));
String secretPath = secretResource.getURL().getPath();
testInvalidPath(location, "file:" + secretPath);
testInvalidPath(location, "/file:" + secretPath);
testInvalidPath(location, "url:" + secretPath);
testInvalidPath(location, "/url:" + secretPath);
testInvalidPath(location, "/" + secretPath);
testInvalidPath(location, "////../.." + secretPath);
testInvalidPath(location, "/%2E%2E/testsecret/secret.txt");
testInvalidPath(location, "/ " + secretPath);
testInvalidPath(location, "url:" + secretPath);
}
@Test
public void ignoreInvalidEscapeSequence() throws Exception {
this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/%foo%/bar.txt");
this.response = new MockHttpServletResponse();
this.handler.handleRequest(this.request, this.response);
assertEquals(404, this.response.getStatus());
}
this.handler.setLocations(Arrays.<Resource>asList(new ClassPathResource("testsecret/", getClass())));
this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "secret.txt");
this.response = new MockHttpServletResponse();
this.handler.handleRequest(this.request, this.response);
assertEquals(200, this.response.getStatus());
assertEquals("text/plain", this.response.getContentType());
assertEquals("big secret", this.response.getContentAsString());
@Test
public void processPath() throws Exception {
assertSame("/foo/bar", this.handler.processPath("/foo/bar"));
assertSame("foo/bar", this.handler.processPath("foo/bar"));
// leading whitespace control characters (00-1F)
assertEquals("/foo/bar", this.handler.processPath(" /foo/bar"));
assertEquals("/foo/bar", this.handler.processPath((char) 1 + "/foo/bar"));
assertEquals("/foo/bar", this.handler.processPath((char) 31 + "/foo/bar"));
assertEquals("foo/bar", this.handler.processPath(" foo/bar"));
assertEquals("foo/bar", this.handler.processPath((char) 31 + "foo/bar"));
// leading control character 0x7F (DEL)
assertEquals("/foo/bar", this.handler.processPath((char) 127 + "/foo/bar"));
assertEquals("/foo/bar", this.handler.processPath((char) 127 + "/foo/bar"));
// leading control and '/' characters
assertEquals("/foo/bar", this.handler.processPath(" / foo/bar"));
assertEquals("/foo/bar", this.handler.processPath(" / / foo/bar"));
assertEquals("/foo/bar", this.handler.processPath(" // /// //// foo/bar"));
assertEquals("/foo/bar", this.handler.processPath((char) 1 + " / " + (char) 127 + " // foo/bar"));
// root or empty path
assertEquals("", this.handler.processPath(" "));
assertEquals("/", this.handler.processPath("/"));
assertEquals("/", this.handler.processPath("///"));
assertEquals("/", this.handler.processPath("/ / / "));
}
@Test
public void initAllowedLocations() throws Exception {
PathResourceResolver resolver = (PathResourceResolver) this.handler.getResourceResolvers().get(0);
Resource[] locations = resolver.getAllowedLocations();
assertEquals(2, locations.length);
assertEquals("test/", ((ClassPathResource) locations[0]).getPath());
assertEquals("testalternatepath/", ((ClassPathResource) locations[1]).getPath());
}
@Test
public void initAllowedLocationsWithExplicitConfiguration() throws Exception {
ClassPathResource location1 = new ClassPathResource("test/", getClass());
ClassPathResource location2 = new ClassPathResource("testalternatepath/", getClass());
PathResourceResolver pathResolver = new PathResourceResolver();
pathResolver.setAllowedLocations(location1);
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
handler.setResourceResolvers(Arrays.asList(pathResolver));
handler.setLocations(Arrays.asList(location1, location2));
handler.afterPropertiesSet();
Resource[] locations = pathResolver.getAllowedLocations();
assertEquals(1, locations.length);
assertEquals("test/", ((ClassPathResource) locations[0]).getPath());
}
@Test
@@ -203,6 +277,14 @@ public class ResourceHttpRequestHandlerTests {
return new ClassPathResource(resourceName, getClass()).getFile().lastModified();
}
private void testInvalidPath(Resource location, String requestPath) throws Exception {
this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, requestPath);
this.response = new MockHttpServletResponse();
this.handler.handleRequest(this.request, this.response);
assertTrue(location.createRelative(requestPath).exists());
assertEquals(404, this.response.getStatus());
}
private static class TestServletContext extends MockServletContext {

View File

@@ -46,9 +46,11 @@ public class ResourceTransformerSupportTests {
@Before
public void setUp() {
VersionResourceResolver resolver = new VersionResourceResolver();
resolver.setStrategyMap(Collections.singletonMap("/**", new ContentVersionStrategy()));
List<ResourceResolver> resolvers = Arrays.asList(resolver, new PathResourceResolver());
VersionResourceResolver versionResolver = new VersionResourceResolver();
versionResolver.setStrategyMap(Collections.singletonMap("/**", new ContentVersionStrategy()));
PathResourceResolver pathResolver = new PathResourceResolver();
pathResolver.setAllowedLocations(new ClassPathResource("test/", getClass()));
List<ResourceResolver> resolvers = Arrays.asList(versionResolver, pathResolver);
this.transformerChain = new DefaultResourceTransformerChain(new DefaultResourceResolverChain(resolvers), null);
this.transformer = new TestResourceTransformerSupport();