1818
1919import java .io .IOException ;
2020import java .io .UncheckedIOException ;
21+ import java .net .URLDecoder ;
2122import java .nio .charset .StandardCharsets ;
2223import java .util .Optional ;
2324import java .util .function .Function ;
2930import org .springframework .util .Assert ;
3031import org .springframework .util .ResourceUtils ;
3132import org .springframework .util .StringUtils ;
33+ import org .springframework .web .context .support .ServletContextResource ;
34+ import org .springframework .web .util .UriUtils ;
3235import org .springframework .web .util .pattern .PathPattern ;
3336import 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 */
4145class 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