Skip to content

Commit c6fa180

Browse files
committed
Extract shared resource handling utility methods
Closes: gh-33574
1 parent df5489b commit c6fa180

File tree

8 files changed

+493
-657
lines changed

8 files changed

+493
-657
lines changed

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

Lines changed: 5 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,14 @@
1818

1919
import java.io.IOException;
2020
import java.io.UncheckedIOException;
21-
import java.net.URLDecoder;
22-
import java.nio.charset.StandardCharsets;
2321
import java.util.function.Function;
2422

2523
import reactor.core.publisher.Mono;
2624

27-
import org.springframework.core.io.ClassPathResource;
2825
import org.springframework.core.io.Resource;
29-
import org.springframework.core.io.UrlResource;
3026
import org.springframework.http.server.PathContainer;
3127
import org.springframework.util.Assert;
32-
import org.springframework.util.ResourceUtils;
33-
import org.springframework.util.StringUtils;
34-
import org.springframework.web.util.UriUtils;
28+
import org.springframework.web.reactive.resource.ResourceHandlerUtils;
3529
import org.springframework.web.util.pattern.PathPattern;
3630
import org.springframework.web.util.pattern.PathPatternParser;
3731

@@ -64,21 +58,14 @@ public Mono<Resource> apply(ServerRequest request) {
6458
}
6559

6660
pathContainer = this.pattern.extractPathWithinPattern(pathContainer);
67-
String path = processPath(pathContainer.value());
68-
if (!StringUtils.hasText(path) || isInvalidPath(path)) {
61+
String path = ResourceHandlerUtils.normalizeInputPath(pathContainer.value());
62+
if (ResourceHandlerUtils.shouldIgnoreInputPath(path)) {
6963
return Mono.empty();
7064
}
71-
if (isInvalidEncodedInputPath(path)) {
72-
return Mono.empty();
73-
}
74-
75-
if (!(this.location instanceof UrlResource)) {
76-
path = UriUtils.decode(path, StandardCharsets.UTF_8);
77-
}
7865

7966
try {
80-
Resource resource = this.location.createRelative(path);
81-
if (resource.isReadable() && isResourceUnderLocation(resource)) {
67+
Resource resource = ResourceHandlerUtils.createRelativeResource(this.location, path);
68+
if (resource.isReadable() && ResourceHandlerUtils.isResourceUnderLocation(this.location, resource)) {
8269
return Mono.just(resource);
8370
}
8471
else {
@@ -90,147 +77,6 @@ public Mono<Resource> apply(ServerRequest request) {
9077
}
9178
}
9279

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

spring-webflux/src/main/java/org/springframework/web/reactive/resource/PathResourceResolver.java

Lines changed: 4 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,22 +17,17 @@
1717
package org.springframework.web.reactive.resource;
1818

1919
import java.io.IOException;
20-
import java.net.URLDecoder;
21-
import java.nio.charset.StandardCharsets;
2220
import java.util.Arrays;
2321
import java.util.List;
2422

2523
import reactor.core.publisher.Flux;
2624
import reactor.core.publisher.Mono;
2725

28-
import org.springframework.core.io.ClassPathResource;
2926
import org.springframework.core.io.Resource;
30-
import org.springframework.core.io.UrlResource;
3127
import org.springframework.core.log.LogFormatUtils;
3228
import org.springframework.lang.Nullable;
3329
import org.springframework.util.StringUtils;
3430
import org.springframework.web.server.ServerWebExchange;
35-
import org.springframework.web.util.UriUtils;
3631

3732
/**
3833
* A simple {@code ResourceResolver} that tries to find a resource under the given
@@ -111,10 +106,7 @@ private Mono<Resource> getResource(String resourcePath, List<? extends Resource>
111106
*/
112107
protected Mono<Resource> getResource(String resourcePath, Resource location) {
113108
try {
114-
if (!(location instanceof UrlResource)) {
115-
resourcePath = UriUtils.decode(resourcePath, StandardCharsets.UTF_8);
116-
}
117-
Resource resource = location.createRelative(resourcePath);
109+
Resource resource = ResourceHandlerUtils.createRelativeResource(location, resourcePath);
118110
if (resource.isReadable()) {
119111
if (checkResource(resource, location)) {
120112
return Mono.just(resource);
@@ -154,61 +146,15 @@ else if (logger.isWarnEnabled()) {
154146
* @return "true" if resource is in a valid location, "false" otherwise
155147
*/
156148
protected boolean checkResource(Resource resource, Resource location) throws IOException {
157-
if (isResourceUnderLocation(resource, location)) {
149+
if (ResourceHandlerUtils.isResourceUnderLocation(location, resource)) {
158150
return true;
159151
}
160152
if (getAllowedLocations() != null) {
161153
for (Resource current : getAllowedLocations()) {
162-
if (isResourceUnderLocation(resource, current)) {
163-
return true;
164-
}
165-
}
166-
}
167-
return false;
168-
}
169-
170-
private boolean isResourceUnderLocation(Resource resource, Resource location) throws IOException {
171-
if (resource.getClass() != location.getClass()) {
172-
return false;
173-
}
174-
175-
String resourcePath;
176-
String locationPath;
177-
178-
if (resource instanceof UrlResource) {
179-
resourcePath = resource.getURL().toExternalForm();
180-
locationPath = StringUtils.cleanPath(location.getURL().toString());
181-
}
182-
else if (resource instanceof ClassPathResource classPathResource) {
183-
resourcePath = classPathResource.getPath();
184-
locationPath = StringUtils.cleanPath(((ClassPathResource) location).getPath());
185-
}
186-
else {
187-
resourcePath = resource.getURL().getPath();
188-
locationPath = StringUtils.cleanPath(location.getURL().getPath());
189-
}
190-
191-
if (locationPath.equals(resourcePath)) {
192-
return true;
193-
}
194-
locationPath = (locationPath.endsWith("/") || locationPath.isEmpty() ? locationPath : locationPath + "/");
195-
return (resourcePath.startsWith(locationPath) && !isInvalidEncodedPath(resourcePath));
196-
}
197-
198-
private boolean isInvalidEncodedPath(String resourcePath) {
199-
if (resourcePath.contains("%")) {
200-
// Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars...
201-
try {
202-
String decodedPath = URLDecoder.decode(resourcePath, StandardCharsets.UTF_8);
203-
if (decodedPath.contains("../") || decodedPath.contains("..\\")) {
204-
logger.warn(LogFormatUtils.formatValue(
205-
"Resolved resource path contains encoded \"../\" or \"..\\\": " + resourcePath, -1, true));
154+
if (ResourceHandlerUtils.isResourceUnderLocation(current, resource)) {
206155
return true;
207156
}
208157
}
209-
catch (IllegalArgumentException ex) {
210-
// May not be possible to decode...
211-
}
212158
}
213159
return false;
214160
}

0 commit comments

Comments
 (0)