Skip to content

Commit 6e5cb7f

Browse files
committed
Protect against '//' in UriComponentsBuilder
Refactor UriComponentsBuilder to ensure that paths do not contain empty segments. For example, prior to this commit: fromUriString("http://example.com/abc/").path("/x/y/z") would build the URL "http://example.com/abc//x/y/z" where as it will now build "http://example.com/abc/x/y/z". Issue: SPR-10270
1 parent 953b2b6 commit 6e5cb7f

File tree

2 files changed

+101
-106
lines changed

2 files changed

+101
-106
lines changed

spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java

Lines changed: 93 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
import java.net.URI;
2020
import java.util.ArrayList;
21-
import java.util.Collection;
21+
import java.util.LinkedList;
2222
import java.util.List;
2323
import java.util.Map;
2424
import java.util.regex.Matcher;
@@ -29,6 +29,7 @@
2929
import org.springframework.util.MultiValueMap;
3030
import org.springframework.util.ObjectUtils;
3131
import org.springframework.util.StringUtils;
32+
import org.springframework.web.util.HierarchicalUriComponents.PathComponent;
3233

3334
/**
3435
* Builder for {@link UriComponents}.
@@ -46,6 +47,7 @@
4647
*
4748
* @author Arjen Poutsma
4849
* @author Rossen Stoyanchev
50+
* @author Phillip Webb
4951
* @since 3.1
5052
* @see #newInstance()
5153
* @see #fromPath(String)
@@ -91,7 +93,7 @@ public class UriComponentsBuilder {
9193

9294
private int port = -1;
9395

94-
private PathComponentBuilder pathBuilder = NULL_PATH_COMPONENT_BUILDER;
96+
private CompositePathComponentBuilder pathBuilder = new CompositePathComponentBuilder();
9597

9698
private final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<String, String>();
9799

@@ -334,7 +336,7 @@ public UriComponentsBuilder uri(URI uri) {
334336
this.port = uri.getPort();
335337
}
336338
if (StringUtils.hasLength(uri.getRawPath())) {
337-
this.pathBuilder = new FullPathComponentBuilder(uri.getRawPath());
339+
this.pathBuilder = new CompositePathComponentBuilder(uri.getRawPath());
338340
}
339341
if (StringUtils.hasLength(uri.getRawQuery())) {
340342
this.queryParams.clear();
@@ -352,7 +354,7 @@ private void resetHierarchicalComponents() {
352354
this.userInfo = null;
353355
this.host = null;
354356
this.port = -1;
355-
this.pathBuilder = NULL_PATH_COMPONENT_BUILDER;
357+
this.pathBuilder = new CompositePathComponentBuilder();
356358
this.queryParams.clear();
357359
}
358360

@@ -436,12 +438,7 @@ public UriComponentsBuilder port(int port) {
436438
* @return this UriComponentsBuilder
437439
*/
438440
public UriComponentsBuilder path(String path) {
439-
if (path != null) {
440-
this.pathBuilder = this.pathBuilder.appendPath(path);
441-
}
442-
else {
443-
this.pathBuilder = NULL_PATH_COMPONENT_BUILDER;
444-
}
441+
this.pathBuilder.addPath(path);
445442
resetSchemeSpecificPart();
446443
return this;
447444
}
@@ -453,22 +450,21 @@ public UriComponentsBuilder path(String path) {
453450
* @return this UriComponentsBuilder
454451
*/
455452
public UriComponentsBuilder replacePath(String path) {
456-
this.pathBuilder = NULL_PATH_COMPONENT_BUILDER;
457-
path(path);
453+
this.pathBuilder = new CompositePathComponentBuilder(path);
458454
resetSchemeSpecificPart();
459455
return this;
460456
}
461457

462458
/**
463-
* Appends the given path segments to the existing path of this builder. Each given path segments may contain URI
464-
* template variables.
459+
* Appends the given path segments to the existing path of this builder. Each given
460+
* path segments may contain URI template variables.
465461
*
466462
* @param pathSegments the URI path segments
467463
* @return this UriComponentsBuilder
468464
*/
469465
public UriComponentsBuilder pathSegment(String... pathSegments) throws IllegalArgumentException {
470466
Assert.notNull(pathSegments, "'segments' must not be null");
471-
this.pathBuilder = this.pathBuilder.appendPathSegments(pathSegments);
467+
this.pathBuilder.addPathSegments(pathSegments);
472468
resetSchemeSpecificPart();
473469
return this;
474470
}
@@ -590,131 +586,122 @@ public UriComponentsBuilder fragment(String fragment) {
590586
return this;
591587
}
592588

593-
/**
594-
* Represents a builder for {@link HierarchicalUriComponents.PathComponent}
595-
*/
596-
private interface PathComponentBuilder {
597-
598-
HierarchicalUriComponents.PathComponent build();
599589

600-
PathComponentBuilder appendPath(String path);
601-
602-
PathComponentBuilder appendPathSegments(String... pathSegments);
590+
private interface PathComponentBuilder {
591+
PathComponent build();
603592
}
604593

605-
/**
606-
* Represents a builder for full string paths.
607-
*/
608-
private static class FullPathComponentBuilder implements PathComponentBuilder {
609-
610-
private final StringBuilder path;
594+
private static class CompositePathComponentBuilder implements PathComponentBuilder {
611595

612-
private FullPathComponentBuilder(String path) {
613-
this.path = new StringBuilder(path);
614-
}
615-
616-
public HierarchicalUriComponents.PathComponent build() {
617-
return new HierarchicalUriComponents.FullPathComponent(path.toString());
618-
}
596+
private LinkedList<PathComponentBuilder> componentBuilders = new LinkedList<PathComponentBuilder>();
619597

620-
public PathComponentBuilder appendPath(String path) {
621-
this.path.append(path);
622-
return this;
623-
}
624-
625-
public PathComponentBuilder appendPathSegments(String... pathSegments) {
626-
PathComponentCompositeBuilder builder = new PathComponentCompositeBuilder(this);
627-
builder.appendPathSegments(pathSegments);
628-
return builder;
598+
public CompositePathComponentBuilder() {
629599
}
630-
}
631-
632-
/**
633-
* Represents a builder for paths segment paths.
634-
*/
635-
private static class PathSegmentComponentBuilder implements PathComponentBuilder {
636-
637-
private final List<String> pathSegments = new ArrayList<String>();
638600

639-
private PathSegmentComponentBuilder(String... pathSegments) {
640-
this.pathSegments.addAll(removeEmptyPathSegments(pathSegments));
601+
public CompositePathComponentBuilder(String path) {
602+
addPath(path);
641603
}
642604

643-
private Collection<String> removeEmptyPathSegments(String... pathSegments) {
644-
List<String> result = new ArrayList<String>();
645-
for (String segment : pathSegments) {
646-
if (StringUtils.hasText(segment)) {
647-
result.add(segment);
605+
public void addPathSegments(String... pathSegments) {
606+
if (!ObjectUtils.isEmpty(pathSegments)) {
607+
PathSegmentComponentBuilder psBuilder = getLastBuilder(PathSegmentComponentBuilder.class);
608+
FullPathComponentBuilder fpBuilder = getLastBuilder(FullPathComponentBuilder.class);
609+
if (psBuilder == null) {
610+
psBuilder = new PathSegmentComponentBuilder();
611+
this.componentBuilders.add(psBuilder);
612+
if (fpBuilder != null) {
613+
fpBuilder.removeTrailingSlash();
614+
}
648615
}
616+
psBuilder.append(pathSegments);
649617
}
650-
return result;
651618
}
652619

653-
public HierarchicalUriComponents.PathComponent build() {
654-
return new HierarchicalUriComponents.PathSegmentComponent(pathSegments);
620+
public void addPath(String path) {
621+
if (StringUtils.hasText(path)) {
622+
PathSegmentComponentBuilder psBuilder = getLastBuilder(PathSegmentComponentBuilder.class);
623+
FullPathComponentBuilder fpBuilder = getLastBuilder(FullPathComponentBuilder.class);
624+
if (psBuilder != null) {
625+
path = path.startsWith("/") ? path : "/" + path;
626+
}
627+
if (fpBuilder == null) {
628+
fpBuilder = new FullPathComponentBuilder();
629+
this.componentBuilders.add(fpBuilder);
630+
}
631+
fpBuilder.append(path);
632+
}
655633
}
656634

657-
public PathComponentBuilder appendPath(String path) {
658-
PathComponentCompositeBuilder builder = new PathComponentCompositeBuilder(this);
659-
builder.appendPath(path);
660-
return builder;
635+
@SuppressWarnings("unchecked")
636+
private <T> T getLastBuilder(Class<T> builderClass) {
637+
if (!this.componentBuilders.isEmpty()) {
638+
PathComponentBuilder last = this.componentBuilders.getLast();
639+
if (builderClass.isInstance(last)) {
640+
return (T) last;
641+
}
642+
}
643+
return null;
661644
}
662645

663-
public PathComponentBuilder appendPathSegments(String... pathSegments) {
664-
this.pathSegments.addAll(removeEmptyPathSegments(pathSegments));
665-
return this;
646+
public PathComponent build() {
647+
int size = this.componentBuilders.size();
648+
List<PathComponent> components = new ArrayList<PathComponent>(size);
649+
for (int i = 0; i < size; i++) {
650+
PathComponent pathComponent = this.componentBuilders.get(i).build();
651+
if (pathComponent != null) {
652+
components.add(pathComponent);
653+
}
654+
}
655+
if (components.isEmpty()) {
656+
return HierarchicalUriComponents.NULL_PATH_COMPONENT;
657+
}
658+
if (components.size() == 1) {
659+
return components.get(0);
660+
}
661+
return new HierarchicalUriComponents.PathComponentComposite(components);
666662
}
667663
}
668664

669-
/**
670-
* Represents a builder for a collection of PathComponents.
671-
*/
672-
private static class PathComponentCompositeBuilder implements PathComponentBuilder {
665+
private static class FullPathComponentBuilder implements PathComponentBuilder {
673666

674-
private final List<PathComponentBuilder> pathComponentBuilders = new ArrayList<PathComponentBuilder>();
667+
private StringBuilder path = new StringBuilder();
675668

676-
private PathComponentCompositeBuilder(PathComponentBuilder builder) {
677-
pathComponentBuilders.add(builder);
669+
public void append(String path) {
670+
this.path.append(path);
678671
}
679672

680-
public HierarchicalUriComponents.PathComponent build() {
681-
List<HierarchicalUriComponents.PathComponent> pathComponents =
682-
new ArrayList<HierarchicalUriComponents.PathComponent>(pathComponentBuilders.size());
683-
684-
for (PathComponentBuilder pathComponentBuilder : pathComponentBuilders) {
685-
pathComponents.add(pathComponentBuilder.build());
673+
public PathComponent build() {
674+
if (this.path.length() == 0) {
675+
return null;
686676
}
687-
return new HierarchicalUriComponents.PathComponentComposite(pathComponents);
688-
}
689-
690-
public PathComponentBuilder appendPath(String path) {
691-
this.pathComponentBuilders.add(new FullPathComponentBuilder(path));
692-
return this;
677+
String path = this.path.toString().replace("//", "/");
678+
return new HierarchicalUriComponents.FullPathComponent(path);
693679
}
694680

695-
public PathComponentBuilder appendPathSegments(String... pathSegments) {
696-
this.pathComponentBuilders.add(new PathSegmentComponentBuilder(pathSegments));
697-
return this;
681+
public void removeTrailingSlash() {
682+
int index = this.path.length() - 1;
683+
if (this.path.charAt(index) == '/') {
684+
this.path.deleteCharAt(index);
685+
}
698686
}
699687
}
700688

689+
private static class PathSegmentComponentBuilder implements PathComponentBuilder {
701690

702-
/**
703-
* Represents a builder for an empty path.
704-
*/
705-
private static PathComponentBuilder NULL_PATH_COMPONENT_BUILDER = new PathComponentBuilder() {
706-
707-
public HierarchicalUriComponents.PathComponent build() {
708-
return HierarchicalUriComponents.NULL_PATH_COMPONENT;
709-
}
691+
private List<String> pathSegments = new LinkedList<String>();
710692

711-
public PathComponentBuilder appendPath(String path) {
712-
return new FullPathComponentBuilder(path);
693+
public void append(String... pathSegments) {
694+
for (String pathSegment : pathSegments) {
695+
if (StringUtils.hasText(pathSegment)) {
696+
this.pathSegments.add(pathSegment);
697+
}
698+
}
713699
}
714700

715-
public PathComponentBuilder appendPathSegments(String... pathSegments) {
716-
return new PathSegmentComponentBuilder(pathSegments);
701+
public PathComponent build() {
702+
return this.pathSegments.isEmpty() ?
703+
null : new HierarchicalUriComponents.PathSegmentComponent(this.pathSegments);
717704
}
718-
};
705+
}
719706

720707
}

spring-web/src/test/java/org/springframework/web/util/UriComponentsBuilderTests.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,4 +346,12 @@ public void relativeUrls() throws Exception {
346346
assertThat(UriComponentsBuilder.fromUriString("http://example.com").path("foo/../bar").build().toUriString(), equalTo("http://example.com/foo/../bar"));
347347
assertThat(UriComponentsBuilder.fromUriString("http://example.com").path("foo/../bar").build().toUri().getPath(), equalTo("/foo/../bar"));
348348
}
349+
350+
@Test
351+
public void emptySegments() throws Exception {
352+
assertThat(UriComponentsBuilder.fromUriString("http://example.com/abc/").path("/x/y/z").build().toString(), equalTo("http://example.com/abc/x/y/z"));
353+
assertThat(UriComponentsBuilder.fromUriString("http://example.com/abc/").pathSegment("x", "y", "z").build().toString(), equalTo("http://example.com/abc/x/y/z"));
354+
assertThat(UriComponentsBuilder.fromUriString("http://example.com/abc/").path("/x/").path("/y/z").build().toString(), equalTo("http://example.com/abc/x/y/z"));
355+
assertThat(UriComponentsBuilder.fromUriString("http://example.com/abc/").pathSegment("x").path("y").build().toString(), equalTo("http://example.com/abc/x/y"));
356+
}
349357
}

0 commit comments

Comments
 (0)