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:
Sam Brannen 2021-06-15 15:11:57 +02:00
parent bcb0580492
commit 3676084472
3 changed files with 79 additions and 35 deletions

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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}. * Default implementation of {@link PathContainer}.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Sam Brannen
* @since 5.0 * @since 5.0
*/ */
final class DefaultPathContainer implements PathContainer { 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 PathContainer EMPTY_PATH = new DefaultPathContainer("", Collections.emptyList());
private static final Map<Character, DefaultSeparator> SEPARATORS = new HashMap<>(2); private static final Map<Character, DefaultSeparator> SEPARATORS = new HashMap<>(2);
@ -120,7 +119,7 @@ final class DefaultPathContainer implements PathContainer {
if (!segment.isEmpty()) { if (!segment.isEmpty()) {
elements.add(options.shouldDecodeAndParseSegments() ? elements.add(options.shouldDecodeAndParseSegments() ?
decodeAndParsePathSegment(segment) : decodeAndParsePathSegment(segment) :
new DefaultPathSegment(segment, separatorElement)); DefaultPathSegment.from(segment, separatorElement));
} }
if (end == -1) { if (end == -1) {
break; break;
@ -136,13 +135,13 @@ final class DefaultPathContainer implements PathContainer {
int index = segment.indexOf(';'); int index = segment.indexOf(';');
if (index == -1) { if (index == -1) {
String valueToMatch = StringUtils.uriDecode(segment, charset); String valueToMatch = StringUtils.uriDecode(segment, charset);
return new DefaultPathSegment(segment, valueToMatch, EMPTY_PARAMS); return DefaultPathSegment.from(segment, valueToMatch);
} }
else { else {
String valueToMatch = StringUtils.uriDecode(segment.substring(0, index), charset); String valueToMatch = StringUtils.uriDecode(segment.substring(0, index), charset);
String pathParameterContent = segment.substring(index); String pathParameterContent = segment.substring(index);
MultiValueMap<String, String> parameters = parsePathParams(pathParameterContent, charset); 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; private final String value;
@ -234,24 +236,33 @@ final class DefaultPathContainer implements PathContainer {
private final MultiValueMap<String, String> parameters; 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) { static DefaultPathSegment from(String value, DefaultSeparator separator) {
this.value = value; String valueToMatch = value.contains(separator.encodedSequence()) ?
this.valueToMatch = valueToMatch; value.replaceAll(separator.encodedSequence(), separator.value()) : value;
this.parameters = CollectionUtils.unmodifiableMultiValueMap(params); 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.value = value;
this.valueToMatch = value.contains(separator.encodedSequence()) ? this.valueToMatch = valueToMatch;
value.replaceAll(separator.encodedSequence(), separator.value()) : value; this.parameters = params;
this.parameters = EMPTY_PARAMS;
} }

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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. * Path parameters associated with this path segment.
* @return an unmodifiable map containing the parameters
*/ */
MultiValueMap<String, String> parameters(); MultiValueMap<String, String> parameters();
} }
@ -136,15 +137,16 @@ public interface PathContainer {
class Options { class Options {
/** /**
* Options for HTTP URL paths: * Options for HTTP URL paths.
* <p>Separator '/' with URL decoding and parsing of path params. * <p>Separator '/' with URL decoding and parsing of path parameters.
*/ */
public final static Options HTTP_PATH = Options.create('/', true); public final static Options HTTP_PATH = Options.create('/', true);
/** /**
* Options for a message route: * Options for a message route.
* <p>Separator '.' without URL decoding nor parsing of params. Escape * <p>Separator '.' with neither URL decoding nor parsing of path parameters.
* sequences for the separator char in segment values are still decoded. * Escape sequences for the separator character in segment values are still
* decoded.
*/ */
public final static Options MESSAGE_ROUTE = Options.create('.', false); public final static Options MESSAGE_ROUTE = Options.create('.', false);

View File

@ -20,11 +20,14 @@ import java.util.stream.Stream;
import org.junit.jupiter.api.Test; 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.http.server.PathContainer.PathSegment;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/** /**
* Unit tests for {@link DefaultPathContainer}. * Unit tests for {@link DefaultPathContainer}.
@ -37,20 +40,20 @@ class DefaultPathContainerTests {
@Test @Test
void pathSegment() { void pathSegment() {
// basic // basic
testPathSegment("cars", "cars", new LinkedMultiValueMap<>()); testPathSegment("cars", "cars", emptyMap());
// empty // empty
testPathSegment("", "", new LinkedMultiValueMap<>()); testPathSegment("", "", emptyMap());
// spaces // spaces
testPathSegment("%20%20", " ", new LinkedMultiValueMap<>()); testPathSegment("%20%20", " ", emptyMap());
testPathSegment("%20a%20", " a ", new LinkedMultiValueMap<>()); testPathSegment("%20a%20", " a ", emptyMap());
} }
@Test @Test
void pathSegmentParams() { void pathSegmentParams() {
// basic // basic
LinkedMultiValueMap<String, String> params = new LinkedMultiValueMap<>(); LinkedMultiValueMap<String, String> params = emptyMap();
params.add("colors", "red"); params.add("colors", "red");
params.add("colors", "blue"); params.add("colors", "blue");
params.add("colors", "green"); params.add("colors", "green");
@ -58,21 +61,45 @@ class DefaultPathContainerTests {
testPathSegment("cars;colors=red,blue,green;year=2012", "cars", params); testPathSegment("cars;colors=red,blue,green;year=2012", "cars", params);
// trailing semicolon // trailing semicolon
params = new LinkedMultiValueMap<>(); params = emptyMap();
params.add("p", "1"); params.add("p", "1");
testPathSegment("path;p=1;", "path", params); testPathSegment("path;p=1;", "path", params);
// params with spaces // params with spaces
params = new LinkedMultiValueMap<>(); params = emptyMap();
params.add("param name", "param value"); params.add("param name", "param value");
testPathSegment("path;param%20name=param%20value;%20", "path", params); testPathSegment("path;param%20name=param%20value;%20", "path", params);
// empty params // empty params
params = new LinkedMultiValueMap<>(); params = emptyMap();
params.add("p", "1"); params.add("p", "1");
testPathSegment("path;;;%20;%20;p=1;%20", "path", params); 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) { private void testPathSegment(String rawValue, String valueToMatch, MultiValueMap<String, String> params) {
PathContainer container = PathContainer.parsePath(rawValue); PathContainer container = PathContainer.parsePath(rawValue);
@ -111,10 +138,10 @@ class DefaultPathContainerTests {
} }
private void testPath(String input, String value, String... expectedElements) { 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.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); .containsExactly(expectedElements);
} }
@ -137,7 +164,7 @@ class DefaultPathContainerTests {
@Test // gh-23310 @Test // gh-23310
void pathWithCustomSeparator() { 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() Stream<String> decodedSegments = path.elements().stream()
.filter(PathSegment.class::isInstance) .filter(PathSegment.class::isInstance)
@ -147,4 +174,8 @@ class DefaultPathContainerTests {
assertThat(decodedSegments).containsExactly("a", "b.b", "c"); assertThat(decodedSegments).containsExactly("a", "b.b", "c");
} }
private static LinkedMultiValueMap<String, String> emptyMap() {
return new LinkedMultiValueMap<>();
}
} }