Moderate severityNVD Advisory· Published Nov 20, 2014· Updated May 6, 2026
CVE-2014-3625
CVE-2014-3625
Description
Directory traversal vulnerability in Pivotal Spring Framework 3.0.4 through 3.2.x before 3.2.12, 4.0.x before 4.0.8, and 4.1.x before 4.1.2 allows remote attackers to read arbitrary files via unspecified vectors, related to static resource handling.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.springframework:spring-webmvcMaven | >= 3.0.4, < 3.2.12 | 3.2.12 |
org.springframework:spring-webmvcMaven | >= 4.0.0, < 4.0.8 | 4.0.8 |
org.springframework:spring-webmvcMaven | >= 4.1.0, < 4.1.2 | 4.1.2 |
Affected products
2- cpe:2.3:a:pivotal_software:spring_framework:*:*:*:*:*:*:*:*Range: >=3.1.0,<=3.1.4
Patches
4161d3e3049f1Fix location checks for servlet 3 resources
2 files changed · +21 −4
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java+5 −0 modified@@ -25,6 +25,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; +import org.springframework.web.context.support.ServletContextResource; /** * A simple {@code ResourceResolver} that tries to find a resource under the given @@ -172,6 +173,10 @@ else if (resource instanceof UrlResource) { resourcePath = resource.getURL().toExternalForm(); locationPath = location.getURL().toExternalForm(); } + else if(resource instanceof ServletContextResource) { + resourcePath = ((ServletContextResource) resource).getPath(); + locationPath = ((ServletContextResource) location).getPath(); + } else { resourcePath = resource.getURL().getPath(); locationPath = location.getURL().getPath();
spring-webmvc/src/test/java/org/springframework/web/servlet/resource/PathResourceResolverTests.java+16 −4 modified@@ -15,10 +15,7 @@ */ package org.springframework.web.servlet.resource; -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 static org.junit.Assert.*; import java.io.IOException; import java.util.Arrays; @@ -28,6 +25,8 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; +import org.springframework.mock.web.test.MockServletContext; +import org.springframework.web.context.support.ServletContextResource; /** * Unit tests for @@ -93,6 +92,19 @@ public void checkResourceWithAllowedLocations() { assertEquals("../testalternatepath/bar.css", actual); } + // SPR-12432 + @Test + public void checkServletContextResource() throws Exception { + Resource classpathLocation = new ClassPathResource("test/", PathResourceResolver.class); + MockServletContext context = new MockServletContext(); + + ServletContextResource servletContextLocation = new ServletContextResource(context, "/webjars/"); + ServletContextResource resource = new ServletContextResource(context, "/webjars/webjar-foo/1.0/foo.js"); + + assertFalse(this.resolver.checkResource(resource, classpathLocation)); + assertTrue(this.resolver.checkResource(resource, servletContextLocation)); + } + 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());
9cef8e3001ddApply extra checks to static resource handling
7 files changed · +404 −52
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java+106 −1 modified@@ -17,10 +17,14 @@ package org.springframework.web.servlet.resource; import java.io.IOException; +import java.net.URLDecoder; import java.util.List; + import javax.servlet.http.HttpServletRequest; +import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; /** * A simple {@code ResourceResolver} that tries to find a resource under the given @@ -36,6 +40,35 @@ */ public class PathResourceResolver extends AbstractResourceResolver { + private Resource[] allowedLocations; + + + /** + * By default when a Resource is found, the path of the resolved resource is + * compared to ensure it's under the input location where it was found. + * However sometimes that may not be the case, e.g. when + * {@link org.springframework.web.servlet.resource.CssLinkResourceTransformer} + * resolves public URLs of links it contains, the CSS file is the location + * and the resources being resolved are css files, images, fonts and others + * located in adjacent or parent directories. + * <p>This property allows configuring a complete list of locations under + * which resources must be so that if a resource is not under the location + * relative to which it was found, this list may be checked as well. + * <p>By default {@link ResourceHttpRequestHandler} initializes this property + * to match its list of locations. + * @param locations the list of allowed locations + * @since 4.1.2 + * @see ResourceHttpRequestHandler#initAllowedLocations() + */ + public void setAllowedLocations(Resource... locations) { + this.allowedLocations = locations; + } + + public Resource[] getAllowedLocations() { + return this.allowedLocations; + } + + @Override protected Resource resolveResourceInternal(HttpServletRequest request, String requestPath, List<? extends Resource> locations, ResourceResolverChain chain) { @@ -84,7 +117,79 @@ else if (logger.isTraceEnabled()) { */ protected Resource getResource(String resourcePath, Resource location) throws IOException { Resource resource = location.createRelative(resourcePath); - return (resource.exists() && resource.isReadable() ? resource : null); + if (resource.exists() && resource.isReadable()) { + if (checkResource(resource, location)) { + return resource; + } + else { + if (logger.isTraceEnabled()) { + logger.trace("resourcePath=\"" + resourcePath + "\" was successfully resolved " + + "but resource=\"" + resource.getURL() + "\" is neither under the " + + "current location=\"" + location.getURL() + "\" nor under any of the " + + "allowed locations=" + getAllowedLocations()); + } + } + } + return null; + } + + /** + * Perform additional checks on a resolved resource beyond checking whether + * the resources exists and is readable. The default implementation also + * verifies the resource is either under the location relative to which it + * was found or is under one of the {@link #setAllowedLocations allowed + * locations}. + * @param resource the resource to check + * @param location the location relative to which the resource was found + * @return "true" if resource is in a valid location, "false" otherwise. + * @since 4.1.2 + */ + protected boolean checkResource(Resource resource, Resource location) throws IOException { + if (isResourceUnderLocation(resource, location)) { + return true; + } + if (getAllowedLocations() != null) { + for (Resource current : getAllowedLocations()) { + if (isResourceUnderLocation(resource, current)) { + return true; + } + } + } + return false; + } + + private boolean isResourceUnderLocation(Resource resource, Resource location) throws IOException { + if (!resource.getClass().equals(location.getClass())) { + return false; + } + String resourcePath; + String locationPath; + if (resource instanceof ClassPathResource) { + resourcePath = ((ClassPathResource) resource).getPath(); + locationPath = ((ClassPathResource) location).getPath(); + } + else if (resource instanceof UrlResource) { + resourcePath = resource.getURL().toExternalForm(); + locationPath = location.getURL().toExternalForm(); + } + else { + resourcePath = resource.getURL().getPath(); + locationPath = location.getURL().getPath(); + } + locationPath = (locationPath.endsWith("/") || locationPath.isEmpty() ? locationPath : locationPath + "/"); + if (!resourcePath.startsWith(locationPath)) { + return false; + } + if (resourcePath.contains("%")) { + // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars + if (URLDecoder.decode(resourcePath, "UTF-8").contains("../")) { + if (logger.isTraceEnabled()) { + logger.trace("Resolved resource path contains \"../\" after decoding: " + resourcePath); + } + return false; + } + } + return true; } }
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java+110 −7 modified@@ -18,8 +18,10 @@ import java.io.IOException; import java.io.InputStream; +import java.net.URLDecoder; import java.util.ArrayList; import java.util.List; + import javax.activation.FileTypeMap; import javax.activation.MimetypesFileTypeMap; import javax.servlet.ServletException; @@ -28,14 +30,15 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.springframework.beans.factory.InitializingBean; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ResourceUtils; import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; import org.springframework.web.HttpRequestHandler; @@ -158,6 +161,29 @@ public void afterPropertiesSet() throws Exception { logger.warn("Locations list is empty. No resources will be served unless a " + "custom ResourceResolver is configured as an alternative to PathResourceResolver."); } + initAllowedLocations(); + } + + /** + * Look for a {@link org.springframework.web.servlet.resource.PathResourceResolver} + * among the {@link #getResourceResolvers() resource resolvers} and configure + * its {@code "allowedLocations"} to match the value of the + * {@link #setLocations(java.util.List) locations} property unless the "allowed + * locations" of the {@code PathResourceResolver} is non-empty. + */ + protected void initAllowedLocations() { + if (CollectionUtils.isEmpty(this.locations)) { + return; + } + for (int i = getResourceResolvers().size()-1; i >= 0; i--) { + if (getResourceResolvers().get(i) instanceof PathResourceResolver) { + PathResourceResolver pathResolver = (PathResourceResolver) getResourceResolvers().get(i); + if (ObjectUtils.isEmpty(pathResolver.getAllowedLocations())) { + pathResolver.setAllowedLocations(getLocations().toArray(new Resource[getLocations().size()])); + } + break; + } + } } /** @@ -214,18 +240,33 @@ public void handleRequest(HttpServletRequest request, HttpServletResponse respon writeContent(response, resource); } - protected Resource getResource(HttpServletRequest request) throws IOException{ + protected Resource getResource(HttpServletRequest request) throws IOException { String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); if (path == null) { throw new IllegalStateException("Required request attribute '" + HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE + "' is not set"); } + path = processPath(path); if (!StringUtils.hasText(path) || isInvalidPath(path)) { if (logger.isTraceEnabled()) { logger.trace("Ignoring invalid resource path [" + path + "]"); } return null; } + if (path.contains("%")) { + try { + // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars + if (isInvalidPath(URLDecoder.decode(path, "UTF-8"))) { + if (logger.isTraceEnabled()) { + logger.trace("Ignoring invalid resource path with escape sequences [" + path + "]."); + } + return null; + } + } + catch (IllegalArgumentException ex) { + // ignore + } + } ResourceResolverChain resolveChain = new DefaultResourceResolverChain(getResourceResolvers()); Resource resource = resolveChain.resolveResource(request, path, getLocations()); if (resource == null || getResourceTransformers().isEmpty()) { @@ -237,14 +278,76 @@ protected Resource getResource(HttpServletRequest request) throws IOException{ } /** - * Validates the given path: returns {@code true} if the given path is not a valid resource path. - * <p>The default implementation rejects paths containing "WEB-INF" or "META-INF" as well as paths - * with relative paths ("../") that result in access of a parent directory. + * Process the given resource path to be used. + * <p>The default implementation replaces any combination of leading '/' and + * control characters (00-1F and 7F) with a single "/" or "". For example + * {@code " // /// //// foo/bar"} becomes {@code "/foo/bar"}. + * @since 3.2.12 + */ + protected String processPath(String path) { + boolean slash = false; + for (int i = 0; i < path.length(); i++) { + if (path.charAt(i) == '/') { + slash = true; + } + else if (path.charAt(i) > ' ' && path.charAt(i) != 127) { + if (i == 0 || (i == 1 && slash)) { + return path; + } + path = slash ? "/" + path.substring(i) : path.substring(i); + if (logger.isTraceEnabled()) { + logger.trace("Path after trimming leading '/' and control characters: " + path); + } + return path; + } + } + return (slash ? "/" : ""); + } + + /** + * Identifies invalid resource paths. By default rejects: + * <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 + * valid URL} or would represent one after the leading slash is removed. + * </ul> + * <p><strong>Note:</strong> this method assumes that leading, duplicate '/' + * or control characters (e.g. white space) have been trimmed so that the + * path starts predictably with a single '/' or does not have one. * @param path the path to validate - * @return {@code true} if the path has been recognized as invalid, {@code false} otherwise + * @return {@code true} if the path is invalid, {@code false} otherwise */ protected boolean isInvalidPath(String path) { - return (path.contains("WEB-INF") || path.contains("META-INF") || StringUtils.cleanPath(path).startsWith("..")); + if (logger.isTraceEnabled()) { + logger.trace("Applying \"invalid path\" checks to path: " + path); + } + if (path.contains("WEB-INF") || path.contains("META-INF")) { + if (logger.isTraceEnabled()) { + logger.trace("Path contains \"WEB-INF\" or \"META-INF\"."); + } + return true; + } + if (path.contains(":/")) { + String relativePath = (path.charAt(0) == '/' ? path.substring(1) : path); + if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) { + if (logger.isTraceEnabled()) { + logger.trace("Path represents URL or has \"url:\" prefix."); + } + return true; + } + } + if (path.contains("../")) { + path = StringUtils.cleanPath(path); + if (path.contains("../")) { + if (logger.isTraceEnabled()) { + logger.trace("Path contains \"../\" after call to StringUtils#cleanPath."); + } + return true; + } + } + return false; } /**
spring-webmvc/src/test/java/org/springframework/web/servlet/resource/AppCacheManifestTransformerTests.java+7 −6 modified@@ -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 void syntaxErrorInManifest() throws Exception { @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<>();
spring-webmvc/src/test/java/org/springframework/web/servlet/resource/CssLinkResourceTransformerTests.java+6 −3 modified@@ -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);
spring-webmvc/src/test/java/org/springframework/web/servlet/resource/PathResourceResolverTests.java+73 −17 modified@@ -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() { + this.resolver = new PathResourceResolver(); + } - List<ResourceResolver> resolvers = new ArrayList<>(); - resolvers.add(new PathResourceResolver()); - this.chain = new DefaultResourceResolverChain(resolvers); + @Test + 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); + } + + @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); + } - this.locations = new ArrayList<>(); - this.locations.add(new ClassPathResource("test/", getClass())); + @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 resolveResourceInternal() { - String file = "bar.css"; - Resource expected = new ClassPathResource("test/" + file, getClass()); - Resource actual = this.chain.resolveResource(null, file, this.locations); + 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); + } - assertEquals(expected, 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); } }
spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java+97 −15 modified@@ -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 void getResourceFromSubDirectoryOfAlternatePath() throws Exception { } @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 { + + 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); + } - this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "test/../../testsecret/secret.txt"); + @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 @@ private long resourceLastModified(String resourceName) throws IOException { 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 {
spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceTransformerSupportTests.java+5 −3 modified@@ -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();
9beae9ae4226Apply extra checks to static resource handling
2 files changed · +214 −28
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java+136 −11 modified@@ -18,7 +18,10 @@ import java.io.IOException; import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; import java.util.List; + import javax.activation.FileTypeMap; import javax.activation.MimetypesFileTypeMap; import javax.servlet.ServletException; @@ -27,14 +30,15 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.springframework.beans.factory.InitializingBean; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; import org.springframework.http.MediaType; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +import org.springframework.util.ResourceUtils; import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; import org.springframework.web.HttpRequestHandler; @@ -161,25 +165,50 @@ protected Resource getResource(HttpServletRequest request) { throw new IllegalStateException("Required request attribute '" + HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE + "' is not set"); } - + path = processPath(path); if (!StringUtils.hasText(path) || isInvalidPath(path)) { if (logger.isDebugEnabled()) { logger.debug("Ignoring invalid resource path [" + path + "]"); } return null; } - + if (path.contains("%")) { + try { + // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars + if (isInvalidPath(URLDecoder.decode(path, "UTF-8"))) { + if (logger.isTraceEnabled()) { + logger.trace("Ignoring invalid resource path with escape sequences [" + path + "]."); + } + return null; + } + } + catch (UnsupportedEncodingException e) { + // ignore: shouldn't happen + } + catch (IllegalArgumentException ex) { + // ignore + } + } for (Resource location : this.locations) { try { if (logger.isDebugEnabled()) { logger.debug("Trying relative path [" + path + "] against base location: " + location); } Resource resource = location.createRelative(path); if (resource.exists() && resource.isReadable()) { - if (logger.isDebugEnabled()) { - logger.debug("Found matching resource: " + resource); + if (isResourceUnderLocation(resource, location)) { + if (logger.isDebugEnabled()) { + logger.debug("Found matching resource: " + resource); + } + return resource; + } + else { + if (logger.isTraceEnabled()) { + logger.trace("resource=\"" + resource + "\" was successfully resolved " + + "but is not under the location=\"" + location); + } + return null; } - return resource; } else if (logger.isTraceEnabled()) { logger.trace("Relative resource doesn't exist or isn't readable: " + resource); @@ -193,14 +222,110 @@ else if (logger.isTraceEnabled()) { } /** - * Validates the given path: returns {@code true} if the given path is not a valid resource path. - * <p>The default implementation rejects paths containing "WEB-INF" or "META-INF" as well as paths - * with relative paths ("../") that result in access of a parent directory. + * Process the given resource path to be used. + * <p>The default implementation replaces any combination of leading '/' and + * control characters (00-1F and 7F) with a single "/" or "". For example + * {@code " // /// //// foo/bar"} becomes {@code "/foo/bar"}. + * @since 3.2.12 + */ + protected String processPath(String path) { + boolean slash = false; + for (int i = 0; i < path.length(); i++) { + if (path.charAt(i) == '/') { + slash = true; + } + else if (path.charAt(i) > ' ' && path.charAt(i) != 127) { + if (i == 0 || (i == 1 && slash)) { + return path; + } + path = slash ? "/" + path.substring(i) : path.substring(i); + if (logger.isTraceEnabled()) { + logger.trace("Path trimmed for leading '/' and control characters: " + path); + } + return path; + } + } + return (slash ? "/" : ""); + } + + /** + * Identifies invalid resource paths. By default rejects: + * <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 + * valid URL} or would represent one after the leading slash is removed. + * </ul> + * <p><strong>Note:</strong> this method assumes that leading, duplicate '/' + * or control characters (e.g. white space) have been trimmed so that the + * path starts predictably with a single '/' or does not have one. * @param path the path to validate - * @return {@code true} if the path has been recognized as invalid, {@code false} otherwise + * @return {@code true} if the path is invalid, {@code false} otherwise */ protected boolean isInvalidPath(String path) { - return (path.contains("WEB-INF") || path.contains("META-INF") || StringUtils.cleanPath(path).startsWith("..")); + if (logger.isTraceEnabled()) { + logger.trace("Applying \"invalid path\" checks to path: " + path); + } + if (path.contains("WEB-INF") || path.contains("META-INF")) { + if (logger.isTraceEnabled()) { + logger.trace("Path contains \"WEB-INF\" or \"META-INF\"."); + } + return true; + } + if (path.contains(":/")) { + String relativePath = (path.charAt(0) == '/' ? path.substring(1) : path); + if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) { + if (logger.isTraceEnabled()) { + logger.trace("Path represents URL or has \"url:\" prefix."); + } + return true; + } + } + if (path.contains("../")) { + path = StringUtils.cleanPath(path); + if (path.contains("../")) { + if (logger.isTraceEnabled()) { + logger.trace("Path contains \"../\" after call to StringUtils#cleanPath."); + } + return true; + } + } + return false; + } + + private boolean isResourceUnderLocation(Resource resource, Resource location) throws IOException { + if (!resource.getClass().equals(location.getClass())) { + return false; + } + String resourcePath; + String locationPath; + if (resource instanceof ClassPathResource) { + resourcePath = ((ClassPathResource) resource).getPath(); + locationPath = ((ClassPathResource) location).getPath(); + } + else if (resource instanceof UrlResource) { + resourcePath = resource.getURL().toExternalForm(); + locationPath = location.getURL().toExternalForm(); + } + else { + resourcePath = resource.getURL().getPath(); + locationPath = location.getURL().getPath(); + } + locationPath = (locationPath.endsWith("/") || locationPath.isEmpty() ? locationPath : locationPath + "/"); + if (!resourcePath.startsWith(locationPath)) { + return false; + } + if (resourcePath.contains("%")) { + // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars + if (URLDecoder.decode(resourcePath, "UTF-8").contains("../")) { + if (logger.isTraceEnabled()) { + logger.trace("Resolved resource path contains \"../\" after decoding: " + resourcePath); + } + return false; + } + } + return true; } /**
spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java+78 −17 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * 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. @@ -16,6 +16,10 @@ 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.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -26,14 +30,13 @@ 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.*; - /** * @author Keith Donald * @author Jeremy Grelle @@ -124,28 +127,76 @@ public void getResourceFromSubDirectoryOfAlternatePath() throws Exception { assertEquals("function foo() { console.log(\"hello world\"); }", response.getContentAsString()); } + @Test - public void getResourceViaDirectoryTraversal() throws Exception { + public void invalidPath() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest(); request.setMethod("GET"); - - request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "../testsecret/secret.txt"); MockHttpServletResponse response = new MockHttpServletResponse(); - handler.handleRequest(request, response); - assertEquals(404, response.getStatus()); - 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", request, response); + testInvalidPath(location, "test/../../testsecret/secret.txt", request, response); + testInvalidPath(location, ":/../../testsecret/secret.txt", request, response); + + 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, request, response); + testInvalidPath(location, "/file:" + secretPath, request, response); + testInvalidPath(location, "url:" + secretPath, request, response); + testInvalidPath(location, "/url:" + secretPath, request, response); + testInvalidPath(location, "/" + secretPath, request, response); + testInvalidPath(location, "////../.." + secretPath, request, response); + testInvalidPath(location, "/%2E%2E/testsecret/secret.txt", request, response); + testInvalidPath(location, "/%2e%2e/testsecret/secret.txt", request, response); + testInvalidPath(location, " " + secretPath, request, response); + testInvalidPath(location, "/ " + secretPath, request, response); + testInvalidPath(location, "url:" + secretPath, request, response); + } + + @Test + public void ignoreInvalidEscapeSequence() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + MockHttpServletResponse response = new MockHttpServletResponse(); + request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/%foo%/bar.txt"); response = new MockHttpServletResponse(); - handler.handleRequest(request, response); + this.handler.handleRequest(request, response); assertEquals(404, response.getStatus()); + } - handler.setLocations(Arrays.<Resource>asList(new ClassPathResource("testsecret/", getClass()))); - request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "secret.txt"); - response = new MockHttpServletResponse(); - handler.handleRequest(request, response); - assertEquals(200, response.getStatus()); - assertEquals("text/plain", response.getContentType()); - assertEquals("big secret", 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 ot empty path + assertEquals("", this.handler.processPath(" ")); + assertEquals("/", this.handler.processPath("/")); + assertEquals("/", this.handler.processPath("///")); + assertEquals("/", this.handler.processPath("/ / / ")); } @Test @@ -219,6 +270,16 @@ public void resourceNotFound() throws Exception { assertEquals(404, response.getStatus()); } + private void testInvalidPath(Resource location, String requestPath, + MockHttpServletRequest request, MockHttpServletResponse response) throws Exception { + + request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, requestPath); + response = new MockHttpServletResponse(); + this.handler.handleRequest(request, response); + assertTrue(location.createRelative(requestPath).exists()); + assertEquals(404, response.getStatus()); + } + private static class TestServletContext extends MockServletContext {
3f68cd633f03Apply extra checks to static resource handling
2 files changed · +214 −28
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java+136 −11 modified@@ -18,7 +18,10 @@ import java.io.IOException; import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; import java.util.List; + import javax.activation.FileTypeMap; import javax.activation.MimetypesFileTypeMap; import javax.servlet.ServletException; @@ -27,14 +30,15 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.springframework.beans.factory.InitializingBean; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; import org.springframework.http.MediaType; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +import org.springframework.util.ResourceUtils; import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; import org.springframework.web.HttpRequestHandler; @@ -159,25 +163,50 @@ protected Resource getResource(HttpServletRequest request) { throw new IllegalStateException("Required request attribute '" + HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE + "' is not set"); } - + path = processPath(path); if (!StringUtils.hasText(path) || isInvalidPath(path)) { if (logger.isDebugEnabled()) { logger.debug("Ignoring invalid resource path [" + path + "]"); } return null; } - + if (path.contains("%")) { + try { + // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars + if (isInvalidPath(URLDecoder.decode(path, "UTF-8"))) { + if (logger.isTraceEnabled()) { + logger.trace("Ignoring invalid resource path with escape sequences [" + path + "]."); + } + return null; + } + } + catch (UnsupportedEncodingException e) { + // ignore: shouldn't happen + } + catch (IllegalArgumentException ex) { + // ignore + } + } for (Resource location : this.locations) { try { if (logger.isDebugEnabled()) { logger.debug("Trying relative path [" + path + "] against base location: " + location); } Resource resource = location.createRelative(path); if (resource.exists() && resource.isReadable()) { - if (logger.isDebugEnabled()) { - logger.debug("Found matching resource: " + resource); + if (isResourceUnderLocation(resource, location)) { + if (logger.isDebugEnabled()) { + logger.debug("Found matching resource: " + resource); + } + return resource; + } + else { + if (logger.isTraceEnabled()) { + logger.trace("resource=\"" + resource + "\" was successfully resolved " + + "but is not under the location=\"" + location); + } + return null; } - return resource; } else if (logger.isTraceEnabled()) { logger.trace("Relative resource doesn't exist or isn't readable: " + resource); @@ -191,14 +220,110 @@ else if (logger.isTraceEnabled()) { } /** - * Validates the given path: returns {@code true} if the given path is not a valid resource path. - * <p>The default implementation rejects paths containing "WEB-INF" or "META-INF" as well as paths - * with relative paths ("../") that result in access of a parent directory. + * Process the given resource path to be used. + * <p>The default implementation replaces any combination of leading '/' and + * control characters (00-1F and 7F) with a single "/" or "". For example + * {@code " // /// //// foo/bar"} becomes {@code "/foo/bar"}. + * @since 3.2.12 + */ + protected String processPath(String path) { + boolean slash = false; + for (int i = 0; i < path.length(); i++) { + if (path.charAt(i) == '/') { + slash = true; + } + else if (path.charAt(i) > ' ' && path.charAt(i) != 127) { + if (i == 0 || (i == 1 && slash)) { + return path; + } + path = slash ? "/" + path.substring(i) : path.substring(i); + if (logger.isTraceEnabled()) { + logger.trace("Path trimmed for leading '/' and control characters: " + path); + } + return path; + } + } + return (slash ? "/" : ""); + } + + /** + * Identifies invalid resource paths. By default rejects: + * <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 + * valid URL} or would represent one after the leading slash is removed. + * </ul> + * <p><strong>Note:</strong> this method assumes that leading, duplicate '/' + * or control characters (e.g. white space) have been trimmed so that the + * path starts predictably with a single '/' or does not have one. * @param path the path to validate - * @return {@code true} if the path has been recognized as invalid, {@code false} otherwise + * @return {@code true} if the path is invalid, {@code false} otherwise */ protected boolean isInvalidPath(String path) { - return (path.contains("WEB-INF") || path.contains("META-INF") || StringUtils.cleanPath(path).startsWith("..")); + if (logger.isTraceEnabled()) { + logger.trace("Applying \"invalid path\" checks to path: " + path); + } + if (path.contains("WEB-INF") || path.contains("META-INF")) { + if (logger.isTraceEnabled()) { + logger.trace("Path contains \"WEB-INF\" or \"META-INF\"."); + } + return true; + } + if (path.contains(":/")) { + String relativePath = (path.charAt(0) == '/' ? path.substring(1) : path); + if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) { + if (logger.isTraceEnabled()) { + logger.trace("Path represents URL or has \"url:\" prefix."); + } + return true; + } + } + if (path.contains("../")) { + path = StringUtils.cleanPath(path); + if (path.contains("../")) { + if (logger.isTraceEnabled()) { + logger.trace("Path contains \"../\" after call to StringUtils#cleanPath."); + } + return true; + } + } + return false; + } + + private boolean isResourceUnderLocation(Resource resource, Resource location) throws IOException { + if (!resource.getClass().equals(location.getClass())) { + return false; + } + String resourcePath; + String locationPath; + if (resource instanceof ClassPathResource) { + resourcePath = ((ClassPathResource) resource).getPath(); + locationPath = ((ClassPathResource) location).getPath(); + } + else if (resource instanceof UrlResource) { + resourcePath = resource.getURL().toExternalForm(); + locationPath = location.getURL().toExternalForm(); + } + else { + resourcePath = resource.getURL().getPath(); + locationPath = location.getURL().getPath(); + } + locationPath = (locationPath.endsWith("/") || locationPath.isEmpty() ? locationPath : locationPath + "/"); + if (!resourcePath.startsWith(locationPath)) { + return false; + } + if (resourcePath.contains("%")) { + // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars + if (URLDecoder.decode(resourcePath, "UTF-8").contains("../")) { + if (logger.isTraceEnabled()) { + logger.trace("Resolved resource path contains \"../\" after decoding: " + resourcePath); + } + return false; + } + } + return true; } /**
spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java+78 −17 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * 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. @@ -16,6 +16,10 @@ 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.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -26,14 +30,13 @@ 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.*; - /** * @author Keith Donald * @author Jeremy Grelle @@ -124,28 +127,76 @@ public void getResourceFromSubDirectoryOfAlternatePath() throws Exception { assertEquals("function foo() { console.log(\"hello world\"); }", response.getContentAsString()); } + @Test - public void getResourceViaDirectoryTraversal() throws Exception { + public void invalidPath() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest(); request.setMethod("GET"); - - request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "../testsecret/secret.txt"); MockHttpServletResponse response = new MockHttpServletResponse(); - handler.handleRequest(request, response); - assertEquals(404, response.getStatus()); - 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", request, response); + testInvalidPath(location, "test/../../testsecret/secret.txt", request, response); + testInvalidPath(location, ":/../../testsecret/secret.txt", request, response); + + 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, request, response); + testInvalidPath(location, "/file:" + secretPath, request, response); + testInvalidPath(location, "url:" + secretPath, request, response); + testInvalidPath(location, "/url:" + secretPath, request, response); + testInvalidPath(location, "/" + secretPath, request, response); + testInvalidPath(location, "////../.." + secretPath, request, response); + testInvalidPath(location, "/%2E%2E/testsecret/secret.txt", request, response); + testInvalidPath(location, "/%2e%2e/testsecret/secret.txt", request, response); + testInvalidPath(location, " " + secretPath, request, response); + testInvalidPath(location, "/ " + secretPath, request, response); + testInvalidPath(location, "url:" + secretPath, request, response); + } + + @Test + public void ignoreInvalidEscapeSequence() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("GET"); + MockHttpServletResponse response = new MockHttpServletResponse(); + request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/%foo%/bar.txt"); response = new MockHttpServletResponse(); - handler.handleRequest(request, response); + this.handler.handleRequest(request, response); assertEquals(404, response.getStatus()); + } - handler.setLocations(Arrays.<Resource>asList(new ClassPathResource("testsecret/", getClass()))); - request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "secret.txt"); - response = new MockHttpServletResponse(); - handler.handleRequest(request, response); - assertEquals(200, response.getStatus()); - assertEquals("text/plain", response.getContentType()); - assertEquals("big secret", 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 ot empty path + assertEquals("", this.handler.processPath(" ")); + assertEquals("/", this.handler.processPath("/")); + assertEquals("/", this.handler.processPath("///")); + assertEquals("/", this.handler.processPath("/ / / ")); } @Test @@ -219,6 +270,16 @@ public void resourceNotFound() throws Exception { assertEquals(404, response.getStatus()); } + private void testInvalidPath(Resource location, String requestPath, + MockHttpServletRequest request, MockHttpServletResponse response) throws Exception { + + request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, requestPath); + response = new MockHttpServletResponse(); + this.handler.handleRequest(request, response); + assertTrue(location.createRelative(requestPath).exists()); + assertEquals(404, response.getStatus()); + } + private static class TestServletContext extends MockServletContext {
Vulnerability mechanics
Synthesis attempt was rejected by the grounding validator. Re-run pending.
References
11- rhn.redhat.com/errata/RHSA-2015-0236.htmlnvdThird Party AdvisoryWEB
- rhn.redhat.com/errata/RHSA-2015-0720.htmlnvdThird Party AdvisoryWEB
- www.pivotal.io/security/cve-2014-3625nvdVendor AdvisoryWEB
- github.com/advisories/GHSA-hhm4-hwq6-3c6wghsaADVISORY
- jira.spring.io/browse/SPR-12354nvdThird Party AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2014-3625ghsaADVISORY
- github.com/spring-projects/spring-framework/commit/161d3e3049f129e211f68a4e94b544e0f0d8384dghsaWEB
- github.com/spring-projects/spring-framework/commit/3f68cd633f03370d33c2603a6496e81273782601ghsaWEB
- github.com/spring-projects/spring-framework/commit/9beae9ae4226c45cd428035dae81214439324676ghsaWEB
- github.com/spring-projects/spring-framework/commit/9cef8e3001ddd61c734281a7556efd84b6cc2755ghsaWEB
- lists.debian.org/debian-lts-announce/2019/07/msg00012.htmlnvdWEB
News mentions
0No linked articles in our index yet.