6363_MULTI_SEGMENT_PATTERN = r"(.+)"
6464
6565
66+ def _validate_multi_segment_value (val ):
67+ """Validate a multi-segment wildcard value for path traversal vulnerabilities.
68+
69+ This function implements the dot and double-dot traversal validation rule
70+ for values matching '**'. It splits the value by '/' into segments and
71+ simulates path traversal:
72+ - '.' segments do not modify the segment count.
73+ - '..' segments decrease the leftover segment count by 1.
74+ - Any other segments increase the leftover segment count by 1.
75+
76+ Validation fails if:
77+ - Path traversal overflows to the left (leftover segment count drops below 0),
78+ meaning it would traverse out of the multi-segment value boundaries.
79+ - No segments remain after executing all path traversal commands (leftover
80+ segment count is 0), meaning the entire value is consumed.
81+
82+ Args:
83+ val (str): The value matched to the '**' wildcard to validate.
84+
85+ Returns:
86+ bool: True if the value is valid and does not violate traversal boundaries,
87+ False otherwise.
88+ """
89+ segments = val .split ("/" )
90+ leftover_segments = 0
91+ unseen_segments = len (segments )
92+
93+ for segment in segments :
94+ unseen_segments -= 1
95+ if segment == ".." :
96+ leftover_segments -= 1
97+ elif segment != "." :
98+ leftover_segments += 1
99+
100+ if leftover_segments < 0 :
101+ return False
102+ if leftover_segments > unseen_segments :
103+ return True
104+
105+ return leftover_segments > 0
106+
107+
108+ def _build_capture_pattern (template_str ):
109+ """Build a regex pattern to capture wildcard matches from a template.
110+
111+ This function parses a template string containing positional/named
112+ wildcards ('*' or '**'), compiles it into a regular expression, and
113+ records the order and types of all wildcards to allow extracting
114+ sub-segments for individual validation.
115+
116+ Args:
117+ template_str (str): The template string (e.g. "projects/*/locations/*").
118+
119+ Returns:
120+ tuple[str, list[str]]: A tuple containing:
121+ - The compiled regex pattern string with capture groups.
122+ - A list of wildcard type strings ('*' or '**') in matching order.
123+ """
124+ wildcard_types = []
125+
126+ def replacer (match ):
127+ positional = match .group ("positional" )
128+ name = match .group ("name" )
129+ template = match .group ("template" )
130+
131+ if positional == "*" :
132+ wildcard_types .append ("*" )
133+ return r"([^/]+)"
134+ elif positional == "**" :
135+ wildcard_types .append ("**" )
136+ return r"(.+)"
137+ elif name is not None :
138+ if not template or template == "*" :
139+ wildcard_types .append ("*" )
140+ return r"([^/]+)"
141+ elif template == "**" :
142+ wildcard_types .append ("**" )
143+ return r"(.+)"
144+ else :
145+ sub_pattern , sub_types = _build_capture_pattern (template )
146+ wildcard_types .extend (sub_types )
147+ return sub_pattern
148+ return match .group (0 )
149+
150+ parts = []
151+ last_idx = 0
152+ for match in _VARIABLE_RE .finditer (template_str ):
153+ literal = template_str [last_idx : match .start ()]
154+ parts .append (re .escape (literal ))
155+ replaced = replacer (match )
156+ parts .append (replaced )
157+ last_idx = match .end ()
158+ literal = template_str [last_idx :]
159+ parts .append (re .escape (literal ))
160+
161+ pattern = "" .join (parts )
162+ return pattern , wildcard_types
163+
164+
165+ def _validate_value_against_template (val , template_str , property_name = None ):
166+ """Validate a variable's value against its template structure.
167+
168+ This function extracts substrings matching individual wildcards in the
169+ variable's template structure and enforces safety constraints:
170+ - Single-segment matches ('*') must not be exactly '.' or '..' because
171+ this breaks the URI routing contract and leads to path traversal.
172+ - Multi-segment matches ('**') are checked using _validate_multi_segment_value
173+ to ensure path traversal commands do not consume the entire value or
174+ escape the starting boundaries of the matched parameter.
175+
176+ Args:
177+ val (str): The raw string value to validate.
178+ template_str (str): The template string of the variable (e.g. 'projects/*/locations/*').
179+ property_name (str | None): The name of the property being validated, used
180+ to construct descriptive error messages.
181+
182+ Raises:
183+ ValueError: If a wildcard within the value violates path traversal rules.
184+ """
185+ if template_str is None or template_str == "*" :
186+ pattern , wildcard_types = r"^([^/]+)$" , ["*" ]
187+ elif template_str == "**" :
188+ pattern , wildcard_types = r"^(.+)$" , ["**" ]
189+ else :
190+ pattern_body , wildcard_types = _build_capture_pattern (template_str )
191+ pattern = "^" + pattern_body + "$"
192+
193+ m = re .match (pattern , val )
194+ if m is not None :
195+ for i , wildcard_type in enumerate (wildcard_types ):
196+ captured_val = m .group (i + 1 )
197+ if wildcard_type == "*" :
198+ if captured_val in ("." , ".." ):
199+ name_str = property_name if property_name else "positional variable"
200+ raise ValueError ("Invalid value {} for {}." .format (val , name_str ))
201+ elif wildcard_type == "**" :
202+ if not _validate_multi_segment_value (captured_val ):
203+ name_str = property_name if property_name else "positional variable"
204+ raise ValueError ("Invalid value {} for {}." .format (val , name_str ))
205+
206+
66207def _expand_variable_match (positional_vars , named_vars , match ):
67208 """Expand a matched variable with its value.
68209
@@ -82,10 +223,12 @@ def _expand_variable_match(positional_vars, named_vars, match):
82223 """
83224 positional = match .group ("positional" )
84225 name = match .group ("name" )
226+ template = match .group ("template" )
227+
85228 if name is not None :
86229 try :
87230 val = str (named_vars [name ])
88- # Percent-encode while keeping '/' safe to preserve existing route and slash validation
231+ _validate_value_against_template ( val , template , name )
89232 return urllib .parse .quote (val , safe = "/" )
90233 except KeyError :
91234 raise ValueError (
@@ -95,7 +238,7 @@ def _expand_variable_match(positional_vars, named_vars, match):
95238 elif positional is not None :
96239 try :
97240 val = str (positional_vars .pop (0 ))
98- # Percent-encode while keeping '/' safe to preserve existing route and slash validation
241+ _validate_value_against_template ( val , positional , None )
99242 return urllib .parse .quote (val , safe = "/" )
100243 except IndexError :
101244 raise ValueError (
@@ -326,8 +469,7 @@ def transcode(http_options, message=None, **request_kwargs):
326469 return request
327470
328471 bindings_description = [
329- '\n \t URI: "{}"'
330- "\n \t Required request fields:\n \t \t {}" .format (
472+ '\n \t URI: "{}"\n \t Required request fields:\n \t \t {}' .format (
331473 uri ,
332474 "\n \t \t " .join (
333475 [
0 commit comments