Skip to content

Commit e4cb25f

Browse files
committed
Reject "/path/**/other" patterns in PathPatternParser
Prior to this commit, patterns like `"/path/**/other"` would be treated as `"/path/*/other"` (single wildcard, i.e. matching zero to many chars within a path segment). This will not match multiple segments, as expected by `AntPathMatcher` users or by `PathPatternParser` users when in patterns like `"/resource/**"`. This commit now rejects patterns like `"/path/**/other"` as invalid. This behavior was previously warned against since gh-24958. Closes gh-24952
1 parent 7bcda3a commit e4cb25f

File tree

5 files changed

+35
-40
lines changed

5 files changed

+35
-40
lines changed

spring-web/src/main/java/org/springframework/web/util/pattern/InternalPathPatternParser.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ else if (ch == '}' && !previousBackslash) {
236236

237237
/**
238238
* After processing a separator, a quick peek whether it is followed by
239-
* (and only before the end of the pattern or the next separator).
239+
* a double wildcard (and only as the last path element).
240240
*/
241241
private boolean peekDoubleWildcard() {
242242
if ((this.pos + 2) >= this.pathPatternLength) {
@@ -245,6 +245,11 @@ private boolean peekDoubleWildcard() {
245245
if (this.pathPatternData[this.pos + 1] != '*' || this.pathPatternData[this.pos + 2] != '*') {
246246
return false;
247247
}
248+
char separator = this.parser.getPathOptions().separator();
249+
if ((this.pos + 3) < this.pathPatternLength && this.pathPatternData[this.pos + 3] == separator) {
250+
throw new PatternParseException(this.pos, this.pathPatternData,
251+
PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
252+
}
248253
return (this.pos + 3 == this.pathPatternLength);
249254
}
250255

spring-web/src/main/java/org/springframework/web/util/pattern/PathPatternParser.java

-10
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@
1616

1717
package org.springframework.web.util.pattern;
1818

19-
import org.apache.commons.logging.Log;
20-
import org.apache.commons.logging.LogFactory;
21-
2219
import org.springframework.http.server.PathContainer;
2320

2421
/**
@@ -37,8 +34,6 @@
3734
*/
3835
public class PathPatternParser {
3936

40-
private static final Log logger = LogFactory.getLog(PathPatternParser.class);
41-
4237
private boolean matchOptionalTrailingSeparator = true;
4338

4439
private boolean caseSensitive = true;
@@ -114,11 +109,6 @@ public PathContainer.Options getPathOptions() {
114109
* @throws PatternParseException in case of parse errors
115110
*/
116111
public PathPattern parse(String pathPattern) throws PatternParseException {
117-
int wildcardIndex = pathPattern.indexOf("**" + this.pathOptions.separator());
118-
if (wildcardIndex != -1 && wildcardIndex != pathPattern.length() - 3) {
119-
logger.warn("'**' patterns are not supported in the middle of patterns and will be rejected in the future. " +
120-
"Consider using '*' instead for matching a single path segment.");
121-
}
122112
return new InternalPathPatternParser(this).parse(pathPattern);
123113
}
124114

spring-web/src/main/java/org/springframework/web/util/pattern/PatternParseException.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public enum PatternMessage {
100100
CANNOT_HAVE_ADJACENT_CAPTURES("Adjacent captures are not allowed"),
101101
ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR("Char ''{0}'' not allowed at start of captured variable name"),
102102
ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR("Char ''{0}'' is not allowed in a captured variable name"),
103-
NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST("No more pattern data allowed after '{*...}' pattern element"),
103+
NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST("No more pattern data allowed after '{*...}' or '**' pattern element"),
104104
BADLY_FORMED_CAPTURE_THE_REST("Expected form when capturing the rest of the path is simply '{*...}'"),
105105
MISSING_REGEX_CONSTRAINT("Missing regex constraint on capture"),
106106
ILLEGAL_DOUBLE_CAPTURE("Not allowed to capture ''{0}'' twice in the same pattern"),

spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternParserTests.java

+24-23
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import static org.assertj.core.api.Assertions.assertThat;
2929
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
3030
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
31+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
3132
import static org.assertj.core.api.Assertions.fail;
3233

3334
/**
@@ -128,12 +129,12 @@ public void regexPathElementPatterns() {
128129
pathPattern = checkStructure("/{var:\\\\}");
129130
PathElement next = pathPattern.getHeadSection().next;
130131
assertThat(next.getClass().getName()).isEqualTo(CaptureVariablePathElement.class.getName());
131-
assertMatches(pathPattern,"/\\");
132+
assertMatches(pathPattern, "/\\");
132133

133134
pathPattern = checkStructure("/{var:\\/}");
134135
next = pathPattern.getHeadSection().next;
135136
assertThat(next.getClass().getName()).isEqualTo(CaptureVariablePathElement.class.getName());
136-
assertNoMatch(pathPattern,"/aaa");
137+
assertNoMatch(pathPattern, "/aaa");
137138

138139
pathPattern = checkStructure("/{var:a{1,2}}");
139140
next = pathPattern.getHeadSection().next;
@@ -142,25 +143,25 @@ public void regexPathElementPatterns() {
142143
pathPattern = checkStructure("/{var:[^\\/]*}");
143144
next = pathPattern.getHeadSection().next;
144145
assertThat(next.getClass().getName()).isEqualTo(CaptureVariablePathElement.class.getName());
145-
PathPattern.PathMatchInfo result = matchAndExtract(pathPattern,"/foo");
146+
PathPattern.PathMatchInfo result = matchAndExtract(pathPattern, "/foo");
146147
assertThat(result.getUriVariables().get("var")).isEqualTo("foo");
147148

148149
pathPattern = checkStructure("/{var:\\[*}");
149150
next = pathPattern.getHeadSection().next;
150151
assertThat(next.getClass().getName()).isEqualTo(CaptureVariablePathElement.class.getName());
151-
result = matchAndExtract(pathPattern,"/[[[");
152+
result = matchAndExtract(pathPattern, "/[[[");
152153
assertThat(result.getUriVariables().get("var")).isEqualTo("[[[");
153154

154155
pathPattern = checkStructure("/{var:[\\{]*}");
155156
next = pathPattern.getHeadSection().next;
156157
assertThat(next.getClass().getName()).isEqualTo(CaptureVariablePathElement.class.getName());
157-
result = matchAndExtract(pathPattern,"/{{{");
158+
result = matchAndExtract(pathPattern, "/{{{");
158159
assertThat(result.getUriVariables().get("var")).isEqualTo("{{{");
159160

160161
pathPattern = checkStructure("/{var:[\\}]*}");
161162
next = pathPattern.getHeadSection().next;
162163
assertThat(next.getClass().getName()).isEqualTo(CaptureVariablePathElement.class.getName());
163-
result = matchAndExtract(pathPattern,"/}}}");
164+
result = matchAndExtract(pathPattern, "/}}}");
164165
assertThat(result.getUriVariables().get("var")).isEqualTo("}}}");
165166

166167
pathPattern = checkStructure("*");
@@ -249,10 +250,10 @@ public void illegalCapturePatterns() {
249250
PathPattern pp = parse("/{abc:foo(bar)}");
250251
assertThatIllegalArgumentException().isThrownBy(() ->
251252
pp.matchAndExtract(toPSC("/foo")))
252-
.withMessage("No capture groups allowed in the constraint regex: foo(bar)");
253+
.withMessage("No capture groups allowed in the constraint regex: foo(bar)");
253254
assertThatIllegalArgumentException().isThrownBy(() ->
254255
pp.matchAndExtract(toPSC("/foobar")))
255-
.withMessage("No capture groups allowed in the constraint regex: foo(bar)");
256+
.withMessage("No capture groups allowed in the constraint regex: foo(bar)");
256257
}
257258

258259
@Test
@@ -414,12 +415,12 @@ public void compareTests() {
414415
assertThat(patterns.get(1)).isEqualTo(p2);
415416
}
416417

417-
@Test // Should be updated with gh-24952
418-
public void doubleWildcardWithinPatternNotSupported() {
418+
@Test
419+
public void captureTheRestWithinPatternNotSupported() {
419420
PathPatternParser parser = new PathPatternParser();
420-
PathPattern pattern = parser.parse("/resources/**/details");
421-
assertThat(pattern.matches(PathContainer.parsePath("/resources/test/details"))).isTrue();
422-
assertThat(pattern.matches(PathContainer.parsePath("/resources/projects/spring/details"))).isFalse();
421+
assertThatThrownBy(() -> parser.parse("/resources/**/details"))
422+
.isInstanceOf(PatternParseException.class)
423+
.extracting("messageType").isEqualTo(PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
423424
}
424425

425426
@Test
@@ -461,16 +462,16 @@ private void checkError(String pattern, int expectedPos, PatternMessage expected
461462
String... expectedInserts) {
462463

463464
assertThatExceptionOfType(PatternParseException.class)
464-
.isThrownBy(() -> pathPattern = parse(pattern))
465-
.satisfies(ex -> {
466-
if (expectedPos >= 0) {
467-
assertThat(ex.getPosition()).as(ex.toDetailedString()).isEqualTo(expectedPos);
468-
}
469-
assertThat(ex.getMessageType()).as(ex.toDetailedString()).isEqualTo(expectedMessage);
470-
if (expectedInserts.length != 0) {
471-
assertThat(ex.getInserts()).isEqualTo(expectedInserts);
472-
}
473-
});
465+
.isThrownBy(() -> pathPattern = parse(pattern))
466+
.satisfies(ex -> {
467+
if (expectedPos >= 0) {
468+
assertThat(ex.getPosition()).as(ex.toDetailedString()).isEqualTo(expectedPos);
469+
}
470+
assertThat(ex.getMessageType()).as(ex.toDetailedString()).isEqualTo(expectedMessage);
471+
if (expectedInserts.length != 0) {
472+
assertThat(ex.getInserts()).isEqualTo(expectedInserts);
473+
}
474+
});
474475
}
475476

476477
@SafeVarargs

spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternTests.java

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -54,8 +54,10 @@ public void pathContainer() {
5454
public void hasPatternSyntax() {
5555
PathPatternParser parser = new PathPatternParser();
5656
assertThat(parser.parse("/foo/*").hasPatternSyntax()).isTrue();
57-
assertThat(parser.parse("/foo/**/bar").hasPatternSyntax()).isTrue();
57+
assertThat(parser.parse("/foo/**").hasPatternSyntax()).isFalse();
58+
assertThat(parser.parse("/foo/{*elem}").hasPatternSyntax()).isFalse();
5859
assertThat(parser.parse("/f?o").hasPatternSyntax()).isTrue();
60+
assertThat(parser.parse("/f*").hasPatternSyntax()).isTrue();
5961
assertThat(parser.parse("/foo/{bar}/baz").hasPatternSyntax()).isTrue();
6062
assertThat(parser.parse("/foo/bar").hasPatternSyntax()).isFalse();
6163
}
@@ -867,13 +869,10 @@ public void combine() {
867869
assertThat(pathMatcher.combine("", "/hotels")).isEqualTo("/hotels");
868870
assertThat(pathMatcher.combine("/hotels/*", "booking")).isEqualTo("/hotels/booking");
869871
assertThat(pathMatcher.combine("/hotels/*", "/booking")).isEqualTo("/hotels/booking");
870-
assertThat(pathMatcher.combine("/hotels/**", "booking")).isEqualTo("/hotels/**/booking");
871-
assertThat(pathMatcher.combine("/hotels/**", "/booking")).isEqualTo("/hotels/**/booking");
872872
assertThat(pathMatcher.combine("/hotels", "/booking")).isEqualTo("/hotels/booking");
873873
assertThat(pathMatcher.combine("/hotels", "booking")).isEqualTo("/hotels/booking");
874874
assertThat(pathMatcher.combine("/hotels/", "booking")).isEqualTo("/hotels/booking");
875875
assertThat(pathMatcher.combine("/hotels/*", "{hotel}")).isEqualTo("/hotels/{hotel}");
876-
assertThat(pathMatcher.combine("/hotels/**", "{hotel}")).isEqualTo("/hotels/**/{hotel}");
877876
assertThat(pathMatcher.combine("/hotels", "{hotel}")).isEqualTo("/hotels/{hotel}");
878877
assertThat(pathMatcher.combine("/hotels", "{hotel}.*")).isEqualTo("/hotels/{hotel}.*");
879878
assertThat(pathMatcher.combine("/hotels/*/booking", "{booking}")).isEqualTo("/hotels/*/booking/{booking}");

0 commit comments

Comments
 (0)