Skip to content

Commit 4ad59e2

Browse files
authored
fix(api-common): validate path template variables against dot segments (#14641)
Validates URI path template variable segments during `PathTemplate.instantiate` to prevent unintended dot-segment (`"."` and `".."`) path traversal and ensures strict percent-encoding of non-unreserved URI characters: - Reject any path segment equal to `"."` or `".."` in single-segment (`*`), multi-segment (`*`), and path wildcard (`**`) bindings (`ValidationException: "Value for %s must not contain segments that are exactly . or .."`). - Encode path variable segments using `PercentEscaper("-_.~", false)` so all characters outside `[-_.~0-9a-zA-Z]` are percent-encoded (e.g., spaces as `%20`, `?` as `%3F`, `#` as `%23`, `%` as `%25`). fixes: b/545212190
1 parent f1db2bb commit 4ad59e2

2 files changed

Lines changed: 127 additions & 6 deletions

File tree

‎sdk-platform-java/api-common-java/src/main/java/com/google/api/pathtemplate/PathTemplate.java‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@
3636
import com.google.common.collect.ImmutableMap;
3737
import com.google.common.collect.Lists;
3838
import com.google.common.collect.Maps;
39+
import com.google.common.net.PercentEscaper;
3940
import java.io.UnsupportedEncodingException;
4041
import java.net.URLDecoder;
41-
import java.net.URLEncoder;
4242
import java.util.ArrayList;
4343
import java.util.List;
4444
import java.util.ListIterator;
@@ -888,12 +888,14 @@ private String instantiate(Map<String, String> values, boolean allowPartial) {
888888
|| nextNext.kind() != SegmentKind.END_BINDING;
889889
restore(iterator, iterator.nextIndex() - 2);
890890
if (!pathEscape) {
891+
validatePathSegment(var, value);
891892
result.append(encodeUrl(value));
892893
} else {
893894
// For a path wildcard or path of length greater 1, split the value and escape
894895
// every sub-segment.
895896
boolean first = true;
896897
for (String subSeg : SLASH_SPLITTER.split(value)) {
898+
validatePathSegment(var, subSeg);
897899
if (!first) {
898900
result.append('/');
899901
}
@@ -1208,13 +1210,18 @@ private static List<Segment> parseComplexResourceId(String seg) {
12081210
// Helpers
12091211
// =======
12101212

1213+
private static final PercentEscaper URL_ESCAPER = new PercentEscaper("-_.~", false);
1214+
1215+
private static void validatePathSegment(String var, String segment) {
1216+
if (".".equals(segment) || "..".equals(segment)) {
1217+
throw new ValidationException(
1218+
"Value for %s must not contain segments that are exactly . or ..", var);
1219+
}
1220+
}
1221+
12111222
private String encodeUrl(String text) {
12121223
if (urlEncoding) {
1213-
try {
1214-
return URLEncoder.encode(text, "UTF-8");
1215-
} catch (UnsupportedEncodingException e) {
1216-
throw new ValidationException("UTF-8 encoding is not supported on this platform");
1217-
}
1224+
return URL_ESCAPER.escape(text);
12181225
} else {
12191226
// When encoding is disabled, we accept any character except '/'
12201227
final String INVALID_CHAR = "/";

‎sdk-platform-java/api-common-java/src/test/java/com/google/api/pathtemplate/PathTemplateTest.java‎

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,120 @@ void instantiateEscapeUnsafeChar() {
642642
.isEqualTo("buckets/f%2Fo%2Fo/objects/b%2Fa%2Fr");
643643
}
644644

645+
@Test
646+
void instantiateRejectsDotInSingleWildcard() {
647+
PathTemplate template =
648+
PathTemplate.create("/compute/v1/projects/{project}/regions/{region}/addresses");
649+
ValidationException dotException =
650+
Assertions.assertThrows(
651+
ValidationException.class,
652+
() -> template.instantiate("project", "my-project", "region", "."));
653+
Truth.assertThat(dotException.getMessage())
654+
.isEqualTo("Value for region must not contain segments that are exactly . or ..");
655+
}
656+
657+
@Test
658+
void instantiateRejectsDoubleDotInSingleWildcard() {
659+
PathTemplate template =
660+
PathTemplate.create("/compute/v1/projects/{project}/regions/{region}/addresses");
661+
ValidationException dotDotException =
662+
Assertions.assertThrows(
663+
ValidationException.class,
664+
() -> template.instantiate("project", "my-project", "region", ".."));
665+
Truth.assertThat(dotDotException.getMessage())
666+
.isEqualTo("Value for region must not contain segments that are exactly . or ..");
667+
}
668+
669+
@Test
670+
void instantiateRejectsDotInMultiSegmentWildcard() {
671+
PathTemplate template =
672+
PathTemplate.create("/v2/{parent=projects/*/locations/*}/content:inspect");
673+
ValidationException dotException =
674+
Assertions.assertThrows(
675+
ValidationException.class,
676+
() -> template.instantiate("parent", "projects/my-project/locations/."));
677+
Truth.assertThat(dotException.getMessage())
678+
.isEqualTo("Value for parent must not contain segments that are exactly . or ..");
679+
}
680+
681+
@Test
682+
void instantiateRejectsDoubleDotInMultiSegmentWildcard() {
683+
PathTemplate template =
684+
PathTemplate.create("/v2/{parent=projects/*/locations/*}/content:inspect");
685+
ValidationException dotDotException =
686+
Assertions.assertThrows(
687+
ValidationException.class,
688+
() -> template.instantiate("parent", "projects/my-project/locations/.."));
689+
Truth.assertThat(dotDotException.getMessage())
690+
.isEqualTo("Value for parent must not contain segments that are exactly . or ..");
691+
}
692+
693+
@Test
694+
void instantiateRejectsDotInPathWildcard() {
695+
PathTemplate template =
696+
PathTemplate.create("/v3/{name=projects/*/monitoredResourceDescriptors/**}");
697+
for (String invalidName :
698+
new String[] {
699+
"projects/my-project/monitoredResourceDescriptors/instance/my-instance/.",
700+
"projects/my-project/monitoredResourceDescriptors/.",
701+
"projects/my-project/monitoredResourceDescriptors/instance/./my-instance",
702+
"projects/my-project/monitoredResourceDescriptors/ . "
703+
}) {
704+
ValidationException exception =
705+
Assertions.assertThrows(
706+
ValidationException.class, () -> template.instantiate("name", invalidName));
707+
Truth.assertThat(exception.getMessage())
708+
.isEqualTo("Value for name must not contain segments that are exactly . or ..");
709+
}
710+
}
711+
712+
@Test
713+
void instantiateRejectsDoubleDotInPathWildcard() {
714+
PathTemplate template =
715+
PathTemplate.create("/v3/{name=projects/*/monitoredResourceDescriptors/**}");
716+
for (String invalidName :
717+
new String[] {
718+
"projects/my-project/monitoredResourceDescriptors/instance/my-instance/..",
719+
"projects/my-project/monitoredResourceDescriptors/..",
720+
"projects/my-project/monitoredResourceDescriptors/instance/./my-instance/..",
721+
"projects/my-project/monitoredResourceDescriptors/instance/../..",
722+
"projects/my-project/monitoredResourceDescriptors/ .. "
723+
}) {
724+
ValidationException exception =
725+
Assertions.assertThrows(
726+
ValidationException.class, () -> template.instantiate("name", invalidName));
727+
Truth.assertThat(exception.getMessage())
728+
.isEqualTo("Value for name must not contain segments that are exactly . or ..");
729+
}
730+
}
731+
732+
@Test
733+
void instantiateAllowsSafeDotsAndPercentEncodesReservedChars() {
734+
PathTemplate template = PathTemplate.create("/v1/{x=projects/*/databases/*/documents/**}");
735+
Truth.assertThat(
736+
template.instantiate("x", "projects/sys-prod-123/databases/default/documents/my.txt"))
737+
.isEqualTo("v1/projects/sys-prod-123/databases/default/documents/my.txt");
738+
Truth.assertThat(
739+
template.instantiate(
740+
"x", "projects/sys-prod-123/databases/default/documents/my..file.txt"))
741+
.isEqualTo("v1/projects/sys-prod-123/databases/default/documents/my..file.txt");
742+
Truth.assertThat(
743+
template.instantiate(
744+
"x", "projects/sys-prod-123/databases/default/documents/doc?key=val"))
745+
.isEqualTo("v1/projects/sys-prod-123/databases/default/documents/doc%3Fkey%3Dval");
746+
Truth.assertThat(
747+
template.instantiate("x", "projects/sys-prod-123/databases/default/documents/doc#frag"))
748+
.isEqualTo("v1/projects/sys-prod-123/databases/default/documents/doc%23frag");
749+
Truth.assertThat(
750+
template.instantiate(
751+
"x", "projects/sys-prod-123/databases/default/documents/doc with space"))
752+
.isEqualTo("v1/projects/sys-prod-123/databases/default/documents/doc%20with%20space");
753+
Truth.assertThat(
754+
template.instantiate(
755+
"x", "projects/sys-prod-123/databases/default/documents/%2e%2e/escape-db"))
756+
.isEqualTo("v1/projects/sys-prod-123/databases/default/documents/%252e%252e/escape-db");
757+
}
758+
645759
@Test
646760
void instantiateNotEscapeForUnboundedWildcard() {
647761
PathTemplate template = PathTemplate.create("buckets/*/objects/**");

0 commit comments

Comments
 (0)