From b63b457c6fee0985e3cee9f09bae2c3284137b61 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Tue, 30 Jun 2026 15:34:25 -0700 Subject: [PATCH 01/15] fix rest templates --- .../google-api-core/google/api_core/path_template.py | 9 +++++++-- .../google-api-core/tests/unit/test_path_template.py | 11 +++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/google-api-core/google/api_core/path_template.py b/packages/google-api-core/google/api_core/path_template.py index b8ebb2af92af..adb5ec458f27 100644 --- a/packages/google-api-core/google/api_core/path_template.py +++ b/packages/google-api-core/google/api_core/path_template.py @@ -29,6 +29,7 @@ import copy import functools import re +import urllib.parse # Regular expression for extracting variable parts from a path template. # The variables can be expressed as: @@ -83,7 +84,9 @@ def _expand_variable_match(positional_vars, named_vars, match): name = match.group("name") if name is not None: try: - return str(named_vars[name]) + val = str(named_vars[name]) + # Percent-encode while keeping '/' safe to preserve existing route and slash validation + return urllib.parse.quote(val, safe="/") except KeyError: raise ValueError( "Named variable '{}' not specified and needed by template " @@ -91,7 +94,9 @@ def _expand_variable_match(positional_vars, named_vars, match): ) elif positional is not None: try: - return str(positional_vars.pop(0)) + val = str(positional_vars.pop(0)) + # Percent-encode while keeping '/' safe to preserve existing route and slash validation + return urllib.parse.quote(val, safe="/") except IndexError: raise ValueError( "Positional variable not specified and needed by template " diff --git a/packages/google-api-core/tests/unit/test_path_template.py b/packages/google-api-core/tests/unit/test_path_template.py index c34dd0f32cbb..db918b83140e 100644 --- a/packages/google-api-core/tests/unit/test_path_template.py +++ b/packages/google-api-core/tests/unit/test_path_template.py @@ -63,6 +63,17 @@ {"name": "parent/child/object"}, "/v1/a/parent/child/object", ], + # Encoding / Metacharacters in positional and named params + ["/v1/*", ["..?$httpMethod=DELETE#"], {}, "/v1/..%3F%24httpMethod%3DDELETE%23"], + ["/v1/**", ["path/../with/?and#"], {}, "/v1/path/../with/%3Fand%23"], + ["/v1/{name}", [], {"name": "..?$httpMethod=DELETE#"}, "/v1/..%3F%24httpMethod%3DDELETE%23"], + ["/v1/{name=**}", [], {"name": "path/../with/?and#"}, "/v1/path/../with/%3Fand%23"], + [ + "/v3/{session=projects/*/locations/*/agents/*/sessions/*}:detectIntent", + [], + {"session": "projects/cx/locations/global/agents/a1/sessions/..?$httpMethod=DELETE#"}, + "/v3/projects/cx/locations/global/agents/a1/sessions/..%3F%24httpMethod%3DDELETE%23:detectIntent", + ], ], ) def test_expand_success(tmpl, args, kwargs, expected_result): From bf489e689f866f0cc79a8a3f76245787a166a03e Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 16 Jul 2026 13:46:32 -0700 Subject: [PATCH 02/15] added more advanced validation --- .../google/api_core/path_template.py | 150 +++++++++++++++++- .../tests/unit/test_path_template.py | 115 +++++++++++++- 2 files changed, 258 insertions(+), 7 deletions(-) diff --git a/packages/google-api-core/google/api_core/path_template.py b/packages/google-api-core/google/api_core/path_template.py index adb5ec458f27..4580040f5080 100644 --- a/packages/google-api-core/google/api_core/path_template.py +++ b/packages/google-api-core/google/api_core/path_template.py @@ -63,6 +63,147 @@ _MULTI_SEGMENT_PATTERN = r"(.+)" +def _validate_multi_segment_value(val): + """Validate a multi-segment wildcard value for path traversal vulnerabilities. + + This function implements the dot and double-dot traversal validation rule + for values matching '**'. It splits the value by '/' into segments and + simulates path traversal: + - '.' segments do not modify the segment count. + - '..' segments decrease the leftover segment count by 1. + - Any other segments increase the leftover segment count by 1. + + Validation fails if: + - Path traversal overflows to the left (leftover segment count drops below 0), + meaning it would traverse out of the multi-segment value boundaries. + - No segments remain after executing all path traversal commands (leftover + segment count is 0), meaning the entire value is consumed. + + Args: + val (str): The value matched to the '**' wildcard to validate. + + Returns: + bool: True if the value is valid and does not violate traversal boundaries, + False otherwise. + """ + segments = val.split("/") + leftover_segments = 0 + unseen_segments = len(segments) + + for segment in segments: + unseen_segments -= 1 + if segment == "..": + leftover_segments -= 1 + elif segment != ".": + leftover_segments += 1 + + if leftover_segments < 0: + return False + if leftover_segments > unseen_segments: + return True + + return leftover_segments > 0 + + +def _build_capture_pattern(template_str): + """Build a regex pattern to capture wildcard matches from a template. + + This function parses a template string containing positional/named + wildcards ('*' or '**'), compiles it into a regular expression, and + records the order and types of all wildcards to allow extracting + sub-segments for individual validation. + + Args: + template_str (str): The template string (e.g. "projects/*/locations/*"). + + Returns: + tuple[str, list[str]]: A tuple containing: + - The compiled regex pattern string with capture groups. + - A list of wildcard type strings ('*' or '**') in matching order. + """ + wildcard_types = [] + + def replacer(match): + positional = match.group("positional") + name = match.group("name") + template = match.group("template") + + if positional == "*": + wildcard_types.append("*") + return r"([^/]+)" + elif positional == "**": + wildcard_types.append("**") + return r"(.+)" + elif name is not None: + if not template or template == "*": + wildcard_types.append("*") + return r"([^/]+)" + elif template == "**": + wildcard_types.append("**") + return r"(.+)" + else: + sub_pattern, sub_types = _build_capture_pattern(template) + wildcard_types.extend(sub_types) + return sub_pattern + return match.group(0) + + parts = [] + last_idx = 0 + for match in _VARIABLE_RE.finditer(template_str): + literal = template_str[last_idx : match.start()] + parts.append(re.escape(literal)) + replaced = replacer(match) + parts.append(replaced) + last_idx = match.end() + literal = template_str[last_idx:] + parts.append(re.escape(literal)) + + pattern = "".join(parts) + return pattern, wildcard_types + + +def _validate_value_against_template(val, template_str, property_name=None): + """Validate a variable's value against its template structure. + + This function extracts substrings matching individual wildcards in the + variable's template structure and enforces safety constraints: + - Single-segment matches ('*') must not be exactly '.' or '..' because + this breaks the URI routing contract and leads to path traversal. + - Multi-segment matches ('**') are checked using _validate_multi_segment_value + to ensure path traversal commands do not consume the entire value or + escape the starting boundaries of the matched parameter. + + Args: + val (str): The raw string value to validate. + template_str (str): The template string of the variable (e.g. 'projects/*/locations/*'). + property_name (str | None): The name of the property being validated, used + to construct descriptive error messages. + + Raises: + ValueError: If a wildcard within the value violates path traversal rules. + """ + if template_str is None or template_str == "*": + pattern, wildcard_types = r"^([^/]+)$", ["*"] + elif template_str == "**": + pattern, wildcard_types = r"^(.+)$", ["**"] + else: + pattern_body, wildcard_types = _build_capture_pattern(template_str) + pattern = "^" + pattern_body + "$" + + m = re.match(pattern, val) + if m is not None: + for i, wildcard_type in enumerate(wildcard_types): + captured_val = m.group(i + 1) + if wildcard_type == "*": + if captured_val in (".", ".."): + name_str = property_name if property_name else "positional variable" + raise ValueError("Invalid value {} for {}.".format(val, name_str)) + elif wildcard_type == "**": + if not _validate_multi_segment_value(captured_val): + name_str = property_name if property_name else "positional variable" + raise ValueError("Invalid value {} for {}.".format(val, name_str)) + + def _expand_variable_match(positional_vars, named_vars, match): """Expand a matched variable with its value. @@ -82,10 +223,12 @@ def _expand_variable_match(positional_vars, named_vars, match): """ positional = match.group("positional") name = match.group("name") + template = match.group("template") + if name is not None: try: val = str(named_vars[name]) - # Percent-encode while keeping '/' safe to preserve existing route and slash validation + _validate_value_against_template(val, template, name) return urllib.parse.quote(val, safe="/") except KeyError: raise ValueError( @@ -95,7 +238,7 @@ def _expand_variable_match(positional_vars, named_vars, match): elif positional is not None: try: val = str(positional_vars.pop(0)) - # Percent-encode while keeping '/' safe to preserve existing route and slash validation + _validate_value_against_template(val, positional, None) return urllib.parse.quote(val, safe="/") except IndexError: raise ValueError( @@ -326,8 +469,7 @@ def transcode(http_options, message=None, **request_kwargs): return request bindings_description = [ - '\n\tURI: "{}"' - "\n\tRequired request fields:\n\t\t{}".format( + '\n\tURI: "{}"\n\tRequired request fields:\n\t\t{}'.format( uri, "\n\t\t".join( [ diff --git a/packages/google-api-core/tests/unit/test_path_template.py b/packages/google-api-core/tests/unit/test_path_template.py index db918b83140e..4b7fdbff439e 100644 --- a/packages/google-api-core/tests/unit/test_path_template.py +++ b/packages/google-api-core/tests/unit/test_path_template.py @@ -66,12 +66,24 @@ # Encoding / Metacharacters in positional and named params ["/v1/*", ["..?$httpMethod=DELETE#"], {}, "/v1/..%3F%24httpMethod%3DDELETE%23"], ["/v1/**", ["path/../with/?and#"], {}, "/v1/path/../with/%3Fand%23"], - ["/v1/{name}", [], {"name": "..?$httpMethod=DELETE#"}, "/v1/..%3F%24httpMethod%3DDELETE%23"], - ["/v1/{name=**}", [], {"name": "path/../with/?and#"}, "/v1/path/../with/%3Fand%23"], + [ + "/v1/{name}", + [], + {"name": "..?$httpMethod=DELETE#"}, + "/v1/..%3F%24httpMethod%3DDELETE%23", + ], + [ + "/v1/{name=**}", + [], + {"name": "path/../with/?and#"}, + "/v1/path/../with/%3Fand%23", + ], [ "/v3/{session=projects/*/locations/*/agents/*/sessions/*}:detectIntent", [], - {"session": "projects/cx/locations/global/agents/a1/sessions/..?$httpMethod=DELETE#"}, + { + "session": "projects/cx/locations/global/agents/a1/sessions/..?$httpMethod=DELETE#" + }, "/v3/projects/cx/locations/global/agents/a1/sessions/..%3F%24httpMethod%3DDELETE%23:detectIntent", ], ], @@ -661,3 +673,100 @@ def helper_test_transcode(http_options_list, expected_result_list): if expected_result_list[2]: expected_result["body"] = expected_result_list[2] return (http_options, expected_result) + + +def test_path_traversal_dots_validation_star(): + # 1. Dots in values matched to * + # Single-segment positional variable * with exactly '.' or '..' + with pytest.raises( + ValueError, match="Invalid value \\. for positional variable\\." + ): + path_template.expand("/v1/*", ".") + with pytest.raises( + ValueError, match="Invalid value \\.\\. for positional variable\\." + ): + path_template.expand("/v1/*", "..") + + # Named variable matching * with exactly '.' or '..' + with pytest.raises(ValueError, match="Invalid value \\.\\. for region\\."): + path_template.expand( + "/compute/v1/projects/{project}/regions/{region}/addresses", + project="my-project", + region="..", + ) + + # Sub-template named variable where a segment matching * is exactly '.' or '..' + with pytest.raises( + ValueError, + match="Invalid value projects/my-project/locations/\\.\\. for parent\\.", + ): + path_template.expand( + "/v2/{parent=projects/*/locations/*}/content:inspect", + parent="projects/my-project/locations/..", + ) + with pytest.raises( + ValueError, + match="Invalid value projects/my-project/locations/\\. for parent\\.", + ): + path_template.expand( + "/v2/{parent=projects/*/locations/*}/content:inspect", + parent="projects/my-project/locations/.", + ) + + +def test_path_traversal_dots_validation_double_star(): + # 2. Dots in values matched to ** + # Valid cases: leftover_segments > 0 + # leftover_segments == 1 + assert ( + path_template.expand( + "/v3/{name=projects/*/monitoredResourceDescriptors/**}", + name="projects/my-project/monitoredResourceDescriptors/instance/my-instance/..", + ) + == "/v3/projects/my-project/monitoredResourceDescriptors/instance/my-instance/.." + ) + + assert ( + path_template.expand( + "/v3/{name=projects/*/monitoredResourceDescriptors/**}", + name="projects/my-project/monitoredResourceDescriptors/instance/my-instance/.", + ) + == "/v3/projects/my-project/monitoredResourceDescriptors/instance/my-instance/." + ) + + assert ( + path_template.expand( + "/v3/{name=projects/*/monitoredResourceDescriptors/**}", + name="projects/my-project/monitoredResourceDescriptors/a/b/c/d/e/../../../..", + ) + == "/v3/projects/my-project/monitoredResourceDescriptors/a/b/c/d/e/../../../.." + ) + + # Invalid cases: resulting path traversal consumes whole value or overflows left + invalid_cases = [ + "projects/my-project/monitoredResourceDescriptors/.", + "projects/my-project/monitoredResourceDescriptors/instance/my-instance/../..", + "projects/my-project/monitoredResourceDescriptors/instance/../my-instance/..", + "projects/my-project/monitoredResourceDescriptors/..", + "projects/my-project/monitoredResourceDescriptors/instance/../..", + "projects/my-project/monitoredResourceDescriptors/a/b/../../../c/d/e/..", + ] + for case in invalid_cases: + with pytest.raises(ValueError, match="Invalid value .* for name\\."): + path_template.expand( + "/v3/{name=projects/*/monitoredResourceDescriptors/**}", name=case + ) + + +def test_percent_encoding_unreserved_characters(): + # 3. Values for all variable parts should be percent-encoded except for [-_.~/0-9a-zA-Z] characters. + # We should keep [-_.~/0-9a-zA-Z] safe for both single-star and double-star + result = path_template.expand("/v1/{name}", name="abc-._~") + assert result == "/v1/abc-._~" + + result = path_template.expand("/v1/{name=**}", name="abc-._~/") + assert result == "/v1/abc-._~/" + + # Other characters like '$', '?', '=', '#', ' ' should be encoded + result = path_template.expand("/v1/{name}", name="a$b?c=d#e f") + assert result == "/v1/a%24b%3Fc%3Dd%23e%20f" From 7c05cdcf5026991b3f59381401533411646c17ca Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 16 Jul 2026 13:46:44 -0700 Subject: [PATCH 03/15] added perf improvements --- .../google/api_core/path_template.py | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/google-api-core/google/api_core/path_template.py b/packages/google-api-core/google/api_core/path_template.py index 4580040f5080..49a06921a2af 100644 --- a/packages/google-api-core/google/api_core/path_template.py +++ b/packages/google-api-core/google/api_core/path_template.py @@ -105,6 +105,7 @@ def _validate_multi_segment_value(val): return leftover_segments > 0 +@functools.lru_cache(maxsize=1024) def _build_capture_pattern(template_str): """Build a regex pattern to capture wildcard matches from a template. @@ -159,7 +160,8 @@ def replacer(match): parts.append(re.escape(literal)) pattern = "".join(parts) - return pattern, wildcard_types + # Convert wildcard_types to a tuple to ensure the return value is fully hashable and robust + return pattern, tuple(wildcard_types) def _validate_value_against_template(val, template_str, property_name=None): @@ -183,12 +185,20 @@ def _validate_value_against_template(val, template_str, property_name=None): ValueError: If a wildcard within the value violates path traversal rules. """ if template_str is None or template_str == "*": - pattern, wildcard_types = r"^([^/]+)$", ["*"] - elif template_str == "**": - pattern, wildcard_types = r"^(.+)$", ["**"] - else: - pattern_body, wildcard_types = _build_capture_pattern(template_str) - pattern = "^" + pattern_body + "$" + if val in (".", ".."): + name_str = property_name if property_name else "positional variable" + raise ValueError("Invalid value {} for {}.".format(val, name_str)) + return + + if template_str == "**": + if not _validate_multi_segment_value(val): + name_str = property_name if property_name else "positional variable" + raise ValueError("Invalid value {} for {}.".format(val, name_str)) + return + + # Sub-template case: use cached capture pattern and regex match + pattern_body, wildcard_types = _build_capture_pattern(template_str) + pattern = "^" + pattern_body + "$" m = re.match(pattern, val) if m is not None: From f39758fd58430ad63edbde9cc1852f8273a2f88b Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 16 Jul 2026 14:04:25 -0700 Subject: [PATCH 04/15] improving test cases --- .../tests/unit/test_path_template.py | 173 ++++++++++-------- 1 file changed, 96 insertions(+), 77 deletions(-) diff --git a/packages/google-api-core/tests/unit/test_path_template.py b/packages/google-api-core/tests/unit/test_path_template.py index 4b7fdbff439e..b1b0607f2c10 100644 --- a/packages/google-api-core/tests/unit/test_path_template.py +++ b/packages/google-api-core/tests/unit/test_path_template.py @@ -675,98 +675,117 @@ def helper_test_transcode(http_options_list, expected_result_list): return (http_options, expected_result) -def test_path_traversal_dots_validation_star(): - # 1. Dots in values matched to * - # Single-segment positional variable * with exactly '.' or '..' - with pytest.raises( - ValueError, match="Invalid value \\. for positional variable\\." - ): - path_template.expand("/v1/*", ".") - with pytest.raises( - ValueError, match="Invalid value \\.\\. for positional variable\\." - ): - path_template.expand("/v1/*", "..") - - # Named variable matching * with exactly '.' or '..' - with pytest.raises(ValueError, match="Invalid value \\.\\. for region\\."): - path_template.expand( +@pytest.mark.parametrize( + "tmpl, args, kwargs, expected_err_match", + [ + # Positional * with . or .. + [ + "/v1/*", + [ + ".", + ], + {}, + "Invalid value \\. for positional variable\\.", + ], + [ + "/v1/*", + [ + "..", + ], + {}, + "Invalid value \\.\\. for positional variable\\.", + ], + # Named matching * with . or .. + [ "/compute/v1/projects/{project}/regions/{region}/addresses", - project="my-project", - region="..", - ) - - # Sub-template named variable where a segment matching * is exactly '.' or '..' - with pytest.raises( - ValueError, - match="Invalid value projects/my-project/locations/\\.\\. for parent\\.", - ): - path_template.expand( + [], + {"project": "my-project", "region": ".."}, + "Invalid value \\.\\. for region\\.", + ], + [ + "/compute/v1/projects/{project}/regions/{region}/addresses", + [], + {"project": "my-project", "region": "."}, + "Invalid value \\. for region\\.", + ], + # Sub-template named matching * with . or .. + [ "/v2/{parent=projects/*/locations/*}/content:inspect", - parent="projects/my-project/locations/..", - ) - with pytest.raises( - ValueError, - match="Invalid value projects/my-project/locations/\\. for parent\\.", - ): - path_template.expand( + [], + {"parent": "projects/my-project/locations/.."}, + "Invalid value projects/my-project/locations/\\.\\. for parent\\.", + ], + [ "/v2/{parent=projects/*/locations/*}/content:inspect", - parent="projects/my-project/locations/.", - ) - + [], + {"parent": "projects/my-project/locations/."}, + "Invalid value projects/my-project/locations/\\. for parent\\.", + ], + ], +) +def test_path_traversal_dots_validation_star(tmpl, args, kwargs, expected_err_match): + with pytest.raises(ValueError, match=expected_err_match): + path_template.expand(tmpl, *args, **kwargs) -def test_path_traversal_dots_validation_double_star(): - # 2. Dots in values matched to ** - # Valid cases: leftover_segments > 0 - # leftover_segments == 1 - assert ( - path_template.expand( - "/v3/{name=projects/*/monitoredResourceDescriptors/**}", - name="projects/my-project/monitoredResourceDescriptors/instance/my-instance/..", - ) - == "/v3/projects/my-project/monitoredResourceDescriptors/instance/my-instance/.." - ) +@pytest.mark.parametrize( + "name_val, expected_path", + [ + ( + "projects/my-project/monitoredResourceDescriptors/instance/my-instance/..", + "/v3/projects/my-project/monitoredResourceDescriptors/instance/my-instance/..", + ), + ( + "projects/my-project/monitoredResourceDescriptors/instance/my-instance/.", + "/v3/projects/my-project/monitoredResourceDescriptors/instance/my-instance/.", + ), + ( + "projects/my-project/monitoredResourceDescriptors/a/b/c/d/e/../../../..", + "/v3/projects/my-project/monitoredResourceDescriptors/a/b/c/d/e/../../../..", + ), + ], +) +def test_path_traversal_dots_validation_double_star_valid(name_val, expected_path): assert ( path_template.expand( "/v3/{name=projects/*/monitoredResourceDescriptors/**}", - name="projects/my-project/monitoredResourceDescriptors/instance/my-instance/.", + name=name_val, ) - == "/v3/projects/my-project/monitoredResourceDescriptors/instance/my-instance/." + == expected_path ) - assert ( - path_template.expand( - "/v3/{name=projects/*/monitoredResourceDescriptors/**}", - name="projects/my-project/monitoredResourceDescriptors/a/b/c/d/e/../../../..", - ) - == "/v3/projects/my-project/monitoredResourceDescriptors/a/b/c/d/e/../../../.." - ) - # Invalid cases: resulting path traversal consumes whole value or overflows left - invalid_cases = [ +@pytest.mark.parametrize( + "name_val", + [ "projects/my-project/monitoredResourceDescriptors/.", "projects/my-project/monitoredResourceDescriptors/instance/my-instance/../..", "projects/my-project/monitoredResourceDescriptors/instance/../my-instance/..", "projects/my-project/monitoredResourceDescriptors/..", "projects/my-project/monitoredResourceDescriptors/instance/../..", "projects/my-project/monitoredResourceDescriptors/a/b/../../../c/d/e/..", - ] - for case in invalid_cases: - with pytest.raises(ValueError, match="Invalid value .* for name\\."): - path_template.expand( - "/v3/{name=projects/*/monitoredResourceDescriptors/**}", name=case - ) - - -def test_percent_encoding_unreserved_characters(): - # 3. Values for all variable parts should be percent-encoded except for [-_.~/0-9a-zA-Z] characters. - # We should keep [-_.~/0-9a-zA-Z] safe for both single-star and double-star - result = path_template.expand("/v1/{name}", name="abc-._~") - assert result == "/v1/abc-._~" - - result = path_template.expand("/v1/{name=**}", name="abc-._~/") - assert result == "/v1/abc-._~/" - - # Other characters like '$', '?', '=', '#', ' ' should be encoded - result = path_template.expand("/v1/{name}", name="a$b?c=d#e f") - assert result == "/v1/a%24b%3Fc%3Dd%23e%20f" + ], +) +def test_path_traversal_dots_validation_double_star_invalid(name_val): + with pytest.raises(ValueError, match="Invalid value .* for name\\."): + path_template.expand( + "/v3/{name=projects/*/monitoredResourceDescriptors/**}", + name=name_val, + ) + + +@pytest.mark.parametrize( + "tmpl, kwargs, expected_result", + [ + ["/v1/{name}", {"name": "abc-._~"}, "/v1/abc-._~"], + ["/v1/{name=**}", {"name": "abc-._~/"}, "/v1/abc-._~/"], + ["/v1/{name}", {"name": "a/b"}, "/v1/a/b"], + ["/v1/{name}", {"name": "a$b?c=d#e f"}, "/v1/a%24b%3Fc%3Dd%23e%20f"], + ], +) +def test_percent_encoding_unreserved_characters(tmpl, kwargs, expected_result): + result = path_template.expand(tmpl, **kwargs) + assert result == expected_result + # For single-segment with '/', validate should fail because '/' is preserved + if "/" in kwargs.get("name", "") and tmpl == "/v1/{name}": + assert not path_template.validate(tmpl, result) From 82e154b678cea4b68b13b267536763e9036b54ab Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 16 Jul 2026 14:15:04 -0700 Subject: [PATCH 05/15] imporoved docstrings --- .../google/api_core/path_template.py | 28 ++++++++++++++++--- .../tests/unit/test_path_template.py | 8 ++---- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/packages/google-api-core/google/api_core/path_template.py b/packages/google-api-core/google/api_core/path_template.py index 49a06921a2af..d63e279a7227 100644 --- a/packages/google-api-core/google/api_core/path_template.py +++ b/packages/google-api-core/google/api_core/path_template.py @@ -63,8 +63,8 @@ _MULTI_SEGMENT_PATTERN = r"(.+)" -def _validate_multi_segment_value(val): - """Validate a multi-segment wildcard value for path traversal vulnerabilities. +def _validate_multi_segment_value(val: str) -> bool: + """Validate a multi-segment wildcard value. This function implements the dot and double-dot traversal validation rule for values matching '**'. It splits the value by '/' into segments and @@ -79,6 +79,14 @@ def _validate_multi_segment_value(val): - No segments remain after executing all path traversal commands (leftover segment count is 0), meaning the entire value is consumed. + Examples: + >>> _validate_multi_segment_value("instance/my-instance") + True + >>> _validate_multi_segment_value("instance/my-instance/..") + True + >>> _validate_multi_segment_value("instance/../..") + False + Args: val (str): The value matched to the '**' wildcard to validate. @@ -106,7 +114,7 @@ def _validate_multi_segment_value(val): @functools.lru_cache(maxsize=1024) -def _build_capture_pattern(template_str): +def _build_capture_pattern(template_str: str) -> tuple[str, tuple[str, ...]]: """Build a regex pattern to capture wildcard matches from a template. This function parses a template string containing positional/named @@ -164,7 +172,9 @@ def replacer(match): return pattern, tuple(wildcard_types) -def _validate_value_against_template(val, template_str, property_name=None): +def _validate_value_against_template( + val: str, template_str: str | None, property_name: str | None = None +) -> None: """Validate a variable's value against its template structure. This function extracts substrings matching individual wildcards in the @@ -175,6 +185,16 @@ def _validate_value_against_template(val, template_str, property_name=None): to ensure path traversal commands do not consume the entire value or escape the starting boundaries of the matched parameter. + Examples: + >>> _validate_value_against_template("us-central1", None, "region") + None + >>> _validate_value_against_template("..", None, "region") + ValueError("Invalid value .. for region.") + >>> _validate_value_against_template( + ... "projects/my-proj/locations/.", "projects/*/locations/*", "parent" + ... ) + ValueError("Invalid value projects/my-proj/locations/. for parent.") + Args: val (str): The raw string value to validate. template_str (str): The template string of the variable (e.g. 'projects/*/locations/*'). diff --git a/packages/google-api-core/tests/unit/test_path_template.py b/packages/google-api-core/tests/unit/test_path_template.py index b1b0607f2c10..aeab97722191 100644 --- a/packages/google-api-core/tests/unit/test_path_template.py +++ b/packages/google-api-core/tests/unit/test_path_template.py @@ -681,17 +681,13 @@ def helper_test_transcode(http_options_list, expected_result_list): # Positional * with . or .. [ "/v1/*", - [ - ".", - ], + ["."], {}, "Invalid value \\. for positional variable\\.", ], [ "/v1/*", - [ - "..", - ], + [".."], {}, "Invalid value \\.\\. for positional variable\\.", ], From 105201b7a78cfce84214eaba2274158238bf5e3e Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 16 Jul 2026 15:55:55 -0700 Subject: [PATCH 06/15] handle empty segments --- packages/google-api-core/google/api_core/path_template.py | 2 +- packages/google-api-core/tests/unit/test_path_template.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/google-api-core/google/api_core/path_template.py b/packages/google-api-core/google/api_core/path_template.py index d63e279a7227..fc0dd039e615 100644 --- a/packages/google-api-core/google/api_core/path_template.py +++ b/packages/google-api-core/google/api_core/path_template.py @@ -102,7 +102,7 @@ def _validate_multi_segment_value(val: str) -> bool: unseen_segments -= 1 if segment == "..": leftover_segments -= 1 - elif segment != ".": + elif segment not in (".", ""): leftover_segments += 1 if leftover_segments < 0: diff --git a/packages/google-api-core/tests/unit/test_path_template.py b/packages/google-api-core/tests/unit/test_path_template.py index aeab97722191..adcaaa0e52d6 100644 --- a/packages/google-api-core/tests/unit/test_path_template.py +++ b/packages/google-api-core/tests/unit/test_path_template.py @@ -760,6 +760,7 @@ def test_path_traversal_dots_validation_double_star_valid(name_val, expected_pat "projects/my-project/monitoredResourceDescriptors/..", "projects/my-project/monitoredResourceDescriptors/instance/../..", "projects/my-project/monitoredResourceDescriptors/a/b/../../../c/d/e/..", + "projects/my-project/monitoredResourceDescriptors/instance//..", ], ) def test_path_traversal_dots_validation_double_star_invalid(name_val): From 030aa6f9a0ef2b7a031dca6543b8ca7a061b01d6 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 16 Jul 2026 15:59:37 -0700 Subject: [PATCH 07/15] updated method name --- .../google/api_core/path_template.py | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/google-api-core/google/api_core/path_template.py b/packages/google-api-core/google/api_core/path_template.py index fc0dd039e615..38d5b2228656 100644 --- a/packages/google-api-core/google/api_core/path_template.py +++ b/packages/google-api-core/google/api_core/path_template.py @@ -172,25 +172,31 @@ def replacer(match): return pattern, tuple(wildcard_types) -def _validate_value_against_template( +def _extract_and_validate_wildcards( val: str, template_str: str | None, property_name: str | None = None ) -> None: - """Validate a variable's value against its template structure. + """Extract and validate wildcard variables against path traversal rules. - This function extracts substrings matching individual wildcards in the - variable's template structure and enforces safety constraints: + This function attempts to structurally match the variable's value against + its template. If the value structurally matches, it extracts the substrings + corresponding to the individual wildcards and enforces safety constraints: - Single-segment matches ('*') must not be exactly '.' or '..' because this breaks the URI routing contract and leads to path traversal. - Multi-segment matches ('**') are checked using _validate_multi_segment_value to ensure path traversal commands do not consume the entire value or escape the starting boundaries of the matched parameter. + If the value does not structurally match the template, this function allows + it to pass through without error. This delegates the rejection of the malformed + string to the standard routing mechanisms (like `validate()`) to ensure that + `additional_bindings` are evaluated correctly. + Examples: - >>> _validate_value_against_template("us-central1", None, "region") + >>> _extract_and_validate_wildcards("us-central1", None, "region") None - >>> _validate_value_against_template("..", None, "region") + >>> _extract_and_validate_wildcards("..", None, "region") ValueError("Invalid value .. for region.") - >>> _validate_value_against_template( + >>> _extract_and_validate_wildcards( ... "projects/my-proj/locations/.", "projects/*/locations/*", "parent" ... ) ValueError("Invalid value projects/my-proj/locations/. for parent.") @@ -202,7 +208,7 @@ def _validate_value_against_template( to construct descriptive error messages. Raises: - ValueError: If a wildcard within the value violates path traversal rules. + ValueError: If a wildcard within a structurally valid value violates path traversal rules. """ if template_str is None or template_str == "*": if val in (".", ".."): @@ -258,7 +264,7 @@ def _expand_variable_match(positional_vars, named_vars, match): if name is not None: try: val = str(named_vars[name]) - _validate_value_against_template(val, template, name) + _extract_and_validate_wildcards(val, template, name) return urllib.parse.quote(val, safe="/") except KeyError: raise ValueError( @@ -268,7 +274,7 @@ def _expand_variable_match(positional_vars, named_vars, match): elif positional is not None: try: val = str(positional_vars.pop(0)) - _validate_value_against_template(val, positional, None) + _extract_and_validate_wildcards(val, positional, None) return urllib.parse.quote(val, safe="/") except IndexError: raise ValueError( From 47a702ed550f47da921731fc0b7285c8116cc59d Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 16 Jul 2026 16:48:56 -0700 Subject: [PATCH 08/15] improved regex cache --- .../google/api_core/path_template.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/google-api-core/google/api_core/path_template.py b/packages/google-api-core/google/api_core/path_template.py index 38d5b2228656..f44dfd66e958 100644 --- a/packages/google-api-core/google/api_core/path_template.py +++ b/packages/google-api-core/google/api_core/path_template.py @@ -114,7 +114,7 @@ def _validate_multi_segment_value(val: str) -> bool: @functools.lru_cache(maxsize=1024) -def _build_capture_pattern(template_str: str) -> tuple[str, tuple[str, ...]]: +def _build_capture_pattern(template_str: str) -> tuple[re.Pattern, tuple[str, ...]]: """Build a regex pattern to capture wildcard matches from a template. This function parses a template string containing positional/named @@ -126,7 +126,7 @@ def _build_capture_pattern(template_str: str) -> tuple[str, tuple[str, ...]]: template_str (str): The template string (e.g. "projects/*/locations/*"). Returns: - tuple[str, list[str]]: A tuple containing: + tuple[re.Pattern, tuple[str, ...]]: A tuple containing: - The compiled regex pattern string with capture groups. - A list of wildcard type strings ('*' or '**') in matching order. """ @@ -153,7 +153,7 @@ def replacer(match): else: sub_pattern, sub_types = _build_capture_pattern(template) wildcard_types.extend(sub_types) - return sub_pattern + return sub_pattern.pattern return match.group(0) parts = [] @@ -168,8 +168,7 @@ def replacer(match): parts.append(re.escape(literal)) pattern = "".join(parts) - # Convert wildcard_types to a tuple to ensure the return value is fully hashable and robust - return pattern, tuple(wildcard_types) + return re.compile(pattern), tuple(wildcard_types) def _extract_and_validate_wildcards( @@ -223,10 +222,9 @@ def _extract_and_validate_wildcards( return # Sub-template case: use cached capture pattern and regex match - pattern_body, wildcard_types = _build_capture_pattern(template_str) - pattern = "^" + pattern_body + "$" + pattern, wildcard_types = _build_capture_pattern(template_str) - m = re.match(pattern, val) + m = pattern.fullmatch(val) if m is not None: for i, wildcard_type in enumerate(wildcard_types): captured_val = m.group(i + 1) From 3384473dca2040e37ba696ddc14136d2a7294319 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 16 Jul 2026 17:05:29 -0700 Subject: [PATCH 09/15] fixed coverage --- .../google/api_core/path_template.py | 24 ++++++++-------- .../tests/unit/test_path_template.py | 28 ++++++++++++++++++- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/packages/google-api-core/google/api_core/path_template.py b/packages/google-api-core/google/api_core/path_template.py index f44dfd66e958..e5ddde09fb6a 100644 --- a/packages/google-api-core/google/api_core/path_template.py +++ b/packages/google-api-core/google/api_core/path_template.py @@ -143,18 +143,16 @@ def replacer(match): elif positional == "**": wildcard_types.append("**") return r"(.+)" - elif name is not None: - if not template or template == "*": - wildcard_types.append("*") - return r"([^/]+)" - elif template == "**": - wildcard_types.append("**") - return r"(.+)" - else: - sub_pattern, sub_types = _build_capture_pattern(template) - wildcard_types.extend(sub_types) - return sub_pattern.pattern - return match.group(0) + elif not template or template == "*": + wildcard_types.append("*") + return r"([^/]+)" + elif template == "**": + wildcard_types.append("**") + return r"(.+)" + else: + sub_pattern, sub_types = _build_capture_pattern(template) + wildcard_types.extend(sub_types) + return sub_pattern.pattern parts = [] last_idx = 0 @@ -232,7 +230,7 @@ def _extract_and_validate_wildcards( if captured_val in (".", ".."): name_str = property_name if property_name else "positional variable" raise ValueError("Invalid value {} for {}.".format(val, name_str)) - elif wildcard_type == "**": + else: if not _validate_multi_segment_value(captured_val): name_str = property_name if property_name else "positional variable" raise ValueError("Invalid value {} for {}.".format(val, name_str)) diff --git a/packages/google-api-core/tests/unit/test_path_template.py b/packages/google-api-core/tests/unit/test_path_template.py index adcaaa0e52d6..6f47e4f37ab0 100644 --- a/packages/google-api-core/tests/unit/test_path_template.py +++ b/packages/google-api-core/tests/unit/test_path_template.py @@ -764,7 +764,7 @@ def test_path_traversal_dots_validation_double_star_valid(name_val, expected_pat ], ) def test_path_traversal_dots_validation_double_star_invalid(name_val): - with pytest.raises(ValueError, match="Invalid value .* for name\\."): + with pytest.raises(ValueError, match=r"Invalid value .* for name\."): path_template.expand( "/v3/{name=projects/*/monitoredResourceDescriptors/**}", name=name_val, @@ -786,3 +786,29 @@ def test_percent_encoding_unreserved_characters(tmpl, kwargs, expected_result): # For single-segment with '/', validate should fail because '/' is preserved if "/" in kwargs.get("name", "") and tmpl == "/v1/{name}": assert not path_template.validate(tmpl, result) + + +@pytest.mark.parametrize( + "tmpl, args, kwargs, expected_err_match", + [ + ("/v1/**", [".."], {}, r"Invalid value .* for positional variable\."), + ("/v1/{name=**}", [], {"name": ".."}, r"Invalid value .* for name\."), + ], +) +def test_path_traversal_dots_validation_bare_double_star(tmpl, args, kwargs, expected_err_match): + with pytest.raises(ValueError, match=expected_err_match): + path_template.expand(tmpl, *args, **kwargs) + +@pytest.mark.parametrize( + "template_str, expected_pattern, expected_wildcards", + [ + ("projects/{project}/locations/{location}", "projects/([^/]+)/locations/([^/]+)", ("*", "*")), + ("projects/{project=**}", "projects/(.+)", ("**",)), + ("projects/{project=locations/*}", "projects/locations/([^/]+)", ("*",)), + ("projects/*/locations/**", "projects/([^/]+)/locations/(.+)", ("*", "**")), + ], +) +def test_build_capture_pattern(template_str, expected_pattern, expected_wildcards): + pattern, wildcards = path_template._build_capture_pattern(template_str) + assert pattern.pattern == expected_pattern + assert wildcards == expected_wildcards From 33741f875041cfaf5ac7a0aa45263182c02dded4 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 16 Jul 2026 17:10:38 -0700 Subject: [PATCH 10/15] fixed lint --- .../google-api-core/google/api_core/path_template.py | 1 - .../google-api-core/tests/unit/test_path_template.py | 11 +++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/google-api-core/google/api_core/path_template.py b/packages/google-api-core/google/api_core/path_template.py index e5ddde09fb6a..2325f64a949c 100644 --- a/packages/google-api-core/google/api_core/path_template.py +++ b/packages/google-api-core/google/api_core/path_template.py @@ -134,7 +134,6 @@ def _build_capture_pattern(template_str: str) -> tuple[re.Pattern, tuple[str, .. def replacer(match): positional = match.group("positional") - name = match.group("name") template = match.group("template") if positional == "*": diff --git a/packages/google-api-core/tests/unit/test_path_template.py b/packages/google-api-core/tests/unit/test_path_template.py index 6f47e4f37ab0..d7c1abc29043 100644 --- a/packages/google-api-core/tests/unit/test_path_template.py +++ b/packages/google-api-core/tests/unit/test_path_template.py @@ -795,14 +795,21 @@ def test_percent_encoding_unreserved_characters(tmpl, kwargs, expected_result): ("/v1/{name=**}", [], {"name": ".."}, r"Invalid value .* for name\."), ], ) -def test_path_traversal_dots_validation_bare_double_star(tmpl, args, kwargs, expected_err_match): +def test_path_traversal_dots_validation_bare_double_star( + tmpl, args, kwargs, expected_err_match +): with pytest.raises(ValueError, match=expected_err_match): path_template.expand(tmpl, *args, **kwargs) + @pytest.mark.parametrize( "template_str, expected_pattern, expected_wildcards", [ - ("projects/{project}/locations/{location}", "projects/([^/]+)/locations/([^/]+)", ("*", "*")), + ( + "projects/{project}/locations/{location}", + "projects/([^/]+)/locations/([^/]+)", + ("*", "*"), + ), ("projects/{project=**}", "projects/(.+)", ("**",)), ("projects/{project=locations/*}", "projects/locations/([^/]+)", ("*",)), ("projects/*/locations/**", "projects/([^/]+)/locations/(.+)", ("*", "**")), From 7c8b94eb461f8be7762fd4ce68dc6e0483f8f6cb Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 16 Jul 2026 17:37:26 -0700 Subject: [PATCH 11/15] added extra check for valdidity --- .../google/api_core/path_template.py | 31 +++++++++++++------ .../tests/unit/test_path_template.py | 13 ++++++++ 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/packages/google-api-core/google/api_core/path_template.py b/packages/google-api-core/google/api_core/path_template.py index 2325f64a949c..e7e71925cbe8 100644 --- a/packages/google-api-core/google/api_core/path_template.py +++ b/packages/google-api-core/google/api_core/path_template.py @@ -206,33 +206,44 @@ def _extract_and_validate_wildcards( Raises: ValueError: If a wildcard within a structurally valid value violates path traversal rules. """ + err = ValueError( + f"Invalid value {val} for {property_name or 'positional variable'}." + ) + + # Single-segment templates (None or "*") cannot match exactly "." or ".." + # and cannot have multi-segment paths resolving to 0 segments. if template_str is None or template_str == "*": - if val in (".", ".."): - name_str = property_name if property_name else "positional variable" - raise ValueError("Invalid value {} for {}.".format(val, name_str)) + if val in (".", "..") or (val and not _validate_multi_segment_value(val)): + raise err return + # Multi-segment templates ("**") must represent at least one valid, non-escaped segment. if template_str == "**": if not _validate_multi_segment_value(val): - name_str = property_name if property_name else "positional variable" - raise ValueError("Invalid value {} for {}.".format(val, name_str)) + raise err return - # Sub-template case: use cached capture pattern and regex match + # Compile the sub-template into a regex capture pattern + # to isolate and validate individual wildcard values. pattern, wildcard_types = _build_capture_pattern(template_str) m = pattern.fullmatch(val) if m is not None: + # Validate each wildcard value within its matched boundaries, + # preventing traversals from escaping their structural positions. for i, wildcard_type in enumerate(wildcard_types): captured_val = m.group(i + 1) if wildcard_type == "*": if captured_val in (".", ".."): - name_str = property_name if property_name else "positional variable" - raise ValueError("Invalid value {} for {}.".format(val, name_str)) + raise err else: if not _validate_multi_segment_value(captured_val): - name_str = property_name if property_name else "positional variable" - raise ValueError("Invalid value {} for {}.".format(val, name_str)) + raise err + else: + # For values that don't match the pattern, ensure the value doesn't + # resolve to 0 segments (e.g. "projects/.."). + if val and not _validate_multi_segment_value(val): + raise err def _expand_variable_match(positional_vars, named_vars, match): diff --git a/packages/google-api-core/tests/unit/test_path_template.py b/packages/google-api-core/tests/unit/test_path_template.py index d7c1abc29043..7f6bf71b8728 100644 --- a/packages/google-api-core/tests/unit/test_path_template.py +++ b/packages/google-api-core/tests/unit/test_path_template.py @@ -717,6 +717,19 @@ def helper_test_transcode(http_options_list, expected_result_list): {"parent": "projects/my-project/locations/."}, "Invalid value projects/my-project/locations/\\. for parent\\.", ], + # Non-matching values with path traversal (bypass prevention) + [ + "/v2/{parent=projects/*/locations/*}/content:inspect", + [], + {"parent": "projects/.."}, + "Invalid value projects/\\.\\. for parent\\.", + ], + [ + "/v1/{name}", + [], + {"name": "projects/.."}, + "Invalid value projects/\\.\\. for name\\.", + ], ], ) def test_path_traversal_dots_validation_star(tmpl, args, kwargs, expected_err_match): From 53e0d32e58e0136b2363006d344042b07cd146f1 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 16 Jul 2026 17:43:56 -0700 Subject: [PATCH 12/15] fix lint --- packages/google-api-core/google/api_core/path_template.py | 4 ++-- packages/google-api-core/tests/unit/test_path_template.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/google-api-core/google/api_core/path_template.py b/packages/google-api-core/google/api_core/path_template.py index e7e71925cbe8..2413746233e0 100644 --- a/packages/google-api-core/google/api_core/path_template.py +++ b/packages/google-api-core/google/api_core/path_template.py @@ -25,11 +25,11 @@ from __future__ import unicode_literals -from collections import deque import copy import functools import re import urllib.parse +from collections import deque # Regular expression for extracting variable parts from a path template. # The variables can be expressed as: @@ -240,7 +240,7 @@ def _extract_and_validate_wildcards( if not _validate_multi_segment_value(captured_val): raise err else: - # For values that don't match the pattern, ensure the value doesn't + # For values that don't match the pattern, ensure the value doesn't # resolve to 0 segments (e.g. "projects/.."). if val and not _validate_multi_segment_value(val): raise err diff --git a/packages/google-api-core/tests/unit/test_path_template.py b/packages/google-api-core/tests/unit/test_path_template.py index 7f6bf71b8728..d64be56477d6 100644 --- a/packages/google-api-core/tests/unit/test_path_template.py +++ b/packages/google-api-core/tests/unit/test_path_template.py @@ -13,11 +13,12 @@ # limitations under the License. from __future__ import unicode_literals + from unittest import mock import pytest - from google.api import auth_pb2 + from google.api_core import path_template From b0d99b9b13e6cd891bb3494f35452111c25d3261 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 16 Jul 2026 17:53:47 -0700 Subject: [PATCH 13/15] added test cases --- .../tests/unit/test_path_template.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/google-api-core/tests/unit/test_path_template.py b/packages/google-api-core/tests/unit/test_path_template.py index d64be56477d6..32e9e23efb4d 100644 --- a/packages/google-api-core/tests/unit/test_path_template.py +++ b/packages/google-api-core/tests/unit/test_path_template.py @@ -833,3 +833,23 @@ def test_build_capture_pattern(template_str, expected_pattern, expected_wildcard pattern, wildcards = path_template._build_capture_pattern(template_str) assert pattern.pattern == expected_pattern assert wildcards == expected_wildcards + + +@pytest.mark.parametrize( + "val, expected_valid", + [ + ("a/b/c", True), + ("a/../b", True), + ("a/b/c/d/e/../../../..", True), + ("a/b/../..", False), + ("a/b/../../..", False), + (".", False), + ("..", False), + ("instance//..", False), + ("instance/my-instance///../..", False), + ], +) +def test_validate_multi_segment_value(val, expected_valid): + result = path_template._validate_multi_segment_value(val) + assert result == expected_valid + From 172aceeb1345dda1afdaff37a3cdc55549ba841c Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 16 Jul 2026 17:55:15 -0700 Subject: [PATCH 14/15] fix lint --- packages/google-api-core/tests/unit/test_path_template.py | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/google-api-core/tests/unit/test_path_template.py b/packages/google-api-core/tests/unit/test_path_template.py index 32e9e23efb4d..3e385ca883e0 100644 --- a/packages/google-api-core/tests/unit/test_path_template.py +++ b/packages/google-api-core/tests/unit/test_path_template.py @@ -852,4 +852,3 @@ def test_build_capture_pattern(template_str, expected_pattern, expected_wildcard def test_validate_multi_segment_value(val, expected_valid): result = path_template._validate_multi_segment_value(val) assert result == expected_valid - From 0d6d1ec5966b6f316e1a346ab0c6c8be38bac909 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 17 Jul 2026 15:21:21 -0700 Subject: [PATCH 15/15] Update packages/google-api-core/tests/unit/test_path_template.py Co-authored-by: Chalmer Lowe --- packages/google-api-core/tests/unit/test_path_template.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/google-api-core/tests/unit/test_path_template.py b/packages/google-api-core/tests/unit/test_path_template.py index 3e385ca883e0..68f03c8daa36 100644 --- a/packages/google-api-core/tests/unit/test_path_template.py +++ b/packages/google-api-core/tests/unit/test_path_template.py @@ -845,6 +845,11 @@ def test_build_capture_pattern(template_str, expected_pattern, expected_wildcard ("a/b/../../..", False), (".", False), ("..", False), + ("", False), + ("/", False), + ("//", False), + ("a/../../b", False), + ("../..", False), ("instance//..", False), ("instance/my-instance///../..", False), ],