Ensure DefaultPathSegment does not allow parameters to be mutated
Prior to this commit, if a PathContainer was created using Options.MESSAGE_ROUTE, DefaultPathSegment#parameters() returned a mutable map which would allow the user to modify the contents of the static, shared EMPTY_PARAMS map in DefaultPathContainer. This commit prevents corruption of the shared EMPTY_PARAMS map by ensuring that parameters stored in DefaultPathSegment are always immutable. Closes gh-27064
This commit is contained in:
parent
bcb0580492
commit
3676084472
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -36,12 +36,11 @@ import org.springframework.util.StringUtils;
|
|||
* Default implementation of {@link PathContainer}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Sam Brannen
|
||||
* @since 5.0
|
||||
*/
|
||||
final class DefaultPathContainer implements PathContainer {
|
||||
|
||||
private static final MultiValueMap<String, String> EMPTY_PARAMS = new LinkedMultiValueMap<>();
|
||||
|
||||
private static final PathContainer EMPTY_PATH = new DefaultPathContainer("", Collections.emptyList());
|
||||
|
||||
private static final Map<Character, DefaultSeparator> SEPARATORS = new HashMap<>(2);
|
||||
|
|
@ -120,7 +119,7 @@ final class DefaultPathContainer implements PathContainer {
|
|||
if (!segment.isEmpty()) {
|
||||
elements.add(options.shouldDecodeAndParseSegments() ?
|
||||
decodeAndParsePathSegment(segment) :
|
||||
new DefaultPathSegment(segment, separatorElement));
|
||||
DefaultPathSegment.from(segment, separatorElement));
|
||||
}
|
||||
if (end == -1) {
|
||||
break;
|
||||
|
|
@ -136,13 +135,13 @@ final class DefaultPathContainer implements PathContainer {
|
|||
int index = segment.indexOf(';');
|
||||
if (index == -1) {
|
||||
String valueToMatch = StringUtils.uriDecode(segment, charset);
|
||||
return new DefaultPathSegment(segment, valueToMatch, EMPTY_PARAMS);
|
||||
return DefaultPathSegment.from(segment, valueToMatch);
|
||||
}
|
||||
else {
|
||||
String valueToMatch = StringUtils.uriDecode(segment.substring(0, index), charset);
|
||||
String pathParameterContent = segment.substring(index);
|
||||
MultiValueMap<String, String> parameters = parsePathParams(pathParameterContent, charset);
|
||||
return new DefaultPathSegment(segment, valueToMatch, parameters);
|
||||
return DefaultPathSegment.from(segment, valueToMatch, parameters);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -226,7 +225,10 @@ final class DefaultPathContainer implements PathContainer {
|
|||
}
|
||||
|
||||
|
||||
private static class DefaultPathSegment implements PathSegment {
|
||||
private static final class DefaultPathSegment implements PathSegment {
|
||||
|
||||
private static final MultiValueMap<String, String> EMPTY_PARAMS =
|
||||
CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>());
|
||||
|
||||
private final String value;
|
||||
|
||||
|
|
@ -234,24 +236,33 @@ final class DefaultPathContainer implements PathContainer {
|
|||
|
||||
private final MultiValueMap<String, String> parameters;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor for decoded and parsed segments.
|
||||
* Factory for segments without decoding and parsing.
|
||||
*/
|
||||
DefaultPathSegment(String value, String valueToMatch, MultiValueMap<String, String> params) {
|
||||
this.value = value;
|
||||
this.valueToMatch = valueToMatch;
|
||||
this.parameters = CollectionUtils.unmodifiableMultiValueMap(params);
|
||||
static DefaultPathSegment from(String value, DefaultSeparator separator) {
|
||||
String valueToMatch = value.contains(separator.encodedSequence()) ?
|
||||
value.replaceAll(separator.encodedSequence(), separator.value()) : value;
|
||||
return from(value, valueToMatch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for segments without decoding and parsing.
|
||||
* Factory for decoded and parsed segments.
|
||||
*/
|
||||
DefaultPathSegment(String value, DefaultSeparator separator) {
|
||||
static DefaultPathSegment from(String value, String valueToMatch) {
|
||||
return new DefaultPathSegment(value, valueToMatch, EMPTY_PARAMS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory for decoded and parsed segments.
|
||||
*/
|
||||
static DefaultPathSegment from(String value, String valueToMatch, MultiValueMap<String, String> params) {
|
||||
return new DefaultPathSegment(value, valueToMatch, CollectionUtils.unmodifiableMultiValueMap(params));
|
||||
}
|
||||
|
||||
private DefaultPathSegment(String value, String valueToMatch, MultiValueMap<String, String> params) {
|
||||
this.value = value;
|
||||
this.valueToMatch = value.contains(separator.encodedSequence()) ?
|
||||
value.replaceAll(separator.encodedSequence(), separator.value()) : value;
|
||||
this.parameters = EMPTY_PARAMS;
|
||||
this.valueToMatch = valueToMatch;
|
||||
this.parameters = params;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
|
@ -124,6 +124,7 @@ public interface PathContainer {
|
|||
|
||||
/**
|
||||
* Path parameters associated with this path segment.
|
||||
* @return an unmodifiable map containing the parameters
|
||||
*/
|
||||
MultiValueMap<String, String> parameters();
|
||||
}
|
||||
|
|
@ -136,15 +137,16 @@ public interface PathContainer {
|
|||
class Options {
|
||||
|
||||
/**
|
||||
* Options for HTTP URL paths:
|
||||
* <p>Separator '/' with URL decoding and parsing of path params.
|
||||
* Options for HTTP URL paths.
|
||||
* <p>Separator '/' with URL decoding and parsing of path parameters.
|
||||
*/
|
||||
public final static Options HTTP_PATH = Options.create('/', true);
|
||||
|
||||
/**
|
||||
* Options for a message route:
|
||||
* <p>Separator '.' without URL decoding nor parsing of params. Escape
|
||||
* sequences for the separator char in segment values are still decoded.
|
||||
* Options for a message route.
|
||||
* <p>Separator '.' with neither URL decoding nor parsing of path parameters.
|
||||
* Escape sequences for the separator character in segment values are still
|
||||
* decoded.
|
||||
*/
|
||||
public final static Options MESSAGE_ROUTE = Options.create('.', false);
|
||||
|
||||
|
|
|
|||
|
|
@ -20,11 +20,14 @@ import java.util.stream.Stream;
|
|||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.server.PathContainer.Element;
|
||||
import org.springframework.http.server.PathContainer.Options;
|
||||
import org.springframework.http.server.PathContainer.PathSegment;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link DefaultPathContainer}.
|
||||
|
|
@ -37,20 +40,20 @@ class DefaultPathContainerTests {
|
|||
@Test
|
||||
void pathSegment() {
|
||||
// basic
|
||||
testPathSegment("cars", "cars", new LinkedMultiValueMap<>());
|
||||
testPathSegment("cars", "cars", emptyMap());
|
||||
|
||||
// empty
|
||||
testPathSegment("", "", new LinkedMultiValueMap<>());
|
||||
testPathSegment("", "", emptyMap());
|
||||
|
||||
// spaces
|
||||
testPathSegment("%20%20", " ", new LinkedMultiValueMap<>());
|
||||
testPathSegment("%20a%20", " a ", new LinkedMultiValueMap<>());
|
||||
testPathSegment("%20%20", " ", emptyMap());
|
||||
testPathSegment("%20a%20", " a ", emptyMap());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pathSegmentParams() {
|
||||
// basic
|
||||
LinkedMultiValueMap<String, String> params = new LinkedMultiValueMap<>();
|
||||
LinkedMultiValueMap<String, String> params = emptyMap();
|
||||
params.add("colors", "red");
|
||||
params.add("colors", "blue");
|
||||
params.add("colors", "green");
|
||||
|
|
@ -58,21 +61,45 @@ class DefaultPathContainerTests {
|
|||
testPathSegment("cars;colors=red,blue,green;year=2012", "cars", params);
|
||||
|
||||
// trailing semicolon
|
||||
params = new LinkedMultiValueMap<>();
|
||||
params = emptyMap();
|
||||
params.add("p", "1");
|
||||
testPathSegment("path;p=1;", "path", params);
|
||||
|
||||
// params with spaces
|
||||
params = new LinkedMultiValueMap<>();
|
||||
params = emptyMap();
|
||||
params.add("param name", "param value");
|
||||
testPathSegment("path;param%20name=param%20value;%20", "path", params);
|
||||
|
||||
// empty params
|
||||
params = new LinkedMultiValueMap<>();
|
||||
params = emptyMap();
|
||||
params.add("p", "1");
|
||||
testPathSegment("path;;;%20;%20;p=1;%20", "path", params);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pathSegmentParamsAreImmutable() {
|
||||
assertPathSegmentParamsAreImmutable("cars", emptyMap(), Options.HTTP_PATH);
|
||||
|
||||
LinkedMultiValueMap<String, String> params = emptyMap();
|
||||
params.add("colors", "red");
|
||||
params.add("colors", "blue");
|
||||
params.add("colors", "green");
|
||||
assertPathSegmentParamsAreImmutable(";colors=red,blue,green", params, Options.HTTP_PATH);
|
||||
|
||||
assertPathSegmentParamsAreImmutable(";colors=red,blue,green", emptyMap(), Options.MESSAGE_ROUTE);
|
||||
}
|
||||
|
||||
private void assertPathSegmentParamsAreImmutable(String path, LinkedMultiValueMap<String, String> params, Options options) {
|
||||
PathContainer container = PathContainer.parsePath(path, options);
|
||||
assertThat(container.elements()).hasSize(1);
|
||||
|
||||
PathSegment segment = (PathSegment) container.elements().get(0);
|
||||
MultiValueMap<String, String> segmentParams = segment.parameters();
|
||||
assertThat(segmentParams).isEqualTo(params);
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class)
|
||||
.isThrownBy(() -> segmentParams.add("enigma", "boom"));
|
||||
}
|
||||
|
||||
private void testPathSegment(String rawValue, String valueToMatch, MultiValueMap<String, String> params) {
|
||||
PathContainer container = PathContainer.parsePath(rawValue);
|
||||
|
||||
|
|
@ -111,10 +138,10 @@ class DefaultPathContainerTests {
|
|||
}
|
||||
|
||||
private void testPath(String input, String value, String... expectedElements) {
|
||||
PathContainer path = PathContainer.parsePath(input, PathContainer.Options.HTTP_PATH);
|
||||
PathContainer path = PathContainer.parsePath(input, Options.HTTP_PATH);
|
||||
|
||||
assertThat(path.value()).as("value: '" + input + "'").isEqualTo(value);
|
||||
assertThat(path.elements()).map(PathContainer.Element::value).as("elements: " + input)
|
||||
assertThat(path.elements()).map(Element::value).as("elements: " + input)
|
||||
.containsExactly(expectedElements);
|
||||
}
|
||||
|
||||
|
|
@ -137,7 +164,7 @@ class DefaultPathContainerTests {
|
|||
|
||||
@Test // gh-23310
|
||||
void pathWithCustomSeparator() {
|
||||
PathContainer path = PathContainer.parsePath("a.b%2Eb.c", PathContainer.Options.MESSAGE_ROUTE);
|
||||
PathContainer path = PathContainer.parsePath("a.b%2Eb.c", Options.MESSAGE_ROUTE);
|
||||
|
||||
Stream<String> decodedSegments = path.elements().stream()
|
||||
.filter(PathSegment.class::isInstance)
|
||||
|
|
@ -147,4 +174,8 @@ class DefaultPathContainerTests {
|
|||
assertThat(decodedSegments).containsExactly("a", "b.b", "c");
|
||||
}
|
||||
|
||||
private static LinkedMultiValueMap<String, String> emptyMap() {
|
||||
return new LinkedMultiValueMap<>();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue