Skip to content

Commit eb4d76c

Browse files
committed
Fix path traversal vulnerability in functional web frameworks
See https://spring.io/security/cve-2024-38819
1 parent 0406553 commit eb4d76c

File tree

4 files changed

+582
-322
lines changed

4 files changed

+582
-322
lines changed

spring-webflux/src/main/java/org/springframework/web/reactive/function/server/PathResourceLookupFunction.java

Lines changed: 6 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,14 @@
1818

1919
import java.io.IOException;
2020
import java.io.UncheckedIOException;
21-
import java.io.UnsupportedEncodingException;
22-
import java.net.URLDecoder;
23-
import java.nio.charset.StandardCharsets;
2421
import java.util.function.Function;
2522

2623
import reactor.core.publisher.Mono;
2724

28-
import org.springframework.core.io.ClassPathResource;
2925
import org.springframework.core.io.Resource;
30-
import org.springframework.core.io.UrlResource;
3126
import org.springframework.http.server.PathContainer;
3227
import org.springframework.util.Assert;
33-
import org.springframework.util.ResourceUtils;
34-
import org.springframework.util.StringUtils;
35-
import org.springframework.web.util.UriUtils;
28+
import org.springframework.web.reactive.resource.ResourceHandlerUtils;
3629
import org.springframework.web.util.pattern.PathPattern;
3730
import org.springframework.web.util.pattern.PathPatternParser;
3831

@@ -51,12 +44,11 @@ class PathResourceLookupFunction implements Function<ServerRequest, Mono<Resourc
5144

5245
public PathResourceLookupFunction(String pattern, Resource location) {
5346
Assert.hasLength(pattern, "'pattern' must not be empty");
54-
Assert.notNull(location, "'location' must not be null");
47+
ResourceHandlerUtils.assertResourceLocation(location);
5548
this.pattern = PathPatternParser.defaultInstance.parse(pattern);
5649
this.location = location;
5750
}
5851

