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
@@ -62,12 +66,15 @@ 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
}
75
+ if (!(this .location instanceof UrlResource )) {
76
+ path = UriUtils .decode (path , StandardCharsets .UTF_8 );
77
+ }
71
78
72
79
try {
73
80
Resource resource = this .location .createRelative (path );
@@ -83,7 +90,47 @@ public Optional<Resource> apply(ServerRequest request) {
83
90
}
84
91
}
85
92
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
+ */
86
104
private 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 ) {
87
134
boolean slash = false ;
88
135
for (int i = 0 ; i < path .length (); i ++) {
89
136
if (path .charAt (i ) == '/' ) {
@@ -93,8 +140,7 @@ else if (path.charAt(i) > ' ' && path.charAt(i) != 127) {
93
140
if (i == 0 || (i == 1 && slash )) {
94
141
return path ;
95
142
}
96
- path = slash ? "/" + path .substring (i ) : path .substring (i );
97
- return path ;
143
+ return (slash ? "/" + path .substring (i ) : path .substring (i ));
98
144
}
99
145
}
100
146
return (slash ? "/" : "" );
@@ -113,6 +159,34 @@ private boolean isInvalidPath(String path) {
113
159
return path .contains (".." ) && StringUtils .cleanPath (path ).contains ("../" );
114
160
}
115
161
162
+ /**
163
+ * Check whether the given path contains invalid escape sequences.
164
+ * @param path the path to validate
165
+ * @return {@code true} if the path is invalid, {@code false} otherwise
166
+ */
167
+ private boolean isInvalidEncodedInputPath (String path ) {
168
+ if (path .contains ("%" )) {
169
+ try {
170
+ // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars
171
+ String decodedPath = URLDecoder .decode (path , StandardCharsets .UTF_8 .name ());
172
+ if (isInvalidPath (decodedPath )) {
173
+ return true ;
174
+ }
175
+ decodedPath = processPath (decodedPath );
176
+ if (isInvalidPath (decodedPath )) {
177
+ return true ;
178
+ }
179
+ }
180
+ catch (IllegalArgumentException ex ) {
181
+ // May not be possible to decode...
182
+ }
183
+ catch (UnsupportedEncodingException ex ) {
184
+ // May not be possible to decode...
185
+ }
186
+ }
187
+ return false ;
188
+ }
189
+
116
190
private boolean isResourceUnderLocation (Resource resource ) throws IOException {
117
191
if (resource .getClass () != this .location .getClass ()) {
118
192
return false ;
@@ -129,6 +203,10 @@ else if (resource instanceof ClassPathResource) {
129
203
resourcePath = ((ClassPathResource ) resource ).getPath ();
130
204
locationPath = StringUtils .cleanPath (((ClassPathResource ) this .location ).getPath ());
131
205
}
206
+ else if (resource instanceof ServletContextResource ) {
207
+ resourcePath = ((ServletContextResource ) resource ).getPath ();
208
+ locationPath = StringUtils .cleanPath (((ServletContextResource ) this .location ).getPath ());
209
+ }
132
210
else {
133
211
resourcePath = resource .getURL ().getPath ();
134
212
locationPath = StringUtils .cleanPath (this .location .getURL ().getPath ());
@@ -138,13 +216,27 @@ else if (resource instanceof ClassPathResource) {
138
216
return true ;
139
217
}
140
218
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 ("../" );
219
+ return (resourcePath .startsWith (locationPath ) && !isInvalidEncodedResourcePath (resourcePath ));
146
220
}
147
221
222
+ private boolean isInvalidEncodedResourcePath (String resourcePath ) {
223
+ if (resourcePath .contains ("%" )) {
224
+ // Use URLDecoder (vs UriUtils) to preserve potentially decoded UTF-8 chars...
225
+ try {
226
+ String decodedPath = URLDecoder .decode (resourcePath , StandardCharsets .UTF_8 .name ());
227
+ if (decodedPath .contains ("../" ) || decodedPath .contains ("..\\ " )) {
228
+ return true ;
229
+ }
230
+ }
231
+ catch (IllegalArgumentException ex ) {
232
+ // May not be possible to decode...
233
+ }
234
+ catch (UnsupportedEncodingException ex ) {
235
+ // May not be possible to decode...
236
+ }
237
+ }
238
+ return false ;
239
+ }
148
240
149
241
@ Override
150
242
public String toString () {
0 commit comments