18
18
19
19
import java .io .IOException ;
20
20
import java .io .UncheckedIOException ;
21
+ import java .net .URLDecoder ;
21
22
import java .nio .charset .StandardCharsets ;
22
23
import java .util .Optional ;
23
24
import java .util .function .Function ;
29
30
import org .springframework .util .Assert ;
30
31
import org .springframework .util .ResourceUtils ;
31
32
import org .springframework .util .StringUtils ;
33
+ import org .springframework .web .context .support .ServletContextResource ;
34
+ import org .springframework .web .util .UriUtils ;
32
35
import org .springframework .web .util .pattern .PathPattern ;
33
36
import org .springframework .web .util .pattern .PathPatternParser ;
34
37
35
38
/**
36
39
* Lookup function used by {@link RouterFunctions#resources(String, Resource)}.
37
40
*
38
41
* @author Arjen Poutsma
42
+ * @author Rossen Stoyanchev
39
43
* @since 5.2
40
44
*/
41
45
class PathResourceLookupFunction implements Function <ServerRequest , Optional <Resource >> {
@@ -62,13 +66,17 @@ public Optional<Resource> apply(ServerRequest request) {
62
66
63
67
pathContainer = this .pattern .extractPathWithinPattern (pathContainer );
64
68
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 ( );
67
71
}
68
- if (! StringUtils . hasLength ( path ) || isInvalidPath (path )) {
72
+ if (isInvalidEncodedInputPath (path )) {
69
73
return Optional .empty ();
70
74
}
71
75
76
+ if (!(this .location instanceof UrlResource )) {
77
+ path = UriUtils .decode (path , StandardCharsets .UTF_8 );
78
+ }
79
+
72
80
try {
73
81
Resource resource = this .location .createRelative (path );
74
82
if (resource .isReadable () && isResourceUnderLocation (resource )) {
@@ -83,7 +91,47 @@ public Optional<Resource> apply(ServerRequest request) {
83
91
}
84
92
}
85
93
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 ) {
87
135
boolean slash = false ;
88
136
for (int i = 0 ; i < path .length (); i ++) {
89
137
if (path .charAt (i ) == '/' ) {
@@ -93,8 +141,7 @@ else if (path.charAt(i) > ' ' && path.charAt(i) != 127) {
93
141
if (i == 0 || (i == 1 && slash )) {
94
142
return path ;
95
143
}
96
- path = slash ? "/" + path .substring (i ) : path .substring (i );
97
- return path ;
144
+ return (slash ? "/" + path .substring (i ) : path .substring (i ));
98
145
}
99
146
}
100
147
return (slash ? "/" : "" );
@@ -113,6 +160,26 @@ private boolean isInvalidPath(String path) {
113
160
return path .contains (".." ) && StringUtils .cleanPath (path ).contains ("../" );
114
161
}
115
162
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
+
116
183
private boolean isResourceUnderLocation (Resource resource ) throws IOException {
117
184
if (resource .getClass () != this .location .getClass ()) {
118
185
return false ;
@@ -129,6 +196,10 @@ else if (resource instanceof ClassPathResource classPathResource) {
129
196
resourcePath = classPathResource .getPath ();
130
197
locationPath = StringUtils .cleanPath (((ClassPathResource ) this .location ).getPath ());
131
198
}
199
+ else if (resource instanceof ServletContextResource servletContextResource ) {
200
+ resourcePath = servletContextResource .getPath ();
201
+ locationPath = StringUtils .cleanPath (((ServletContextResource ) this .location ).getPath ());
202
+ }
132
203
else {
133
204
resourcePath = resource .getURL ().getPath ();
134
205
locationPath = StringUtils .cleanPath (this .location .getURL ().getPath ());
@@ -138,13 +209,24 @@ else if (resource instanceof ClassPathResource classPathResource) {
138
209
return true ;
139
210
}
140
211
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 ));
146
213
}
147
214
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
+ }
148
230
149
231
@ Override
150
232
public String toString () {
0 commit comments