59-
6052
@Override
6153
public Mono<Resource> apply(ServerRequest request) {
6254
PathContainer pathContainer = request.requestPath().pathWithinApplication();
@@ -65,21 +57,14 @@ public Mono<Resource> apply(ServerRequest request) {
6557
}
6658

6759
pathContainer = this.pattern.extractPathWithinPattern(pathContainer);
68-
String path = processPath(pathContainer.value());
69-
if (!StringUtils.hasText(path) || isInvalidPath(path)) {
70-
return Mono.empty();
71-
}
72-
if (isInvalidEncodedInputPath(path)) {
60+
String path = ResourceHandlerUtils.normalizeInputPath(pathContainer.value());
61+
if (ResourceHandlerUtils.shouldIgnoreInputPath(path)) {
7362
return Mono.empty();
7463
}
7564

76-
if (!(this.location instanceof UrlResource)) {
77-
path = UriUtils.decode(path, StandardCharsets.UTF_8);
78-
}
79-
8065
try {
81-
Resource resource = this.location.createRelative(path);
82-
if (resource.isReadable() && isResourceUnderLocation(resource)) {
66+
Resource resource = ResourceHandlerUtils.createRelativeResource(this.location, path);
67+
if (resource.isReadable() && ResourceHandlerUtils.isResourceUnderLocation(this.location, resource)) {
8368
return Mono.just(resource);
8469
}
8570
else {
@@ -91,150 +76,6 @@ public Mono<Resource> apply(ServerRequest request) {
9176
}
9277
}
9378

94-
/**
95-
* Process the given resource path.
96-
* <p>The default implementation replaces:
97-
* <ul>
98-
* <li>Backslash with forward slash.
99-
* <li>Duplicate occurrences of slash with a single slash.
100-
* <li>Any combination of leading slash and control characters (00-1F and 7F)
101-
* with a single "/" or "". For example {@code " / // foo/bar"}
102-
* becomes {@code "/foo/bar"}.
103-
* </ul>
104-
*/
105-
protected String processPath(String path) {
106-
path = StringUtils.replace(path, "\\", "/");
107-
path = cleanDuplicateSlashes(path);
108-
return cleanLeadingSlash(path);
109-
}
110-
111-
private String cleanDuplicateSlashes(String path) {
112-
StringBuilder sb = null;
113-
char prev = 0;
114-
for (int i = 0; i < path.length(); i++) {
115-
char curr = path.charAt(i);
116-
try {
117-
if (curr == '/' && prev == '/') {
118-
if (sb == null) {
119-
sb = new StringBuilder(path.substring(0, i));
120-
}
121-
continue;
122-
}
123-
if (sb != null) {
124-
sb.append(path.charAt(i));
125-
}
126-
}
127-
finally {
128-
prev = curr;
129-
}
130-
}
131-
return (sb != null ? sb.toString() : path);
132-
}
133-
134-
private String cleanLeadingSlash(String path) {
135-
boolean slash = false;
136-
for (int i = 0; i < path.length(); i++) {
137-
if (path.charAt(i) == '/') {
138-
slash = true;
139-
}
140-
else if (path.charAt(i) > ' ' && path.charAt(i) != 127) {
141-
if (i == 0 || (i == 1 && slash)) {
142-
return path;
143-
}
144-
return (slash ? "/" + path.substring(i) : path.substring(i));
145-
}
146-
}
147-
return (slash ? "/" : "");
148-
}
149-
150-
private boolean isInvalidPath(String path) {
151-
if (path.contains("WEB-INF") || path.contains("META-INF")) {
152-
return true;
153-
}
154-
if (path.contains(":/")) {
155-
String relativePath = (path.charAt(0) == '/' ? path.substring(1) : path);
156-
if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) {
157-
return true;
158-
}
159-
}
160-
return path.contains("..") && StringUtils.cleanPath(path).contains("../");
161-
}
162-
163-
/**
164-
* Check whether the given path contains invalid escape sequences.
165-
* @param path the path to validate
166-
* @return {@code true} if the path is invalid, {@code false} otherwise
167-
*/
168-
private boolean isInvalidEncodedInputPath(String path) {
169-
if (path.contains("%")) {
170-
try {
171-
// Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars
172-
String decodedPath = URLDecoder.decode(path, "UTF-8");
173-
if (isInvalidPath(decodedPath)) {
174-
return true;
175-
}
176-
decodedPath = processPath(decodedPath);
177-
if (isInvalidPath(decodedPath)) {
178-
return true;
179-
}
180-
}
181-
catch (IllegalArgumentException ex) {
182-
// May not be possible to decode...
183-
}
184-
catch (UnsupportedEncodingException ex) {
185-
throw new RuntimeException(ex);
186-
}
187-
}
188-
return false;
189-
}
190-
191-
private boolean isResourceUnderLocation(Resource resource) throws IOException {
192-
if (resource.getClass() != this.location.getClass()) {
193-
return false;
194-
}
195-
196-
String resourcePath;
197-
String locationPath;
198-
199-
if (resource instanceof UrlResource) {
200-
resourcePath = resource.getURL().toExternalForm();
201-
locationPath = StringUtils.cleanPath(this.location.getURL().toString());
202-
}
203-
else if (resource instanceof ClassPathResource) {
204-
resourcePath = ((ClassPathResource) resource).getPath();
205-
locationPath = StringUtils.cleanPath(((ClassPathResource) this.location).getPath());
206-
}
207-
else {
208-
resourcePath = resource.getURL().getPath();
209-
locationPath = StringUtils.cleanPath(this.location.getURL().getPath());
210-
}
211-
212-
if (locationPath.equals(resourcePath)) {
213-
return true;
214-
}
215-
locationPath = (locationPath.endsWith("/") || locationPath.isEmpty() ? locationPath : locationPath + "/");
216-
return (resourcePath.startsWith(locationPath) && !isInvalidEncodedInputPath(resourcePath));
217-
}
218-
219-
private boolean isInvalidEncodedResourcePath(String resourcePath) {
220-
if (resourcePath.contains("%")) {
221-
// Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars...
222-
try {
223-
String decodedPath = URLDecoder.decode(resourcePath, "UTF-8");
224-
if (decodedPath.contains("../") || decodedPath.contains("..\\")) {
225-
return true;
226-
}
227-
}
228-
catch (IllegalArgumentException ex) {
229-
// May not be possible to decode...
230-
}
231-
catch (UnsupportedEncodingException ex) {
232-
throw new RuntimeException(ex);
233-
}
234-
}
235-
return false;
236-
}
237-
23879
@Override
23980
public String toString() {
24081
return this.pattern + " -> " + this.location;

0 commit comments

Comments
 (0)