18
18
19
19
import java .io .IOException ;
20
20
import java .io .InputStream ;
21
+ import java .net .URLDecoder ;
21
22
import java .util .ArrayList ;
22
23
import java .util .List ;
24
+
23
25
import javax .activation .FileTypeMap ;
24
26
import javax .activation .MimetypesFileTypeMap ;
25
27
import javax .servlet .ServletException ;
28
30
29
31
import org .apache .commons .logging .Log ;
30
32
import org .apache .commons .logging .LogFactory ;
31
-
32
33
import org .springframework .beans .factory .InitializingBean ;
33
34
import org .springframework .core .io .ClassPathResource ;
34
35
import org .springframework .core .io .Resource ;
35
36
import org .springframework .http .MediaType ;
36
37
import org .springframework .util .Assert ;
37
38
import org .springframework .util .ClassUtils ;
38
39
import org .springframework .util .CollectionUtils ;
40
+ import org .springframework .util .ObjectUtils ;
41
+ import org .springframework .util .ResourceUtils ;
39
42
import org .springframework .util .StreamUtils ;
40
43
import org .springframework .util .StringUtils ;
41
44
import org .springframework .web .HttpRequestHandler ;
@@ -158,6 +161,29 @@ public void afterPropertiesSet() throws Exception {
158
161
logger .warn ("Locations list is empty. No resources will be served unless a " +
159
162
"custom ResourceResolver is configured as an alternative to PathResourceResolver." );
160
163
}
164
+ initAllowedLocations ();
165
+ }
166
+
167
+ /**
168
+ * Look for a {@link org.springframework.web.servlet.resource.PathResourceResolver}
169
+ * among the {@link #getResourceResolvers() resource resolvers} and configure
170
+ * its {@code "allowedLocations"} to match the value of the
171
+ * {@link #setLocations(java.util.List) locations} property unless the "allowed
172
+ * locations" of the {@code PathResourceResolver} is non-empty.
173
+ */
174
+ protected void initAllowedLocations () {
175
+ if (CollectionUtils .isEmpty (this .locations )) {
176
+ return ;
177
+ }
178
+ for (int i = getResourceResolvers ().size ()-1 ; i >= 0 ; i --) {
179
+ if (getResourceResolvers ().get (i ) instanceof PathResourceResolver ) {
180
+ PathResourceResolver pathResolver = (PathResourceResolver ) getResourceResolvers ().get (i );
181
+ if (ObjectUtils .isEmpty (pathResolver .getAllowedLocations ())) {
182
+ pathResolver .setAllowedLocations (getLocations ().toArray (new Resource [getLocations ().size ()]));
183
+ }
184
+ break ;
185
+ }
186
+ }
161
187
}
162
188
163
189
/**
@@ -214,18 +240,33 @@ public void handleRequest(HttpServletRequest request, HttpServletResponse respon
214
240
writeContent (response , resource );
215
241
}
216
242
217
- protected Resource getResource (HttpServletRequest request ) throws IOException {
243
+ protected Resource getResource (HttpServletRequest request ) throws IOException {
218
244
String path = (String ) request .getAttribute (HandlerMapping .PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE );
219
245
if (path == null ) {
220
246
throw new IllegalStateException ("Required request attribute '" +
221
247
HandlerMapping .PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE + "' is not set" );
222
248
}
249
+ path = processPath (path );
223
250
if (!StringUtils .hasText (path ) || isInvalidPath (path )) {
224
251
if (logger .isTraceEnabled ()) {
225
252
logger .trace ("Ignoring invalid resource path [" + path + "]" );
226
253
}
227
254
return null ;
228
255
}
256
+ if (path .contains ("%" )) {
257
+ try {
258
+ // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars
259
+ if (isInvalidPath (URLDecoder .decode (path , "UTF-8" ))) {
260
+ if (logger .isTraceEnabled ()) {
261
+ logger .trace ("Ignoring invalid resource path with escape sequences [" + path + "]." );
262
+ }
263
+ return null ;
264
+ }
265
+ }
266
+ catch (IllegalArgumentException ex ) {
267
+ // ignore
268
+ }
269
+ }
229
270
ResourceResolverChain resolveChain = new DefaultResourceResolverChain (getResourceResolvers ());
230
271
Resource resource = resolveChain .resolveResource (request , path , getLocations ());
231
272
if (resource == null || getResourceTransformers ().isEmpty ()) {
@@ -237,14 +278,76 @@ protected Resource getResource(HttpServletRequest request) throws IOException{
237
278
}
238
279
239
280
/**
240
- * Validates the given path: returns {@code true} if the given path is not a valid resource path.
241
- * <p>The default implementation rejects paths containing "WEB-INF" or "META-INF" as well as paths
242
- * with relative paths ("../") that result in access of a parent directory.
281
+ * Process the given resource path to be used.
282
+ * <p>The default implementation replaces any combination of leading '/' and
283
+ * control characters (00-1F and 7F) with a single "/" or "". For example
284
+ * {@code " // /// //// foo/bar"} becomes {@code "/foo/bar"}.
285
+ * @since 3.2.12
286
+ */
287
+ protected String processPath (String path ) {
288
+ boolean slash = false ;
289
+ for (int i = 0 ; i < path .length (); i ++) {
290
+ if (path .charAt (i ) == '/' ) {
291
+ slash = true ;
292
+ }
293
+ else if (path .charAt (i ) > ' ' && path .charAt (i ) != 127 ) {
294
+ if (i == 0 || (i == 1 && slash )) {
295
+ return path ;
296
+ }
297
+ path = slash ? "/" + path .substring (i ) : path .substring (i );
298
+ if (logger .isTraceEnabled ()) {
299
+ logger .trace ("Path after trimming leading '/' and control characters: " + path );
300
+ }
301
+ return path ;
302
+ }
303
+ }
304
+ return (slash ? "/" : "" );
305
+ }
306
+
307
+ /**
308
+ * Identifies invalid resource paths. By default rejects:
309
+ * <ul>
310
+ * <li>Paths that contain "WEB-INF" or "META-INF"
311
+ * <li>Paths that contain "../" after a call to
312
+ * {@link org.springframework.util.StringUtils#cleanPath}.
313
+ * <li>Paths that represent a {@link org.springframework.util.ResourceUtils#isUrl
314
+ * valid URL} or would represent one after the leading slash is removed.
315
+ * </ul>
316
+ * <p><strong>Note:</strong> this method assumes that leading, duplicate '/'
317
+ * or control characters (e.g. white space) have been trimmed so that the
318
+ * path starts predictably with a single '/' or does not have one.
243
319
* @param path the path to validate
244
- * @return {@code true} if the path has been recognized as invalid, {@code false} otherwise
320
+ * @return {@code true} if the path is invalid, {@code false} otherwise
245
321
*/
246
322
protected boolean isInvalidPath (String path ) {
247
- return (path .contains ("WEB-INF" ) || path .contains ("META-INF" ) || StringUtils .cleanPath (path ).startsWith (".." ));
323
+ if (logger .isTraceEnabled ()) {
324
+ logger .trace ("Applying \" invalid path\" checks to path: " + path );
325
+ }
326
+ if (path .contains ("WEB-INF" ) || path .contains ("META-INF" )) {
327
+ if (logger .isTraceEnabled ()) {
328
+ logger .trace ("Path contains \" WEB-INF\" or \" META-INF\" ." );
329
+ }
330
+ return true ;
331
+ }
332
+ if (path .contains (":/" )) {
333
+ String relativePath = (path .charAt (0 ) == '/' ? path .substring (1 ) : path );
334
+ if (ResourceUtils .isUrl (relativePath ) || relativePath .startsWith ("url:" )) {
335
+ if (logger .isTraceEnabled ()) {
336
+ logger .trace ("Path represents URL or has \" url:\" prefix." );
337
+ }
338
+ return true ;
339
+ }
340
+ }
341
+ if (path .contains ("../" )) {
342
+ path = StringUtils .cleanPath (path );
343
+ if (path .contains ("../" )) {
344
+ if (logger .isTraceEnabled ()) {
345
+ logger .trace ("Path contains \" ../\" after call to StringUtils#cleanPath." );
346
+ }
347
+ return true ;
348
+ }
349
+ }
350
+ return false ;
248
351
}
249
352
250
353
/**
0 commit comments