High severity7.5NVD Advisory· Published Sep 13, 2024· Updated Apr 15, 2026
CVE-2024-38816
CVE-2024-38816
Description
Applications serving static resources through the functional web frameworks WebMvc.fn or WebFlux.fn are vulnerable to path traversal attacks. An attacker can craft malicious HTTP requests and obtain any file on the file system that is also accessible to the process in which the Spring application is running.
Specifically, an application is vulnerable when both of the following are true:
- the web application uses RouterFunctions to serve static resources
- resource handling is explicitly configured with a FileSystemResource location
However, malicious requests are blocked and rejected when any of the following is true:
- the Spring Security HTTP Firewall https://docs.spring.io/spring-security/reference/servlet/exploits/firewall.html is in use
- the application runs on Tomcat or Jetty
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.springframework:spring-webmvcMaven | >= 6.1.0, < 6.1.13 | 6.1.13 |
org.springframework:spring-webfluxMaven | >= 6.1.0, < 6.1.13 | 6.1.13 |
org.springframework:spring-webmvcMaven | >= 6.0.0, <= 6.0.23 | — |
org.springframework:spring-webfluxMaven | >= 6.0.0, <= 6.0.23 | — |
org.springframework:spring-webmvcMaven | >= 5.3.0, <= 5.3.39 | — |
org.springframework:spring-webfluxMaven | >= 5.3.0, <= 5.3.39 | — |
Patches
1d86bf8b20564Align RouterFunctions resource handling
2 files changed · +186 −25
spring-webflux/src/main/java/org/springframework/web/reactive/function/server/PathResourceLookupFunction.java+93 −14 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -18,6 +18,7 @@ import java.io.IOException; import java.io.UncheckedIOException; +import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.function.Function; @@ -30,6 +31,7 @@ import org.springframework.util.Assert; import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; +import org.springframework.web.util.UriUtils; import org.springframework.web.util.pattern.PathPattern; import org.springframework.web.util.pattern.PathPatternParser; @@ -63,13 +65,17 @@ public Mono<Resource> apply(ServerRequest request) { pathContainer = this.pattern.extractPathWithinPattern(pathContainer); String path = processPath(pathContainer.value()); - if (path.contains("%")) { - path = StringUtils.uriDecode(path, StandardCharsets.UTF_8); + if (!StringUtils.hasText(path) || isInvalidPath(path)) { + return Mono.empty(); } - if (!StringUtils.hasLength(path) || isInvalidPath(path)) { + if (isInvalidEncodedInputPath(path)) { return Mono.empty(); } + if (!(this.location instanceof UrlResource)) { + path = UriUtils.decode(path, StandardCharsets.UTF_8); + } + try { Resource resource = this.location.createRelative(path); if (resource.isReadable() && isResourceUnderLocation(resource)) { @@ -84,7 +90,47 @@ public Mono<Resource> apply(ServerRequest request) { } } - private String processPath(String path) { + /** + * 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> + */ + 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) == '/') { @@ -94,8 +140,7 @@ 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); - return path; + return (slash ? "/" + path.substring(i) : path.substring(i)); } } return (slash ? "/" : ""); @@ -117,6 +162,31 @@ private boolean isInvalidPath(String path) { return false; } + /** + * 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 isInvalidEncodedInputPath(String path) { + if (path.contains("%")) { + try { + // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars + String decodedPath = URLDecoder.decode(path, StandardCharsets.UTF_8); + if (isInvalidPath(decodedPath)) { + return true; + } + decodedPath = processPath(decodedPath); + if (isInvalidPath(decodedPath)) { + return true; + } + } + catch (IllegalArgumentException ex) { + // May not be possible to decode... + } + } + return false; + } + private boolean isResourceUnderLocation(Resource resource) throws IOException { if (resource.getClass() != this.location.getClass()) { return false; @@ -142,15 +212,24 @@ else if (resource instanceof ClassPathResource classPathResource) { return true; } locationPath = (locationPath.endsWith("/") || locationPath.isEmpty() ? locationPath : locationPath + "/"); - if (!resourcePath.startsWith(locationPath)) { - return false; - } - if (resourcePath.contains("%") && StringUtils.uriDecode(resourcePath, StandardCharsets.UTF_8).contains("../")) { - return false; - } - return true; + return (resourcePath.startsWith(locationPath) && !isInvalidEncodedInputPath(resourcePath)); } + private boolean isInvalidEncodedResourcePath(String resourcePath) { + if (resourcePath.contains("%")) { + // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars... + try { + String decodedPath = URLDecoder.decode(resourcePath, StandardCharsets.UTF_8); + if (decodedPath.contains("../") || decodedPath.contains("..\\")) { + return true; + } + } + catch (IllegalArgumentException ex) { + // May not be possible to decode... + } + } + return false; + } @Override public String toString() {
spring-webmvc/src/main/java/org/springframework/web/servlet/function/PathResourceLookupFunction.java+93 −11 modified@@ -18,6 +18,7 @@ import java.io.IOException; import java.io.UncheckedIOException; +import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.Optional; import java.util.function.Function; @@ -29,13 +30,16 @@ import org.springframework.util.Assert; import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; +import org.springframework.web.context.support.ServletContextResource; +import org.springframework.web.util.UriUtils; import org.springframework.web.util.pattern.PathPattern; import org.springframework.web.util.pattern.PathPatternParser; /** * Lookup function used by {@link RouterFunctions#resources(String, Resource)}. * * @author Arjen Poutsma + * @author Rossen Stoyanchev * @since 5.2 */ class PathResourceLookupFunction implements Function<ServerRequest, Optional<Resource>> { @@ -62,13 +66,17 @@ public Optional<Resource> apply(ServerRequest request) { pathContainer = this.pattern.extractPathWithinPattern(pathContainer); String path = processPath(pathContainer.value()); - if (path.contains("%")) { - path = StringUtils.uriDecode(path, StandardCharsets.UTF_8); + if (!StringUtils.hasText(path) || isInvalidPath(path)) { + return Optional.empty(); } - if (!StringUtils.hasLength(path) || isInvalidPath(path)) { + if (isInvalidEncodedInputPath(path)) { return Optional.empty(); } + if (!(this.location instanceof UrlResource)) { + path = UriUtils.decode(path, StandardCharsets.UTF_8); + } + try { Resource resource = this.location.createRelative(path); if (resource.isReadable() && isResourceUnderLocation(resource)) { @@ -83,7 +91,47 @@ public Optional<Resource> apply(ServerRequest request) { } } - private String processPath(String path) { + /** + * 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> + */ + 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) == '/') { @@ -93,8 +141,7 @@ 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); - return path; + return (slash ? "/" + path.substring(i) : path.substring(i)); } } return (slash ? "/" : ""); @@ -113,6 +160,26 @@ private boolean isInvalidPath(String path) { return path.contains("..") && StringUtils.cleanPath(path).contains("../"); } + private boolean isInvalidEncodedInputPath(String path) { + if (path.contains("%")) { + try { + // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars + String decodedPath = URLDecoder.decode(path, StandardCharsets.UTF_8); + if (isInvalidPath(decodedPath)) { + return true; + } + decodedPath = processPath(decodedPath); + if (isInvalidPath(decodedPath)) { + return true; + } + } + catch (IllegalArgumentException ex) { + // May not be possible to decode... + } + } + return false; + } + private boolean isResourceUnderLocation(Resource resource) throws IOException { if (resource.getClass() != this.location.getClass()) { return false; @@ -129,6 +196,10 @@ else if (resource instanceof ClassPathResource classPathResource) { resourcePath = classPathResource.getPath(); locationPath = StringUtils.cleanPath(((ClassPathResource) this.location).getPath()); } + else if (resource instanceof ServletContextResource servletContextResource) { + resourcePath = servletContextResource.getPath(); + locationPath = StringUtils.cleanPath(((ServletContextResource) this.location).getPath()); + } else { resourcePath = resource.getURL().getPath(); locationPath = StringUtils.cleanPath(this.location.getURL().getPath()); @@ -138,13 +209,24 @@ else if (resource instanceof ClassPathResource classPathResource) { return true; } locationPath = (locationPath.endsWith("/") || locationPath.isEmpty() ? locationPath : locationPath + "/"); - if (!resourcePath.startsWith(locationPath)) { - return false; - } - return !resourcePath.contains("%") || - !StringUtils.uriDecode(resourcePath, StandardCharsets.UTF_8).contains("../"); + return (resourcePath.startsWith(locationPath) && !isInvalidEncodedResourcePath(resourcePath)); } + private boolean isInvalidEncodedResourcePath(String resourcePath) { + if (resourcePath.contains("%")) { + // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars... + try { + String decodedPath = URLDecoder.decode(resourcePath, StandardCharsets.UTF_8); + if (decodedPath.contains("../") || decodedPath.contains("..\\")) { + return true; + } + } + catch (IllegalArgumentException ex) { + // May not be possible to decode... + } + } + return false; + } @Override public String toString() {
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-cx7f-g6mp-7hqmghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-38816ghsaADVISORY
- github.com/spring-projects/spring-framework/commit/d86bf8b2056429edf5494456cffcb2b243331c49ghsaWEB
- security.netapp.com/advisory/ntap-20241227-0001ghsaWEB
- spring.io/security/cve-2024-38816nvdWEB
- security.netapp.com/advisory/ntap-20241227-0001/nvd
News mentions
0No linked articles in our index yet.