Skip to content

Commit d86bf8b

Browse files
committed
Align RouterFunctions resource handling
Closes: gh-33434
1 parent c77de8a commit d86bf8b

File tree

2 files changed

+186
-25
lines changed

2 files changed

+186
-25
lines changed

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

Lines changed: 93 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 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.
@@ -18,6 +18,7 @@
1818

1919
import java.io.IOException;
2020
import java.io.UncheckedIOException;
21+
import java.net.URLDecoder;
2122
import java.nio.charset.StandardCharsets;
2223
import java.util.function.Function;
2324

@@ -30,6 +31,7 @@
3031
import org.springframework.util.Assert;
3132
import org.springframework.util.ResourceUtils;
3233
import org.springframework.util.StringUtils;
34+
import org.springframework.web.util.UriUtils;
3335
import org.springframework.web.util.pattern.PathPattern;
3436
import org.springframework.web.util.pattern.PathPatternParser;
3537

@@ -63,13 +65,17 @@ public Mono<Resource> apply(ServerRequest request) {
6365

6466
pathContainer = this.pattern.extractPathWithinPattern(pathContainer);
6567
String path = processPath(pathContainer.value());
66-
if (path.contains("%")) {
67-
path = StringUtils.uriDecode(path, StandardCharsets.UTF_8);
68+
if (!StringUtils.hasText(path) || isInvalidPath(path)) {
69+
return Mono.empty();
6870
}
69-
if (!StringUtils.hasLength(path) || isInvalidPath(path)) {
71+
if (isInvalidEncodedInputPath(path)) {
7072
return Mono.empty();
7173
}
7274

75+
if (!(this.location instanceof UrlResource)) {
76+
path = UriUtils.decode(path, StandardCharsets.UTF_8);
77+
}
78+
7379
try {
7480
Resource resource = this.location.createRelative(path);
7581
if (resource.isReadable() && isResourceUnderLocation(resource)) {
@@ -84,7 +90,47 @@ public Mono<Resource> apply(ServerRequest request) {
8490
}
8591
}
8692

87-
private String processPath(String path) {
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) {
88134
boolean slash = false;
89135
for (int i = 0; i < path.length(); i++) {
90136
if (path.charAt(i) == '/') {
@@ -94,8 +140,7 @@ else if (path.charAt(i) > ' ' && path.charAt(i) != 127) {
94140
if (i == 0 || (i == 1 && slash)) {
95141
return path;
96142
}
97-
path = slash ? "/" + path.substring(i) : path.substring(i);
98-
return path;
143+
return (slash ? "/" + path.substring(i) : path.substring(i));
99144
}
100145
}
101146
return (slash ? "/" : "");
@@ -117,6 +162,31 @@ private boolean isInvalidPath(String path) {
117162
return false;
118163
}
119164

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+
120190
private boolean isResourceUnderLocation(Resource resource) throws IOException {
121191
if (resource.getClass() != this.location.getClass()) {
122192
return false;
@@ -142,15 +212,24 @@ else if (resource instanceof ClassPathResource classPathResource) {
142212
return true;
143213
}
144214
locationPath = (locationPath.endsWith("/") || locationPath.isEmpty() ? locationPath : locationPath + "/");
145-
if (!resourcePath.startsWith(locationPath)) {
146-
return false;
147-
}
148-
if (resourcePath.contains("%") && StringUtils.uriDecode(resourcePath, StandardCharsets.UTF_8).contains("../")) {
149-
return false;
150-
}
151-
return true;
215+
return (resourcePath.startsWith(locationPath) && !isInvalidEncodedInputPath(resourcePath));
152216
}
153217

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+
}
154233

155234
@Override
156235
public String toString() {

spring-webmvc/src/main/java/org/springframework/web/servlet/function/PathResourceLookupFunction.java

Lines changed: 93 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.io.IOException;
2020
import java.io.UncheckedIOException;
21+
import java.net.URLDecoder;
2122
import java.nio.charset.StandardCharsets;
2223
import java.util.Optional;
2324
import java.util.function.Function;
@@ -29,13 +30,16 @@
2930
import org.springframework.util.Assert;
3031
import org.springframework.util.ResourceUtils;
3132
import org.springframework.util.StringUtils;
33+
import org.springframework.web.context.support.ServletContextResource;
34+
import org.springframework.web.util.UriUtils;
3235
import org.springframework.web.util.pattern.PathPattern;
3336
import org.springframework.web.util.pattern.PathPatternParser;
3437

3538
/**
3639
* Lookup function used by {@link RouterFunctions#resources(String, Resource)}.
3740
*
3841
* @author Arjen Poutsma
42+
* @author Rossen Stoyanchev
3943
* @since 5.2
4044
*/
4145
class PathResourceLookupFunction implements Function<ServerRequest, Optional<Resource>> {
@@ -62,13 +66,17 @@ public Optional<Resource> apply(ServerRequest request) {
6266

6367
pathContainer = this.pattern.extractPathWithinPattern(pathContainer);
6468
String path = processPath(pathContainer.value());
65-
if (path.contains("%")) {
66-
path = StringUtils.uriDecode(path, StandardCharsets.UTF_8);
69+
if (!StringUtils.hasText(path) || isInvalidPath(path)) {
70+
return Optional.empty();
6771
}
68-
if (!StringUtils.hasLength(path) || isInvalidPath(path)) {
72+
if (isInvalidEncodedInputPath(path)) {
6973
return Optional.empty();
7074
}
7175

76+
if (!(this.location instanceof UrlResource)) {
77+
path = UriUtils.decode(path, StandardCharsets.UTF_8);
78+
}
79+
7280
try {
7381
Resource resource = this.location.createRelative(path);
7482
if (resource.isReadable() && isResourceUnderLocation(resource)) {
@@ -83,7 +91,47 @@ public Optional<Resource> apply(ServerRequest request) {
8391
}
8492
}
8593

86-
private String processPath(String path) {
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) {
87135
boolean slash = false;
88136
for (int i = 0; i < path.length(); i++) {
89137
if (path.charAt(i) == '/') {
@@ -93,8 +141,7 @@ else if (path.charAt(i) > ' ' && path.charAt(i) != 127) {
93141
if (i == 0 || (i == 1 && slash)) {
94142
return path;
95143
}
96-
path = slash ? "/" + path.substring(i) : path.substring(i);
97-
return path;
144+
return (slash ? "/" + path.substring(i) : path.substring(i));
98145
}
99146
}
100147
return (slash ? "/" : "");
@@ -113,6 +160,26 @@ private boolean isInvalidPath(String path) {
113160
return path.contains("..") && StringUtils.cleanPath(path).contains("../");
114161
}
115162

163+
private boolean isInvalidEncodedInputPath(String path) {
164+
if (path.contains("%")) {
165+
try {
166+
// Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars
167+
String decodedPath = URLDecoder.decode(path, StandardCharsets.UTF_8);
168+
if (isInvalidPath(decodedPath)) {
169+
return true;
170+
}
171+
decodedPath = processPath(decodedPath);
172+
if (isInvalidPath(decodedPath)) {
173+
return true;
174+
}
175+
}
176+
catch (IllegalArgumentException ex) {
177+
// May not be possible to decode...
178+
}
179+
}
180+
return false;
181+
}
182+
116183
private boolean isResourceUnderLocation(Resource resource) throws IOException {
117184
if (resource.getClass() != this.location.getClass()) {
118185
return false;
@@ -129,6 +196,10 @@ else if (resource instanceof ClassPathResource classPathResource) {
129196
resourcePath = classPathResource.getPath();
130197
locationPath = StringUtils.cleanPath(((ClassPathResource) this.location).getPath());
131198
}
199+
else if (resource instanceof ServletContextResource servletContextResource) {
200+
resourcePath = servletContextResource.getPath();
201+
locationPath = StringUtils.cleanPath(((ServletContextResource) this.location).getPath());
202+
}
132203
else {
133204
resourcePath = resource.getURL().getPath();
134205
locationPath = StringUtils.cleanPath(this.location.getURL().getPath());
@@ -138,13 +209,24 @@ else if (resource instanceof ClassPathResource classPathResource) {
138209
return true;
139210
}
140211
locationPath = (locationPath.endsWith("/") || locationPath.isEmpty() ? locationPath : locationPath + "/");
141-
if (!resourcePath.startsWith(locationPath)) {
142-
return false;
143-
}
144-
return !resourcePath.contains("%") ||
145-
!StringUtils.uriDecode(resourcePath, StandardCharsets.UTF_8).contains("../");
212+
return (resourcePath.startsWith(locationPath) && !isInvalidEncodedResourcePath(resourcePath));
146213
}
147214

215+
private boolean isInvalidEncodedResourcePath(String resourcePath) {
216+
if (resourcePath.contains("%")) {
217+
// Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars...
218+
try {
219+
String decodedPath = URLDecoder.decode(resourcePath, StandardCharsets.UTF_8);
220+
if (decodedPath.contains("../") || decodedPath.contains("..\\")) {
221+
return true;
222+
}
223+
}
224+
catch (IllegalArgumentException ex) {
225+
// May not be possible to decode...
226+
}
227+
}
228+
return false;
229+
}
148230

149231
@Override
150232
public String toString() {

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy