1
1
/*
2
- * Copyright 2002-2021 the original author or authors.
2
+ * Copyright 2002-2023 the original author or authors.
3
3
*
4
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
5
* you may not use this file except in compliance with the License.
18
18
19
19
import java .io .IOException ;
20
20
import java .io .UncheckedIOException ;
21
+ import java .io .UnsupportedEncodingException ;
22
+ import java .net .URLDecoder ;
21
23
import java .nio .charset .StandardCharsets ;
22
24
import java .util .Optional ;
23
25
import java .util .function .Function ;
29
31
import org .springframework .util .Assert ;
30
32
import org .springframework .util .ResourceUtils ;
31
33
import org .springframework .util .StringUtils ;
34
+ import org .springframework .web .context .support .ServletContextResource ;
35
+ import org .springframework .web .util .UriUtils ;
32
36
import org .springframework .web .util .pattern .PathPattern ;
33
37
import org .springframework .web .util .pattern .PathPatternParser ;
34
38
35
39
/**
36
40
* Lookup function used by {@link RouterFunctions#resources(String, Resource)}.
37
41
*
38
42
* @author Arjen Poutsma
43
+ * @author Rossen Stoyanchev
39
44
* @since 5.2
40
45
*/
41
46
class PathResourceLookupFunction implements Function <ServerRequest , Optional <Resource >> {
@@ -62,13 +67,17 @@ public Optional<Resource> apply(ServerRequest request) {
62
67
63
68
pathContainer = this .pattern .extractPathWithinPattern (pathContainer );
64
69
String path = processPath (pathContainer .value ());
65
- if (path . contains ( "%" )) {
66
- path = StringUtils . uriDecode ( path , StandardCharsets . UTF_8 );
70
+ if (! StringUtils . hasText ( path ) || isInvalidPath ( path )) {
71
+ return Optional . empty ( );
67
72
}
68
- if (! StringUtils . hasLength ( path ) || isInvalidPath (path )) {
73
+ if (isInvalidEncodedInputPath (path )) {
69
74
return Optional .empty ();
70
75
}
71
76
77
+ if (!(this .location instanceof UrlResource )) {
78
+ path = UriUtils .decode (path , StandardCharsets .UTF_8 );
79
+ }
80
+
72
81
try {
73
82
Resource resource = this .location .createRelative (path );
74
83
if (resource .isReadable () && isResourceUnderLocation (resource )) {
@@ -83,7 +92,47 @@ public Optional<Resource> apply(ServerRequest request) {
83
92
}
84
93
}
85
94
86
- private String processPath (String path ) {
95
+ /**
96
+ * Process the given resource path.
97
+ * <p>The default implementation replaces:
98
+ * <ul>
99
+ * <li>Backslash with forward slash.
100
+ * <li>Duplicate occurrences of slash with a single slash.
101
+ * <li>Any combination of leading slash and control characters (00-1F and 7F)
102
+ * with a single "/" or "". For example {@code " / // foo/bar"}
103
+ * becomes {@code "/foo/bar"}.
104
+ * </ul>
105
+ */
106
+ protected String processPath (String path ) {
107
+ path = StringUtils .replace (path , "\\ " , "/" );
108
+ path = cleanDuplicateSlashes (path );
109
+ return cleanLeadingSlash (path );
110
+ }
111
+
112
+ private String cleanDuplicateSlashes (String path ) {
113
+ StringBuilder sb = null ;
114
+ char prev = 0 ;
115
+ for (int i = 0 ; i < path .length (); i ++) {
116
+ char curr = path .charAt (i );
117
+ try {
118
+ if ((curr == '/' ) && (prev == '/' )) {
119
+ if (sb == null ) {
120
+ sb = new StringBuilder (path .substring (0 , i ));
121
+ }
122
+ continue ;
123
+ }
124
+ if (sb != null ) {
125
+ sb .append (path .charAt (i ));
126
+ }
127
+ }
128
+ finally {
129
+ prev = curr ;
130
+ }
131
+ }
132
+ return sb != null ? sb .toString () : path ;
133
+ }
134
+
135
+ private String cleanLeadingSlash (String path ) {
87
136
boolean slash = false ;
88
137
for (int i = 0 ; i < path .length (); i ++) {
89
138
if (path .charAt (i ) == '/' ) {
@@ -93,8 +142,7 @@ else if (path.charAt(i) > ' ' && path.charAt(i) != 127) {
93
142
if (i == 0 || (i == 1 && slash )) {
94
143
return path ;
95
144
}
96
- path = slash ? "/" + path .substring (i ) : path .substring (i );
97
- return path ;
145
+ return (slash ? "/" + path .substring (i ) : path .substring (i ));
98
146
}
99
147
}
100
148
return (slash ? "/" : "" );
@@ -113,6 +161,29 @@ private boolean isInvalidPath(String path) {
113
161
return path .contains (".." ) && StringUtils .cleanPath (path ).contains ("../" );
114
162
}
115
163
164
+ private boolean isInvalidEncodedInputPath (String path ) {
165
+ if (path .contains ("%" )) {
166
+ try {
167
+ // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars
168
+ String decodedPath = URLDecoder .decode (path , "UTF-8" );
169
+ if (isInvalidPath (decodedPath )) {
170
+ return true ;
171
+ }
172
+ decodedPath = processPath (decodedPath );
173
+ if (isInvalidPath (decodedPath )) {
174
+ return true ;
175
+ }
176
+ }
177
+ catch (IllegalArgumentException ex ) {
178
+ // May not be possible to decode...
179
+ }
180
+ catch (UnsupportedEncodingException ex ) {
181
+ throw new RuntimeException (ex );
182
+ }
183
+ }
184
+ return false ;
185
+ }
186
+
116
187
private boolean isResourceUnderLocation (Resource resource ) throws IOException {
117
188
if (resource .getClass () != this .location .getClass ()) {
118
189
return false ;
@@ -129,6 +200,10 @@ else if (resource instanceof ClassPathResource) {
129
200
resourcePath = ((ClassPathResource ) resource ).getPath ();
130
201
locationPath = StringUtils .cleanPath (((ClassPathResource ) this .location ).getPath ());
131
202
}
203
+ else if (resource instanceof ServletContextResource ) {
204
+ resourcePath = ((ServletContextResource ) resource ).getPath ();
205
+ locationPath = StringUtils .cleanPath (((ServletContextResource ) this .location ).getPath ());
206
+ }
132
207
else {
133
208
resourcePath = resource .getURL ().getPath ();
134
209
locationPath = StringUtils .cleanPath (this .location .getURL ().getPath ());
@@ -138,13 +213,27 @@ else if (resource instanceof ClassPathResource) {
138
213
return true ;
139
214
}
140
215
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 ("../" );
216
+ return (resourcePath .startsWith (locationPath ) && !isInvalidEncodedResourcePath (resourcePath ));
146
217
}
147
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
+ }
148
237
149
238
@ Override
150
239
public String toString () {
0 commit comments