VYPR
Moderate severityNVD Advisory· Published Apr 6, 2018· Updated Sep 16, 2024

CVE-2018-1271

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.

PackageAffected versionsPatched versions
org.springframework:spring-coreMaven
>= 5.0.0, < 5.0.55.0.5
org.springframework:spring-coreMaven
< 4.3.154.3.15

Affected products

2

Patches

8
695bf2961fef

Consistent trace logging in PathResourceResolver

https://github.com/spring-projects/spring-frameworkJuergen HoellerMar 29, 2018via ghsa
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;
     				}
    
f046a066ecee

Simplified separator check within isInvalidEncodedPath

https://github.com/spring-projects/spring-frameworkJuergen HoellerMar 27, 2018via ghsa
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
    
98ad23bef8e2

Consistent logging of encoded path evaluation failure

https://github.com/spring-projects/spring-frameworkJuergen HoellerMar 27, 2018via ghsa
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...
    
13356a7ee224

Consistent encoded path evaluation in reactive ResourceWebHandler and co

https://github.com/spring-projects/spring-frameworkJuergen HoellerMar 26, 2018via ghsa
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>
    
f59ea610dfcf

Simplified separator check within isInvalidEncodedPath

https://github.com/spring-projects/spring-frameworkJuergen HoellerMar 26, 2018via ghsa
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...
    
b9ebdaaf3710

Backport clean duplicate separators in resource URLs

https://github.com/spring-projects/spring-frameworkRossen StoyanchevMar 22, 2018via ghsa
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);
    
91b803a23103

Consistent encoded path evaluation in ResourceHttpRequestHandler and co

https://github.com/spring-projects/spring-frameworkJuergen HoellerMar 21, 2018via ghsa
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
    
0e28bee0f155

Clean duplicate separators in resource URLs

https://github.com/spring-projects/spring-frameworkRossen StoyanchevMar 19, 2018via ghsa
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

News mentions

0

No linked articles in our index yet.