CVE-2018-1271
Description
Spring Framework, versions 5.0 prior to 5.0.5 and versions 4.3 prior to 4.3.15 and older unsupported versions, allow applications to configure Spring MVC to serve static resources (e.g. CSS, JS, images). When static resources are served from a file system on Windows (as opposed to the classpath, or the ServletContext), a malicious user can send a request using a specially crafted URL that can lead a directory traversal attack.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Spring Framework on Windows allows directory traversal via crafted URLs when static resources are served from a file system.
Vulnerability
Spring Framework versions 5.0 prior to 5.0.5, 4.3 prior to 4.3.15, and older unsupported versions allow applications to configure Spring MVC to serve static resources from a file system on Windows. A path traversal vulnerability exists in the resource handling code, enabling an attacker to request URLs with encoded path separators to access files outside the intended resource directory [1] [2].
Exploitation
An attacker with network access to the application can send a specially crafted HTTP request containing path traversal sequences (e.g., ../ or encoded variations) in the URL for static resources. The vulnerability is specific to Windows platforms where static resources are served from a file system location (not from the classpath or ServletContext). No authentication is required if the resource handler is publicly exposed [1] [2] [4].
Impact
Successful exploitation allows an attacker to read arbitrary files on the Windows file system outside the configured static resource directory, leading to information disclosure of sensitive data such as configuration files, source code, or credentials [1] [2].
Mitigation
Spring Framework released fixed versions: 5.0.5 and 4.3.15, which include proper path normalization and validation in ResourceHttpRequestHandler [2] [4]. Users should upgrade to these versions or later. Red Hat Fuse 7.1 includes the fix for affected Red Hat products [3]. For older unsupported versions, upgrading to a supported branch is necessary.
AI Insight generated on May 22, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.springframework:spring-coreMaven | >= 5.0.0, < 5.0.5 | 5.0.5 |
org.springframework:spring-coreMaven | < 4.3.15 | 4.3.15 |
Affected products
2- Spring by Pivotal/Spring Frameworkv5Range: Versions prior to 5.0.5 and 4.3.15
Patches
8695bf2961fefConsistent trace logging in PathResourceResolver
2 files changed · +10 −10
spring-webflux/src/main/java/org/springframework/web/reactive/resource/PathResourceResolver.java+5 −5 modified@@ -118,10 +118,10 @@ protected Mono<Resource> getResource(String resourcePath, Resource location) { } else if (logger.isTraceEnabled()) { Resource[] allowedLocations = getAllowedLocations(); - logger.trace("Resource path=\"" + resourcePath + "\" was successfully resolved " + - "but resource=\"" + resource.getURL() + "\" is neither under the " + - "current location=\"" + location.getURL() + "\" nor under any of the " + - "allowed locations=" + (allowedLocations != null ? Arrays.asList(allowedLocations) : "[]")); + logger.trace("Resource path \"" + resourcePath + "\" was successfully resolved " + + "but resource \"" + resource.getURL() + "\" is neither under the " + + "current location \"" + location.getURL() + "\" nor under any of the " + + "allowed locations " + (allowedLocations != null ? Arrays.asList(allowedLocations) : "[]")); } } else if (logger.isTraceEnabled()) { @@ -195,7 +195,7 @@ private boolean isInvalidEncodedPath(String resourcePath) { String decodedPath = URLDecoder.decode(resourcePath, "UTF-8"); if (decodedPath.contains("../") || decodedPath.contains("..\\")) { if (logger.isTraceEnabled()) { - logger.trace("Ignoring invalid resource path with escape sequences [" + resourcePath + "]"); + logger.trace("Resolved resource path contains encoded \"../\" or \"..\\\": " + resourcePath); } return true; }
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java+5 −5 modified@@ -189,10 +189,10 @@ protected Resource getResource(String resourcePath, Resource location) throws IO } else if (logger.isTraceEnabled()) { Resource[] allowedLocations = getAllowedLocations(); - logger.trace("Resource path=\"" + resourcePath + "\" was successfully resolved " + - "but resource=\"" + resource.getURL() + "\" is neither under the " + - "current location=\"" + location.getURL() + "\" nor under any of the " + - "allowed locations=" + (allowedLocations != null ? Arrays.asList(allowedLocations) : "[]")); + logger.trace("Resource path \"" + resourcePath + "\" was successfully resolved " + + "but resource \"" + resource.getURL() + "\" is neither under the " + + "current location \"" + location.getURL() + "\" nor under any of the " + + "allowed locations " + (allowedLocations != null ? Arrays.asList(allowedLocations) : "[]")); } } return null; @@ -286,7 +286,7 @@ private boolean isInvalidEncodedPath(String resourcePath) { String decodedPath = URLDecoder.decode(resourcePath, "UTF-8"); if (decodedPath.contains("../") || decodedPath.contains("..\\")) { if (logger.isTraceEnabled()) { - logger.trace("Ignoring invalid resource path with escape sequences [" + resourcePath + "]"); + logger.trace("Resolved resource path contains encoded \"../\" or \"..\\\": " + resourcePath); } return true; }
f046a066eceeSimplified separator check within isInvalidEncodedPath
2 files changed · +14 −19
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java+3 −7 modified@@ -285,13 +285,9 @@ private boolean isInvalidEncodedPath(String resourcePath) { // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars... try { String decodedPath = URLDecoder.decode(resourcePath, "UTF-8"); - int separatorIndex = decodedPath.indexOf("..") + 2; - if (separatorIndex > 1 && separatorIndex < decodedPath.length()) { - char separator = decodedPath.charAt(separatorIndex); - if (separator == '/' || separator == '\\') { - if (logger.isTraceEnabled()) { - logger.trace("Resolved resource path contains \"../\" after decoding: " + resourcePath); - } + if (decodedPath.contains("../") || decodedPath.contains("..\\")) { + if (logger.isTraceEnabled()) { + logger.trace("Ignoring invalid resource path with escape sequences [" + resourcePath + "]"); } return true; }
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java+11 −12 modified@@ -69,24 +69,23 @@ * according to the guidelines of Page Speed, YSlow, etc. * * <p>The {@linkplain #setLocations "locations"} property takes a list of Spring - * {@link Resource} locations from which static resources are allowed to - * be served by this handler. Resources could be served from a classpath location, - * e.g. "classpath:/META-INF/public-web-resources/", allowing convenient packaging + * {@link Resource} locations from which static resources are allowed to be served + * by this handler. Resources could be served from a classpath location, e.g. + * "classpath:/META-INF/public-web-resources/", allowing convenient packaging * and serving of resources such as .js, .css, and others in jar files. * * <p>This request handler may also be configured with a * {@link #setResourceResolvers(List) resourcesResolver} and * {@link #setResourceTransformers(List) resourceTransformer} chains to support - * arbitrary resolution and transformation of resources being served. By default a - * {@link PathResourceResolver} simply finds resources based on the configured - * "locations". An application can configure additional resolvers and - * transformers such as the {@link VersionResourceResolver} which can resolve - * and prepare URLs for resources with a version in the URL. + * arbitrary resolution and transformation of resources being served. By default + * a {@link PathResourceResolver} simply finds resources based on the configured + * "locations". An application can configure additional resolvers and transformers + * such as the {@link VersionResourceResolver} which can resolve and prepare URLs + * for resources with a version in the URL. * - * <p>This handler also properly evaluates the {@code Last-Modified} header (if - * present) so that a {@code 304} status code will be returned as appropriate, - * avoiding unnecessary overhead for resources that are already cached by the - * client. + * <p>This handler also properly evaluates the {@code Last-Modified} header + * (if present) so that a {@code 304} status code will be returned as appropriate, + * avoiding unnecessary overhead for resources that are already cached by the client. * * @author Keith Donald * @author Jeremy Grelle
98ad23bef8e2Consistent logging of encoded path evaluation failure
2 files changed · +12 −2
spring-webflux/src/main/java/org/springframework/web/reactive/resource/PathResourceResolver.java+6 −1 modified@@ -193,7 +193,12 @@ private boolean isInvalidEncodedPath(String resourcePath) { // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars... try { String decodedPath = URLDecoder.decode(resourcePath, "UTF-8"); - return (decodedPath.contains("../") || decodedPath.contains("..\\")); + if (decodedPath.contains("../") || decodedPath.contains("..\\")) { + if (logger.isTraceEnabled()) { + logger.trace("Ignoring invalid resource path with escape sequences [" + resourcePath + "]"); + } + return true; + } } catch (UnsupportedEncodingException ex) { // Should never happen...
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java+6 −1 modified@@ -284,7 +284,12 @@ private boolean isInvalidEncodedPath(String resourcePath) { // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars... try { String decodedPath = URLDecoder.decode(resourcePath, "UTF-8"); - return (decodedPath.contains("../") || decodedPath.contains("..\\")); + if (decodedPath.contains("../") || decodedPath.contains("..\\")) { + if (logger.isTraceEnabled()) { + logger.trace("Ignoring invalid resource path with escape sequences [" + resourcePath + "]"); + } + return true; + } } catch (UnsupportedEncodingException ex) { // Should never happen...
13356a7ee224Consistent encoded path evaluation in reactive ResourceWebHandler and co
2 files changed · +42 −37
spring-webflux/src/main/java/org/springframework/web/reactive/resource/PathResourceResolver.java+12 −11 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -17,6 +17,7 @@ package org.springframework.web.reactive.resource; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.Arrays; import java.util.List; @@ -184,21 +185,21 @@ else if (resource instanceof ClassPathResource) { return true; } locationPath = (locationPath.endsWith("/") || locationPath.isEmpty() ? locationPath : locationPath + "/"); - if (!resourcePath.startsWith(locationPath)) { - return false; - } + return (resourcePath.startsWith(locationPath) && !isInvalidEncodedPath(resourcePath)); + } + private boolean isInvalidEncodedPath(String resourcePath) { 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; + try { + String decodedPath = URLDecoder.decode(resourcePath, "UTF-8"); + return (decodedPath.contains("../") || decodedPath.contains("..\\")); + } + catch (UnsupportedEncodingException ex) { + // Should never happen... } } - - return true; + return false; } }
spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java+30 −26 modified@@ -28,7 +28,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import reactor.core.Exceptions; import reactor.core.publisher.Mono; import org.springframework.beans.factory.InitializingBean; @@ -314,41 +313,21 @@ public Mono<Void> handle(ServerWebExchange exchange) { } protected Mono<Resource> getResource(ServerWebExchange exchange) { - String name = HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE; PathContainer pathWithinHandler = exchange.getRequiredAttribute(name); + String path = processPath(pathWithinHandler.value()); if (!StringUtils.hasText(path) || isInvalidPath(path)) { if (logger.isTraceEnabled()) { logger.trace("Ignoring invalid resource path [" + path + "]"); } return Mono.empty(); } - - if (path.contains("%")) { - try { - // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars - String decodedPath = URLDecoder.decode(path, "UTF-8"); - if (isInvalidPath(decodedPath)) { - if (logger.isTraceEnabled()) { - logger.trace("Ignoring invalid resource path with escape sequences [" + path + "]."); - } - return Mono.empty(); - } - decodedPath = processPath(decodedPath); - if (isInvalidPath(decodedPath)) { - if (logger.isTraceEnabled()) { - logger.trace("Ignoring invalid resource path with escape sequences [" + path + "]."); - } - return Mono.empty(); - } - } - catch (IllegalArgumentException ex) { - // ignore - } - catch (UnsupportedEncodingException ex) { - return Mono.error(Exceptions.propagate(ex)); + if (isInvalidEncodedPath(path)) { + if (logger.isTraceEnabled()) { + logger.trace("Ignoring invalid resource path with escape sequences [" + path + "]"); } + return Mono.empty(); } ResourceResolverChain resolveChain = createResolverChain(); @@ -420,6 +399,31 @@ else if (path.charAt(i) > ' ' && path.charAt(i) != 127) { return (slash ? "/" : ""); } + /** + * Check whether the given path contains invalid escape sequences. + * @param path the path to validate + * @return {@code true} if the path is invalid, {@code false} otherwise + */ + private boolean isInvalidEncodedPath(String path) { + if (path.contains("%")) { + try { + // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars + String decodedPath = URLDecoder.decode(path, "UTF-8"); + if (isInvalidPath(decodedPath)) { + return true; + } + decodedPath = processPath(decodedPath); + if (isInvalidPath(decodedPath)) { + return true; + } + } + catch (IllegalArgumentException | UnsupportedEncodingException ex) { + // Should never happen... + } + } + return false; + } + /** * Identifies invalid resource paths. By default rejects: * <ul>
f59ea610dfcfSimplified separator check within isInvalidEncodedPath
1 file changed · +1 −10
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java+1 −10 modified@@ -284,16 +284,7 @@ private boolean isInvalidEncodedPath(String resourcePath) { // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars... try { String decodedPath = URLDecoder.decode(resourcePath, "UTF-8"); - int separatorIndex = decodedPath.indexOf("..") + 2; - if (separatorIndex > 1 && separatorIndex < decodedPath.length()) { - char separator = decodedPath.charAt(separatorIndex); - if (separator == '/' || separator == '\\') { - if (logger.isTraceEnabled()) { - logger.trace("Resolved resource path contains \"../\" after decoding: " + resourcePath); - } - } - return true; - } + return (decodedPath.contains("../") || decodedPath.contains("..\\")); } catch (UnsupportedEncodingException ex) { // Should never happen...
b9ebdaaf3710Backport clean duplicate separators in resource URLs
3 files changed · +168 −66
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java+26 −18 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -245,21 +245,7 @@ else if (resource instanceof ServletContextResource) { return true; } 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; + return (resourcePath.startsWith(locationPath) && !isInvalidEncodedPath(resourcePath)); } private String encodeIfNecessary(String path, HttpServletRequest request, Resource location) { @@ -291,8 +277,30 @@ private String encodeIfNecessary(String path, HttpServletRequest request, Resour } private boolean shouldEncodeRelativePath(Resource location) { - return location instanceof UrlResource && - this.urlPathHelper != null && this.urlPathHelper.isUrlDecode(); + return (location instanceof UrlResource && this.urlPathHelper != null && this.urlPathHelper.isUrlDecode()); + } + + private boolean isInvalidEncodedPath(String resourcePath) { + if (resourcePath.contains("%")) { + // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars... + try { + String decodedPath = URLDecoder.decode(resourcePath, "UTF-8"); + int separatorIndex = decodedPath.indexOf("..") + 2; + if (separatorIndex > 1 && separatorIndex < decodedPath.length()) { + char separator = decodedPath.charAt(separatorIndex); + if (separator == '/' || separator == '\\') { + if (logger.isTraceEnabled()) { + logger.trace("Resolved resource path contains \"../\" after decoding: " + resourcePath); + } + } + return true; + } + } + catch (UnsupportedEncodingException ex) { + // Should never happen... + } + } + return false; } }
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java+81 −31 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -17,6 +17,7 @@ package org.springframework.web.servlet.resource; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.nio.charset.Charset; import java.util.ArrayList; @@ -507,46 +508,75 @@ protected Resource getResource(HttpServletRequest request) throws IOException { 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 + if (isInvalidEncodedPath(path)) { + if (logger.isTraceEnabled()) { + logger.trace("Ignoring invalid resource path with escape sequences [" + path + "]"); } + return null; } + ResourceResolverChain resolveChain = new DefaultResourceResolverChain(getResourceResolvers()); Resource resource = resolveChain.resolveResource(request, path, getLocations()); if (resource == null || getResourceTransformers().isEmpty()) { return resource; } + ResourceTransformerChain transformChain = new DefaultResourceTransformerChain(resolveChain, getResourceTransformers()); resource = transformChain.transform(request, resource); return resource; } /** - * 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"}. + * Process the given resource path. + * <p>The default implementation replaces: + * <ul> + * <li>Backslash with forward slash. + * <li>Duplicate occurrences of slash with a single slash. + * <li>Any combination of leading slash and control characters (00-1F and 7F) + * with a single "/" or "". For example {@code " / // foo/bar"} + * becomes {@code "/foo/bar"}. + * </ul> * @since 3.2.12 */ protected String processPath(String path) { + path = StringUtils.replace(path, "\\", "/"); + path = cleanDuplicateSlashes(path); + return cleanLeadingSlash(path); + } + + private String cleanDuplicateSlashes(String path) { + StringBuilder sb = null; + char prev = 0; + for (int i = 0; i < path.length(); i++) { + char curr = path.charAt(i); + try { + if ((curr == '/') && (prev == '/')) { + if (sb == null) { + sb = new StringBuilder(path.substring(0, i)); + } + continue; + } + if (sb != null) { + sb.append(path.charAt(i)); + } + } + finally { + prev = curr; + } + } + return sb != null ? sb.toString() : path; + } + + private String cleanLeadingSlash(String path) { boolean slash = false; for (int i = 0; i < path.length(); i++) { if (path.charAt(i) == '/') { @@ -556,16 +586,44 @@ 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); + path = (slash ? "/" + path.substring(i) : path.substring(i)); if (logger.isTraceEnabled()) { - logger.trace("Path after trimming leading '/' and control characters: " + path); + logger.trace("Path after trimming leading '/' and control characters: [" + path + "]"); } return path; } } return (slash ? "/" : ""); } + /** + * Check whether the given path contains invalid escape sequences. + * @param path the path to validate + * @return {@code true} if the path is invalid, {@code false} otherwise + */ + private boolean isInvalidEncodedPath(String path) { + if (path.contains("%")) { + try { + // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars + String decodedPath = URLDecoder.decode(path, "UTF-8"); + if (isInvalidPath(decodedPath)) { + return true; + } + decodedPath = processPath(decodedPath); + if (isInvalidPath(decodedPath)) { + return true; + } + } + catch (IllegalArgumentException ex) { + // Should never happen... + } + catch (UnsupportedEncodingException ex) { + // Should never happen... + } + } + return false; + } + /** * Identifies invalid resource paths. By default rejects: * <ul> @@ -580,32 +638,24 @@ else if (path.charAt(i) > ' ' && path.charAt(i) != 127) { * path starts predictably with a single '/' or does not have one. * @param path the path to validate * @return {@code true} if the path is invalid, {@code false} otherwise + * @since 3.0.6 */ protected boolean isInvalidPath(String path) { - 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\"."); - } + 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."); - } + 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."); - } + logger.trace("Path contains \"../\" after call to StringUtils#cleanPath."); return true; } }
spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java+61 −17 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -46,6 +46,7 @@ import org.springframework.web.servlet.HandlerMapping; import static org.junit.Assert.*; +import static org.mockito.Mockito.*; /** * Unit tests for {@link ResourceHttpRequestHandler}. @@ -310,41 +311,84 @@ public String getVirtualServerName() { } @Test - public void invalidPath() throws Exception { + public void testInvalidPath() throws Exception { + + // Use mock ResourceResolver: i.e. we're only testing upfront validations... + + Resource resource = mock(Resource.class); + when(resource.getFilename()).thenThrow(new AssertionError("Resource should not be resolved")); + when(resource.getInputStream()).thenThrow(new AssertionError("Resource should not be resolved")); + ResourceResolver resolver = mock(ResourceResolver.class); + when(resolver.resolveResource(any(), any(), any(), any())).thenReturn(resource); + + ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler(); + handler.setLocations(Collections.singletonList(new ClassPathResource("test/", getClass()))); + handler.setResourceResolvers(Collections.singletonList(resolver)); + handler.setServletContext(new TestServletContext()); + handler.afterPropertiesSet(); + + testInvalidPath("../testsecret/secret.txt", handler); + testInvalidPath("test/../../testsecret/secret.txt", handler); + testInvalidPath(":/../../testsecret/secret.txt", handler); + + Resource location = new UrlResource(getClass().getResource("./test/")); + this.handler.setLocations(Collections.singletonList(location)); + Resource secretResource = new UrlResource(getClass().getResource("testsecret/secret.txt")); + String secretPath = secretResource.getURL().getPath(); + + testInvalidPath("file:" + secretPath, handler); + testInvalidPath("/file:" + secretPath, handler); + testInvalidPath("url:" + secretPath, handler); + testInvalidPath("/url:" + secretPath, handler); + testInvalidPath("/../.." + secretPath, handler); + testInvalidPath("/%2E%2E/testsecret/secret.txt", handler); + testInvalidPath("/%2E%2E/testsecret/secret.txt", handler); + testInvalidPath("%2F%2F%2E%2E%2F%2F%2E%2E" + secretPath, handler); + } + + private void testInvalidPath(String requestPath, ResourceHttpRequestHandler handler) throws Exception { + this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, requestPath); + this.response = new MockHttpServletResponse(); + handler.handleRequest(this.request, this.response); + assertEquals(HttpStatus.NOT_FOUND.value(), this.response.getStatus()); + } + + @Test + public void resolvePathWithTraversal() throws Exception { for (HttpMethod method : HttpMethod.values()) { this.request = new MockHttpServletRequest("GET", ""); this.response = new MockHttpServletResponse(); - testInvalidPath(method); + testResolvePathWithTraversal(method); } } - private void testInvalidPath(HttpMethod httpMethod) throws Exception { + private void testResolvePathWithTraversal(HttpMethod httpMethod) throws Exception { this.request.setMethod(httpMethod.name()); Resource location = new ClassPathResource("test/", getClass()); this.handler.setLocations(Collections.singletonList(location)); - testInvalidPath(location, "../testsecret/secret.txt"); - testInvalidPath(location, "test/../../testsecret/secret.txt"); - testInvalidPath(location, ":/../../testsecret/secret.txt"); + testResolvePathWithTraversal(location, "../testsecret/secret.txt"); + testResolvePathWithTraversal(location, "test/../../testsecret/secret.txt"); + testResolvePathWithTraversal(location, ":/../../testsecret/secret.txt"); location = new UrlResource(getClass().getResource("./test/")); this.handler.setLocations(Collections.singletonList(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); + testResolvePathWithTraversal(location, "file:" + secretPath); + testResolvePathWithTraversal(location, "/file:" + secretPath); + testResolvePathWithTraversal(location, "url:" + secretPath); + testResolvePathWithTraversal(location, "/url:" + secretPath); + testResolvePathWithTraversal(location, "/" + secretPath); + testResolvePathWithTraversal(location, "////../.." + secretPath); + testResolvePathWithTraversal(location, "/%2E%2E/testsecret/secret.txt"); + testResolvePathWithTraversal(location, "%2F%2F%2E%2E%2F%2Ftestsecret/secret.txt"); + testResolvePathWithTraversal(location, "/ " + secretPath); } - private void testInvalidPath(Resource location, String requestPath) throws Exception { + private void testResolvePathWithTraversal(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);
91b803a23103Consistent encoded path evaluation in ResourceHttpRequestHandler and co
2 files changed · +94 −82
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java+27 −18 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -17,6 +17,7 @@ package org.springframework.web.servlet.resource; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -251,21 +252,7 @@ else if (resource instanceof ServletContextResource) { return true; } 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; + return (resourcePath.startsWith(locationPath) && !isInvalidEncodedPath(resourcePath)); } private String encodeIfNecessary(String path, @Nullable HttpServletRequest request, Resource location) { @@ -289,8 +276,30 @@ private String encodeIfNecessary(String path, @Nullable HttpServletRequest reque } private boolean shouldEncodeRelativePath(Resource location) { - return location instanceof UrlResource && - this.urlPathHelper != null && this.urlPathHelper.isUrlDecode(); + return (location instanceof UrlResource && this.urlPathHelper != null && this.urlPathHelper.isUrlDecode()); + } + + private boolean isInvalidEncodedPath(String resourcePath) { + if (resourcePath.contains("%")) { + // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars... + try { + String decodedPath = URLDecoder.decode(resourcePath, "UTF-8"); + int separatorIndex = decodedPath.indexOf("..") + 2; + if (separatorIndex > 1 && separatorIndex < decodedPath.length()) { + char separator = decodedPath.charAt(separatorIndex); + if (separator == '/' || separator == '\\') { + if (logger.isTraceEnabled()) { + logger.trace("Resolved resource path contains \"../\" after decoding: " + resourcePath); + } + } + return true; + } + } + catch (UnsupportedEncodingException ex) { + // Should never happen... + } + } + return false; } }
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java+67 −64 modified@@ -17,6 +17,7 @@ package org.springframework.web.servlet.resource; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.nio.charset.Charset; import java.util.ArrayList; @@ -66,24 +67,23 @@ * according to the guidelines of Page Speed, YSlow, etc. * * <p>The {@linkplain #setLocations "locations"} property takes a list of Spring - * {@link Resource} locations from which static resources are allowed to - * be served by this handler. Resources could be served from a classpath location, - * e.g. "classpath:/META-INF/public-web-resources/", allowing convenient packaging + * {@link Resource} locations from which static resources are allowed to be served + * by this handler. Resources could be served from a classpath location, e.g. + * "classpath:/META-INF/public-web-resources/", allowing convenient packaging * and serving of resources such as .js, .css, and others in jar files. * * <p>This request handler may also be configured with a * {@link #setResourceResolvers(List) resourcesResolver} and * {@link #setResourceTransformers(List) resourceTransformer} chains to support - * arbitrary resolution and transformation of resources being served. By default a - * {@link PathResourceResolver} simply finds resources based on the configured - * "locations". An application can configure additional resolvers and - * transformers such as the {@link VersionResourceResolver} which can resolve - * and prepare URLs for resources with a version in the URL. + * arbitrary resolution and transformation of resources being served. By default + * a {@link PathResourceResolver} simply finds resources based on the configured + * "locations". An application can configure additional resolvers and transformers + * such as the {@link VersionResourceResolver} which can resolve and prepare URLs + * for resources with a version in the URL. * - * <p>This handler also properly evaluates the {@code Last-Modified} header (if - * present) so that a {@code 304} status code will be returned as appropriate, - * avoiding unnecessary overhead for resources that are already cached by the - * client. + * <p>This handler also properly evaluates the {@code Last-Modified} header + * (if present) so that a {@code 304} status code will be returned as appropriate, + * avoiding unnecessary overhead for resources that are already cached by the client. * * @author Keith Donald * @author Jeremy Grelle @@ -510,64 +510,33 @@ protected Resource getResource(HttpServletRequest request) throws IOException { 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 - String decodedPath = URLDecoder.decode(path, "UTF-8"); - if (isInvalidPath(decodedPath)) { - if (logger.isTraceEnabled()) { - logger.trace("Ignoring invalid resource path with escape sequences [" + path + "]."); - } - return null; - } - decodedPath = processPath(decodedPath); - if (isInvalidPath(decodedPath)) { - if (logger.isTraceEnabled()) { - logger.trace("Ignoring invalid resource path with escape sequences [" + path + "]."); - } - return null; - } - } - catch (IllegalArgumentException ex) { - // ignore + if (isInvalidEncodedPath(path)) { + if (logger.isTraceEnabled()) { + logger.trace("Ignoring invalid resource path with escape sequences [" + path + "]"); } + return null; } + ResourceResolverChain resolveChain = new DefaultResourceResolverChain(getResourceResolvers()); Resource resource = resolveChain.resolveResource(request, path, getLocations()); if (resource == null || getResourceTransformers().isEmpty()) { return resource; } + ResourceTransformerChain transformChain = new DefaultResourceTransformerChain(resolveChain, getResourceTransformers()); resource = transformChain.transform(request, resource); return resource; } - /** - * Process the given resource path. - * <p>The default implementation replaces: - * <ul> - * <li>Backslash with forward slash. - * <li>Duplicate occurrences of slash with a single slash. - * <li>Any combination of leading slash and control characters (00-1F and 7F) - * with a single "/" or "". For example {@code " / // foo/bar"} - * becomes {@code "/foo/bar"}. - * </ul> - * @since 3.2.12 - */ - protected String processPath(String path) { - path = StringUtils.replace(path, "\\", "/"); - path = cleanDuplicateSlashes(path); - return cleanLeadingSlash(path); - } - private String cleanDuplicateSlashes(String path) { StringBuilder sb = null; char prev = 0; @@ -601,16 +570,41 @@ 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); + path = (slash ? "/" + path.substring(i) : path.substring(i)); if (logger.isTraceEnabled()) { - logger.trace("Path after trimming leading '/' and control characters: " + path); + logger.trace("Path after trimming leading '/' and control characters: [" + path + "]"); } return path; } } return (slash ? "/" : ""); } + /** + * Check whether the given path contains invalid escape sequences. + * @param path the path to validate + * @return {@code true} if the path is invalid, {@code false} otherwise + */ + private boolean isInvalidEncodedPath(String path) { + if (path.contains("%")) { + try { + // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars + String decodedPath = URLDecoder.decode(path, "UTF-8"); + if (isInvalidPath(decodedPath)) { + return true; + } + decodedPath = processPath(decodedPath); + if (isInvalidPath(decodedPath)) { + return true; + } + } + catch (IllegalArgumentException | UnsupportedEncodingException ex) { + // Should never happen... + } + } + return false; + } + /** * Identifies invalid resource paths. By default rejects: * <ul> @@ -627,36 +621,45 @@ else if (path.charAt(i) > ' ' && path.charAt(i) != 127) { * @return {@code true} if the path is invalid, {@code false} otherwise */ protected boolean isInvalidPath(String path) { - 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\"."); - } + 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."); - } + 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."); - } + logger.trace("Path contains \"../\" after call to StringUtils#cleanPath."); return true; } } return false; } + /** + * Process the given resource path. + * <p>The default implementation replaces: + * <ul> + * <li>Backslash with forward slash. + * <li>Duplicate occurrences of slash with a single slash. + * <li>Any combination of leading slash and control characters (00-1F and 7F) + * with a single "/" or "". For example {@code " / // foo/bar"} + * becomes {@code "/foo/bar"}. + * </ul> + * @since 3.2.12 + */ + protected String processPath(String path) { + path = StringUtils.replace(path, "\\", "/"); + path = cleanDuplicateSlashes(path); + return cleanLeadingSlash(path); + } + /** * Determine the media type for the given request and the resource matched * to it. This implementation tries to determine the MediaType based on the
0e28bee0f155Clean duplicate separators in resource URLs
4 files changed · +231 −51
spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java+48 −5 modified@@ -328,7 +328,15 @@ protected Mono<Resource> getResource(ServerWebExchange exchange) { if (path.contains("%")) { try { // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars - if (isInvalidPath(URLDecoder.decode(path, "UTF-8"))) { + String decodedPath = URLDecoder.decode(path, "UTF-8"); + if (isInvalidPath(decodedPath)) { + if (logger.isTraceEnabled()) { + logger.trace("Ignoring invalid resource path with escape sequences [" + path + "]."); + } + return Mono.empty(); + } + decodedPath = processPath(decodedPath); + if (isInvalidPath(decodedPath)) { if (logger.isTraceEnabled()) { logger.trace("Ignoring invalid resource path with escape sequences [" + path + "]."); } @@ -352,12 +360,47 @@ protected Mono<Resource> getResource(ServerWebExchange exchange) { } /** - * 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"}. + * Process the given resource path. + * <p>The default implementation replaces: + * <ul> + * <li>Backslash with forward slash. + * <li>Duplicate occurrences of slash with a single slash. + * <li>Any combination of leading slash and control characters (00-1F and 7F) + * with a single "/" or "". For example {@code " / // foo/bar"} + * becomes {@code "/foo/bar"}. + * </ul> + * @since 3.2.12 */ protected String processPath(String path) { + path = StringUtils.replace(path, "\\", "/"); + path = cleanDuplicateSlashes(path); + return cleanLeadingSlash(path); + } + + private String cleanDuplicateSlashes(String path) { + StringBuilder sb = null; + char prev = 0; + for (int i = 0; i < path.length(); i++) { + char curr = path.charAt(i); + try { + if ((curr == '/') && (prev == '/')) { + if (sb == null) { + sb = new StringBuilder(path.substring(0, i)); + } + continue; + } + if (sb != null) { + sb.append(path.charAt(i)); + } + } + finally { + prev = curr; + } + } + return sb != null ? sb.toString() : path; + } + + private String cleanLeadingSlash(String path) { boolean slash = false; for (int i = 0; i < path.length(); i++) { if (path.charAt(i) == '/') {
spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java+66 −23 modified@@ -54,13 +54,10 @@ import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ServerWebExchange; -import static org.hamcrest.Matchers.instanceOf; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; /** * Unit tests for {@link ResourceWebHandler}. @@ -240,39 +237,85 @@ public void getMediaTypeWithFavorPathExtensionOff() throws Exception { } @Test - public void invalidPath() throws Exception { + public void testInvalidPath() throws Exception { + + // Use mock ResourceResolver: i.e. we're only testing upfront validations... + + Resource resource = mock(Resource.class); + when(resource.getFilename()).thenThrow(new AssertionError("Resource should not be resolved")); + when(resource.getInputStream()).thenThrow(new AssertionError("Resource should not be resolved")); + ResourceResolver resolver = mock(ResourceResolver.class); + when(resolver.resolveResource(any(), any(), any(), any())).thenReturn(Mono.just(resource)); + + ResourceWebHandler handler = new ResourceWebHandler(); + handler.setLocations(Collections.singletonList(new ClassPathResource("test/", getClass()))); + handler.setResourceResolvers(Collections.singletonList(resolver)); + handler.afterPropertiesSet(); + + testInvalidPath("../testsecret/secret.txt", handler); + testInvalidPath("test/../../testsecret/secret.txt", handler); + testInvalidPath(":/../../testsecret/secret.txt", handler); + + Resource location = new UrlResource(getClass().getResource("./test/")); + this.handler.setLocations(Collections.singletonList(location)); + Resource secretResource = new UrlResource(getClass().getResource("testsecret/secret.txt")); + String secretPath = secretResource.getURL().getPath(); + + testInvalidPath("file:" + secretPath, handler); + testInvalidPath("/file:" + secretPath, handler); + testInvalidPath("url:" + secretPath, handler); + testInvalidPath("/url:" + secretPath, handler); + testInvalidPath("/../.." + secretPath, handler); + testInvalidPath("/%2E%2E/testsecret/secret.txt", handler); + testInvalidPath("/%2E%2E/testsecret/secret.txt", handler); + testInvalidPath("%2F%2F%2E%2E%2F%2F%2E%2E" + secretPath, handler); + } + + private void testInvalidPath(String requestPath, ResourceWebHandler handler) { + ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("")); + setPathWithinHandlerMapping(exchange, requestPath); + StepVerifier.create(handler.handle(exchange)) + .expectErrorSatisfies(err -> { + assertThat(err, instanceOf(ResponseStatusException.class)); + assertEquals(HttpStatus.NOT_FOUND, ((ResponseStatusException) err).getStatus()); + }).verify(TIMEOUT); + } + + @Test + public void testResolvePathWithTraversal() throws Exception { for (HttpMethod method : HttpMethod.values()) { - testInvalidPath(method); + testResolvePathWithTraversal(method); } } - private void testInvalidPath(HttpMethod httpMethod) throws Exception { + private void testResolvePathWithTraversal(HttpMethod httpMethod) throws Exception { Resource location = new ClassPathResource("test/", getClass()); this.handler.setLocations(Collections.singletonList(location)); - testInvalidPath(httpMethod, "../testsecret/secret.txt", location); - testInvalidPath(httpMethod, "test/../../testsecret/secret.txt", location); - testInvalidPath(httpMethod, ":/../../testsecret/secret.txt", location); + testResolvePathWithTraversal(httpMethod, "../testsecret/secret.txt", location); + testResolvePathWithTraversal(httpMethod, "test/../../testsecret/secret.txt", location); + testResolvePathWithTraversal(httpMethod, ":/../../testsecret/secret.txt", location); location = new UrlResource(getClass().getResource("./test/")); this.handler.setLocations(Collections.singletonList(location)); Resource secretResource = new UrlResource(getClass().getResource("testsecret/secret.txt")); String secretPath = secretResource.getURL().getPath(); - testInvalidPath(httpMethod, "file:" + secretPath, location); - testInvalidPath(httpMethod, "/file:" + secretPath, location); - testInvalidPath(httpMethod, "url:" + secretPath, location); - testInvalidPath(httpMethod, "/url:" + secretPath, location); - testInvalidPath(httpMethod, "////../.." + secretPath, location); - testInvalidPath(httpMethod, "/%2E%2E/testsecret/secret.txt", location); - testInvalidPath(httpMethod, "url:" + secretPath, location); + testResolvePathWithTraversal(httpMethod, "file:" + secretPath, location); + testResolvePathWithTraversal(httpMethod, "/file:" + secretPath, location); + testResolvePathWithTraversal(httpMethod, "url:" + secretPath, location); + testResolvePathWithTraversal(httpMethod, "/url:" + secretPath, location); + testResolvePathWithTraversal(httpMethod, "////../.." + secretPath, location); + testResolvePathWithTraversal(httpMethod, "/%2E%2E/testsecret/secret.txt", location); + testResolvePathWithTraversal(httpMethod, "%2F%2F%2E%2E%2F%2Ftestsecret/secret.txt", location); + testResolvePathWithTraversal(httpMethod, "url:" + secretPath, location); // The following tests fail with a MalformedURLException on Windows - // testInvalidPath(location, "/" + secretPath); - // testInvalidPath(location, "/ " + secretPath); + // testResolvePathWithTraversal(location, "/" + secretPath); + // testResolvePathWithTraversal(location, "/ " + secretPath); } - private void testInvalidPath(HttpMethod httpMethod, String requestPath, Resource location) throws Exception { + private void testResolvePathWithTraversal(HttpMethod httpMethod, String requestPath, Resource location) throws Exception { ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.method(httpMethod, "")); setPathWithinHandlerMapping(exchange, requestPath); StepVerifier.create(this.handler.handle(exchange))
spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java+47 −5 modified@@ -520,7 +520,15 @@ protected Resource getResource(HttpServletRequest request) throws IOException { if (path.contains("%")) { try { // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars - if (isInvalidPath(URLDecoder.decode(path, "UTF-8"))) { + String decodedPath = URLDecoder.decode(path, "UTF-8"); + if (isInvalidPath(decodedPath)) { + if (logger.isTraceEnabled()) { + logger.trace("Ignoring invalid resource path with escape sequences [" + path + "]."); + } + return null; + } + decodedPath = processPath(decodedPath); + if (isInvalidPath(decodedPath)) { if (logger.isTraceEnabled()) { logger.trace("Ignoring invalid resource path with escape sequences [" + path + "]."); } @@ -543,13 +551,47 @@ protected Resource getResource(HttpServletRequest request) throws IOException { } /** - * 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"}. + * Process the given resource path. + * <p>The default implementation replaces: + * <ul> + * <li>Backslash with forward slash. + * <li>Duplicate occurrences of slash with a single slash. + * <li>Any combination of leading slash and control characters (00-1F and 7F) + * with a single "/" or "". For example {@code " / // foo/bar"} + * becomes {@code "/foo/bar"}. + * </ul> * @since 3.2.12 */ protected String processPath(String path) { + path = StringUtils.replace(path, "\\", "/"); + path = cleanDuplicateSlashes(path); + return cleanLeadingSlash(path); + } + + private String cleanDuplicateSlashes(String path) { + StringBuilder sb = null; + char prev = 0; + for (int i = 0; i < path.length(); i++) { + char curr = path.charAt(i); + try { + if ((curr == '/') && (prev == '/')) { + if (sb == null) { + sb = new StringBuilder(path.substring(0, i)); + } + continue; + } + if (sb != null) { + sb.append(path.charAt(i)); + } + } + finally { + prev = curr; + } + } + return sb != null ? sb.toString() : path; + } + + private String cleanLeadingSlash(String path) { boolean slash = false; for (int i = 0; i < path.length(); i++) { if (path.charAt(i) == '/') {
spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerTests.java+70 −18 modified@@ -43,6 +43,7 @@ import org.springframework.web.servlet.HandlerMapping; import static org.junit.Assert.*; +import static org.mockito.Mockito.*; /** * Unit tests for {@link ResourceHttpRequestHandler}. @@ -303,41 +304,84 @@ public String getVirtualServerName() { } @Test - public void invalidPath() throws Exception { + public void testInvalidPath() throws Exception { + + // Use mock ResourceResolver: i.e. we're only testing upfront validations... + + Resource resource = mock(Resource.class); + when(resource.getFilename()).thenThrow(new AssertionError("Resource should not be resolved")); + when(resource.getInputStream()).thenThrow(new AssertionError("Resource should not be resolved")); + ResourceResolver resolver = mock(ResourceResolver.class); + when(resolver.resolveResource(any(), any(), any(), any())).thenReturn(resource); + + ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler(); + handler.setLocations(Collections.singletonList(new ClassPathResource("test/", getClass()))); + handler.setResourceResolvers(Collections.singletonList(resolver)); + handler.setServletContext(new TestServletContext()); + handler.afterPropertiesSet(); + + testInvalidPath("../testsecret/secret.txt", handler); + testInvalidPath("test/../../testsecret/secret.txt", handler); + testInvalidPath(":/../../testsecret/secret.txt", handler); + + Resource location = new UrlResource(getClass().getResource("./test/")); + this.handler.setLocations(Collections.singletonList(location)); + Resource secretResource = new UrlResource(getClass().getResource("testsecret/secret.txt")); + String secretPath = secretResource.getURL().getPath(); + + testInvalidPath("file:" + secretPath, handler); + testInvalidPath("/file:" + secretPath, handler); + testInvalidPath("url:" + secretPath, handler); + testInvalidPath("/url:" + secretPath, handler); + testInvalidPath("/../.." + secretPath, handler); + testInvalidPath("/%2E%2E/testsecret/secret.txt", handler); + testInvalidPath("/%2E%2E/testsecret/secret.txt", handler); + testInvalidPath("%2F%2F%2E%2E%2F%2F%2E%2E" + secretPath, handler); + } + + private void testInvalidPath(String requestPath, ResourceHttpRequestHandler handler) throws Exception { + this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, requestPath); + this.response = new MockHttpServletResponse(); + handler.handleRequest(this.request, this.response); + assertEquals(HttpStatus.NOT_FOUND.value(), this.response.getStatus()); + } + + @Test + public void resolvePathWithTraversal() throws Exception { for (HttpMethod method : HttpMethod.values()) { this.request = new MockHttpServletRequest("GET", ""); this.response = new MockHttpServletResponse(); - testInvalidPath(method); + testResolvePathWithTraversal(method); } } - private void testInvalidPath(HttpMethod httpMethod) throws Exception { + private void testResolvePathWithTraversal(HttpMethod httpMethod) throws Exception { this.request.setMethod(httpMethod.name()); Resource location = new ClassPathResource("test/", getClass()); this.handler.setLocations(Collections.singletonList(location)); - testInvalidPath(location, "../testsecret/secret.txt"); - testInvalidPath(location, "test/../../testsecret/secret.txt"); - testInvalidPath(location, ":/../../testsecret/secret.txt"); + testResolvePathWithTraversal(location, "../testsecret/secret.txt"); + testResolvePathWithTraversal(location, "test/../../testsecret/secret.txt"); + testResolvePathWithTraversal(location, ":/../../testsecret/secret.txt"); location = new UrlResource(getClass().getResource("./test/")); this.handler.setLocations(Collections.singletonList(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); + testResolvePathWithTraversal(location, "file:" + secretPath); + testResolvePathWithTraversal(location, "/file:" + secretPath); + testResolvePathWithTraversal(location, "url:" + secretPath); + testResolvePathWithTraversal(location, "/url:" + secretPath); + testResolvePathWithTraversal(location, "/" + secretPath); + testResolvePathWithTraversal(location, "////../.." + secretPath); + testResolvePathWithTraversal(location, "/%2E%2E/testsecret/secret.txt"); + testResolvePathWithTraversal(location, "%2F%2F%2E%2E%2F%2Ftestsecret/secret.txt"); + testResolvePathWithTraversal(location, "/ " + secretPath); } - private void testInvalidPath(Resource location, String requestPath) throws Exception { + private void testResolvePathWithTraversal(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); @@ -356,7 +400,8 @@ public void ignoreInvalidEscapeSequence() throws Exception { } @Test - public void processPath() throws Exception { + public void processPath() { + // Unchanged assertSame("/foo/bar", this.handler.processPath("/foo/bar")); assertSame("foo/bar", this.handler.processPath("foo/bar")); @@ -382,10 +427,17 @@ public void processPath() throws Exception { assertEquals("/", this.handler.processPath("/")); assertEquals("/", this.handler.processPath("///")); assertEquals("/", this.handler.processPath("/ / / ")); + assertEquals("/", this.handler.processPath("\\/ \\/ \\/ ")); + + // duplicate slash or backslash + assertEquals("/foo/ /bar/baz/", this.handler.processPath("//foo/ /bar//baz//")); + assertEquals("/foo/ /bar/baz/", this.handler.processPath("\\\\foo\\ \\bar\\\\baz\\\\")); + assertEquals("foo/bar", this.handler.processPath("foo\\\\/\\////bar")); + } @Test - public void initAllowedLocations() throws Exception { + public void initAllowedLocations() { PathResourceResolver resolver = (PathResourceResolver) this.handler.getResourceResolvers().get(0); Resource[] locations = resolver.getAllowedLocations();
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
21- access.redhat.com/errata/RHSA-2018:1320ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHSA-2018:2669ghsavendor-advisoryx_refsource_REDHATWEB
- access.redhat.com/errata/RHSA-2018:2939ghsavendor-advisoryx_refsource_REDHATWEB
- github.com/advisories/GHSA-g8hw-794c-4j9gghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2018-1271ghsaADVISORY
- www.oracle.com/technetwork/security-advisory/cpujul2018-4258247.htmlghsax_refsource_CONFIRMWEB
- www.oracle.com/technetwork/security-advisory/cpuoct2018-4428296.htmlghsax_refsource_CONFIRMWEB
- www.securityfocus.com/bid/103699ghsavdb-entryx_refsource_BIDWEB
- github.com/spring-projects/spring-framework/commit/0e28bee0f155b9bf240b4bafc4646e4810cb23f8ghsaWEB
- github.com/spring-projects/spring-framework/commit/13356a7ee2240f740737c5c83bdccdacc30603abghsaWEB
- github.com/spring-projects/spring-framework/commit/695bf2961feffd35b5560ccc982a2189dcca611fghsaWEB
- github.com/spring-projects/spring-framework/commit/91b803a2310344d925e5d4b1709bbcea90375548ghsaWEB
- github.com/spring-projects/spring-framework/commit/98ad23bef8e2e04143f8f5b201380543a8d8c0c3ghsaWEB
- github.com/spring-projects/spring-framework/commit/b9ebdaaf3710db473a2e1fec8641c316483a22aaghsaWEB
- github.com/spring-projects/spring-framework/commit/f046a066eceefa0799d1bc89bd6e1318f39bdf69ghsaWEB
- github.com/spring-projects/spring-framework/commit/f59ea610dfcf55cd0b42f6dd76a9b3dab0218aaaghsaWEB
- pivotal.io/security/cve-2018-1271ghsax_refsource_CONFIRMWEB
- www.oracle.com/security-alerts/cpujul2020.htmlghsax_refsource_MISCWEB
- www.oracle.com/security-alerts/cpuoct2021.htmlghsax_refsource_MISCWEB
- www.oracle.com/technetwork/security-advisory/cpujan2019-5072801.htmlghsax_refsource_CONFIRMWEB
- www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.htmlghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.