google-apis-core 1.2.5 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 496e7d887e5cfdbe739f62370d447dc9d663352a6385ab89e7bc7aa75f58d391
4
- data.tar.gz: d68a12cee717e901edd3f1092dbe338f1692c0ccaba75408096ed5a05256eecf
3
+ metadata.gz: 3732fd02580bd217827d1301e1737b4cf816324234980a06892e12f74970cbf5
4
+ data.tar.gz: ed2a0068e67097f5fe2bf39f80f382d27440bd479e5843df8995d5c85a61112a
5
5
  SHA512:
6
- metadata.gz: 01423b834b7449a7824b35a64bda7e31df896e0a40a5084217ff52186d467cbea8d3d2359475ebd5d0b7d3bc0585fbfaef1682965b72195262ef591c2ef8a5e9
7
- data.tar.gz: 53f83a8ed0557d57939c017bd5910e98d38e0211a25053f79ecf5ddfd528000d5382f8347769529f182263f4d2089ec371d1c575481e0070cef94051c29673c9
6
+ metadata.gz: f4216e6c22892a88e775a7210e2d2be785f6dacff4c5c53f43ede511124b0429117ec95dd92da9d546ab3e922205be2d678a69add9fda4f0cafa53a7108b05e0
7
+ data.tar.gz: d143809acffb08f0a09662825fbe733f33eae474dc16d40a72db64a34cf0d359a3ccb4584221d4dfb5b8e137775af23d31d47d84168d7097feca3c5af3c01e8c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Release History
2
2
 
3
+ ### 1.3.0 (2026-10-04)
4
+
5
+ #### Features
6
+
7
+ * implement path validation and traversal checks in HttpCommand ([#27476](https://github.com/googleapis/google-api-ruby-client/issues/27476))
8
+
3
9
  ### 1.2.5 (2026-07-20)
4
10
 
5
11
  #### Bug Fixes
@@ -31,7 +31,7 @@ module Google
31
31
  RETRIABLE_ERRORS = [Google::Apis::ServerError,
32
32
  Google::Apis::RateLimitError,
33
33
  Google::Apis::TransmissionError,
34
- Google::Apis::RequestTimeOutError]
34
+ Google::Apis::RequestTimeOutError].freeze
35
35
 
36
36
  begin
37
37
  require 'opencensus'
@@ -83,15 +83,15 @@ module Google
83
83
  self.url = url
84
84
  self.url = Addressable::Template.new(url) if url.is_a?(String)
85
85
  self.method = method
86
- self.header = Hash.new
86
+ self.header = {}
87
87
  self.body = body
88
88
  self.query = {}
89
89
  self.params = {}
90
90
  @opencensus_span = nil
91
91
  if OPENCENSUS_AVAILABLE
92
92
  logger.warn 'OpenCensus support is now deprecated. ' +
93
- 'Please refer https://github.com/googleapis/google-api-ruby-client#tracing for migrating to use OpenTelemetry.'
94
-
93
+ 'Please refer https://github.com/googleapis/google-api-ruby-client#tracing for migrating to use OpenTelemetry.'
94
+
95
95
  end
96
96
  end
97
97
 
@@ -114,34 +114,30 @@ module Google
114
114
  release!
115
115
  end
116
116
 
117
- def do_retry func, client
118
- begin
119
- Retriable.retriable tries: options.retries + 1,
120
- max_elapsed_time: options.max_elapsed_time,
121
- base_interval: options.base_interval,
122
- max_interval: options.max_interval,
123
- multiplier: options.multiplier,
124
- on: RETRIABLE_ERRORS do |try|
125
- # This 2nd level retriable only catches auth errors, and supports 1 retry, which allows
126
- # auth to be re-attempted without having to retry all sorts of other failures like
127
- # NotFound, etc
128
- auth_tries = (try == 1 && authorization_refreshable? ? 2 : 1)
129
- Retriable.retriable tries: auth_tries,
130
- on: [Google::Apis::AuthorizationError, Signet::AuthorizationError, Signet::RemoteServerError, Signet::UnexpectedStatusError],
131
- on_retry: proc { |*| refresh_authorization } do
132
- send(func, client).tap do |result|
133
- if block_given?
134
- yield result, nil
135
- end
136
- end
117
+ def do_retry(func, client)
118
+ Retriable.retriable tries: options.retries + 1,
119
+ max_elapsed_time: options.max_elapsed_time,
120
+ base_interval: options.base_interval,
121
+ max_interval: options.max_interval,
122
+ multiplier: options.multiplier,
123
+ on: RETRIABLE_ERRORS do |try|
124
+ # This 2nd level retriable only catches auth errors, and supports 1 retry, which allows
125
+ # auth to be re-attempted without having to retry all sorts of other failures like
126
+ # NotFound, etc
127
+ auth_tries = (try == 1 && authorization_refreshable? ? 2 : 1)
128
+ Retriable.retriable tries: auth_tries,
129
+ on: [Google::Apis::AuthorizationError, Signet::AuthorizationError, Signet::RemoteServerError, Signet::UnexpectedStatusError],
130
+ on_retry: proc { |*| refresh_authorization } do
131
+ send(func, client).tap do |result|
132
+ yield result, nil if block_given?
137
133
  end
138
134
  end
139
- rescue => e
140
- if block_given?
141
- yield nil, e
142
- else
143
- raise e
144
- end
135
+ end
136
+ rescue StandardError => e
137
+ if block_given?
138
+ yield nil, e
139
+ else
140
+ raise e
145
141
  end
146
142
  end
147
143
 
@@ -164,6 +160,7 @@ module Google
164
160
  #
165
161
  # @private
166
162
  # @return [void]
163
+ # @raise [Google::Apis::ClientError] If path parameter validation fails.
167
164
  def prepare!
168
165
  normalize_unicode = true
169
166
  if options
@@ -171,7 +168,10 @@ module Google
171
168
  query.update(options.query) if options.query
172
169
  normalize_unicode = options.normalize_unicode
173
170
  end
174
- self.url = url.expand(params, nil, normalize_unicode) if url.is_a?(Addressable::Template)
171
+ if url.is_a?(Addressable::Template)
172
+ validate_path_parameters!
173
+ self.url = encode_reserved_parameters(url, normalize_unicode).expand(params, nil, normalize_unicode)
174
+ end
175
175
  url.query_values = normalize_query_values(query).merge(url.query_values || {})
176
176
 
177
177
  if allow_form_encoding?
@@ -182,17 +182,16 @@ module Google
182
182
  @form_encoded = false
183
183
  end
184
184
 
185
- self.body = '' if self.body.nil? && [:post, :put, :patch].include?(method)
185
+ self.body = '' if self.body.nil? && %i[post put patch].include?(method)
186
186
  if defined?(::Google::Apis::Core::CompositeIO) && body.is_a?(::Google::Apis::Core::CompositeIO)
187
- header["Content-Length"] ||= body.size.to_s
187
+ header['Content-Length'] ||= body.size.to_s
188
188
  end
189
189
  end
190
190
 
191
191
  # Release any resources used by this command
192
192
  # @private
193
193
  # @return [void]
194
- def release!
195
- end
194
+ def release!; end
196
195
 
197
196
  # Check the response and either decode body or raise error
198
197
  #
@@ -298,7 +297,7 @@ module Google
298
297
  err = Google::Apis::TransmissionError.new(err)
299
298
  end
300
299
  block.call(nil, err) if block_given?
301
- fail err if rethrow || block.nil?
300
+ raise err if rethrow || block.nil?
302
301
  end
303
302
 
304
303
  # Execute the command once.
@@ -323,7 +322,7 @@ module Google
323
322
  logger.debug { safe_single_line_representation @http_res }
324
323
  response = process_response(@http_res.status.to_i, @http_res.headers, @http_res.body)
325
324
  success(response)
326
- rescue => e
325
+ rescue StandardError => e
327
326
  logger.debug { sprintf('Caught error %s', e) }
328
327
  error(e, rethrow: true)
329
328
  end
@@ -343,28 +342,33 @@ module Google
343
342
  end
344
343
 
345
344
  def allow_form_encoding?
346
- [:post, :put].include?(method) && body.nil?
345
+ %i[post put].include?(method) && body.nil?
347
346
  end
348
347
 
349
348
  # Set the API version header for the service if not empty.
350
349
  # @return [void]
351
- def set_api_version_header api_version
350
+ def set_api_version_header(api_version)
352
351
  self.header['X-Goog-Api-Version'] = api_version unless api_version.empty?
353
352
  end
354
353
 
355
354
  private
356
355
 
357
356
  UNSAFE_CLASS_NAMES = [
358
- "Google::Apis::CloudkmsV1::DecryptResponse",
359
- "Google::Apis::SecretmanagerV1::SecretPayload",
360
- "Google::Apis::SecretmanagerV1beta1::SecretPayload"
361
- ]
357
+ 'Google::Apis::CloudkmsV1::DecryptResponse',
358
+ 'Google::Apis::SecretmanagerV1::SecretPayload',
359
+ 'Google::Apis::SecretmanagerV1beta1::SecretPayload'
360
+ ].freeze
361
+
362
+ # Matches the two URI template expressions Discovery documents use: simple ({var})
363
+ # and reserved ({+var}). Group 1 is '+' for reserved expansion, group 2 the name.
364
+ TEMPLATE_VAR_PATTERN = /\{(\+?)(\w+)\}/.freeze
362
365
 
363
366
  module RedactingPPMethods
364
- def pp_object obj
367
+ def pp_object(obj)
365
368
  return super unless UNSAFE_CLASS_NAMES.include? obj.class.name
369
+
366
370
  object_address_group obj do
367
- text "(fields redacted)"
371
+ text '(fields redacted)'
368
372
  end
369
373
  end
370
374
  end
@@ -377,16 +381,16 @@ module Google
377
381
  include RedactingPPMethods
378
382
  end
379
383
 
380
- def safe_pretty_representation obj
381
- out = +""
384
+ def safe_pretty_representation(obj)
385
+ out = +''
382
386
  printer = RedactingPP.new out, 79
383
387
  printer.guard_inspect_key { printer.pp obj }
384
388
  printer.flush
385
389
  out << "\n"
386
390
  end
387
391
 
388
- def safe_single_line_representation obj
389
- out = +""
392
+ def safe_single_line_representation(obj)
393
+ out = +''
390
394
  printer = RedactingSingleLine.new out
391
395
  printer.guard_inspect_key { printer.pp obj }
392
396
  printer.flush
@@ -400,9 +404,9 @@ module Google
400
404
 
401
405
  @opencensus_span = OpenCensus::Trace.start_span url.path.to_s
402
406
  @opencensus_span.kind = OpenCensus::Trace::SpanBuilder::CLIENT
403
- @opencensus_span.put_attribute "http.host", url.host.to_s
404
- @opencensus_span.put_attribute "http.method", method.to_s.upcase
405
- @opencensus_span.put_attribute "http.path", url.path.to_s
407
+ @opencensus_span.put_attribute 'http.host', url.host.to_s
408
+ @opencensus_span.put_attribute 'http.method', method.to_s.upcase
409
+ @opencensus_span.put_attribute 'http.path', url.path.to_s
406
410
  sent_size =
407
411
  if body.respond_to? :bytesize
408
412
  body.bytesize
@@ -436,7 +440,7 @@ module Google
436
440
  status = @http_res.status.to_i
437
441
  if status > 0
438
442
  @opencensus_span.set_status map_http_status status
439
- @opencensus_span.put_attribute "http.status_code", status
443
+ @opencensus_span.put_attribute 'http.status_code', status
440
444
  end
441
445
  end
442
446
 
@@ -452,7 +456,7 @@ module Google
452
456
  @form_encoded
453
457
  end
454
458
 
455
- def map_http_status http_status
459
+ def map_http_status(http_status)
456
460
  case http_status
457
461
  when 200..399 then 0 # OK
458
462
  when 400 then 3 # INVALID_ARGUMENT
@@ -468,9 +472,8 @@ module Google
468
472
  end
469
473
 
470
474
  def normalize_query_values(input)
471
- input.inject({}) do |h, (k, v)|
475
+ input.each_with_object({}) do |(k, v), h|
472
476
  h[k] = normalize_query_value(v)
473
- h
474
477
  end
475
478
  end
476
479
 
@@ -484,6 +487,96 @@ module Google
484
487
  v.to_s
485
488
  end
486
489
  end
490
+
491
+ # Validates user-supplied path parameter values against the URL template specification
492
+ # to prevent directory traversal and parameter injection exploits.
493
+ #
494
+ # Validation rejects only dot segments. Other characters are percent-encoded instead.
495
+ #
496
+ # Validation Mechanism:
497
+ # 1. For simple variables ({var}, standard single-wildcard behavior):
498
+ # - Rejects if the value is exactly '.' or '..'.
499
+ # - Reserved characters such as '/', '?' and '#' are percent-encoded by Addressable.
500
+ # 2. For reserved variables ({+var}, double-wildcard behavior):
501
+ # - Splits the value by slash ('/') and rejects any segment that is exactly '.' or '..'.
502
+ # - Every character except '/' and unreserved characters is percent-encoded by
503
+ # #encode_reserved_parameters.
504
+ #
505
+ # @raise [Google::Apis::ClientError] If any validation check fails.
506
+ def validate_path_parameters!
507
+ template_pattern = url.pattern
508
+
509
+ variables = template_pattern.scan(TEMPLATE_VAR_PATTERN).map do |operator, var_name|
510
+ { name: var_name, reserved: operator == '+' }
511
+ end
512
+
513
+ variables.each do |v|
514
+ var_name = v[:name]
515
+ var_key = params.key?(var_name) ? var_name : var_name.to_sym
516
+ next unless params.key?(var_key)
517
+
518
+ value = params[var_key].to_s
519
+
520
+ if v[:reserved]
521
+ value.split('/').each do |seg|
522
+ if ['.', '..'].include?(seg)
523
+ raise Google::Apis::ClientError,
524
+ "Value for #{var_name} must not contain segments that are exactly '#{seg}'."
525
+ end
526
+ end
527
+ elsif ['.', '..'].include?(value)
528
+ raise Google::Apis::ClientError,
529
+ "Invalid value for #{var_name} '#{value}'."
530
+ end
531
+ end
532
+ end
533
+
534
+ # Substitutes percent-encoded values for reserved ({+var}) variables in the template.
535
+ #
536
+ # Addressable leaves reserved characters such as '+', ':', '?' and '#' literal in a
537
+ # reserved expansion, so they would reach the server unencoded. Only '/' and unreserved
538
+ # characters should stay literal, so these values are encoded here and written into the
539
+ # template's literal text, which Addressable passes through untouched when it expands
540
+ # the rest.
541
+ #
542
+ # @param [Addressable::Template] template
543
+ # The URL template.
544
+ # @param [Boolean] normalize_unicode
545
+ # Whether to apply Unicode NFC normalization to the values.
546
+ # @return [Addressable::Template]
547
+ # A template with reserved variables replaced by their encoded values.
548
+ def encode_reserved_parameters(template, normalize_unicode)
549
+ pattern = template.pattern.dup
550
+ template.pattern.scan(TEMPLATE_VAR_PATTERN).each do |operator, var_name|
551
+ next unless operator == '+'
552
+
553
+ var_key = params.key?(var_name) ? var_name : var_name.to_sym
554
+ next unless params.key?(var_key)
555
+
556
+ # The encoded value contains no braces, so the new pattern parses cleanly and the
557
+ # value cannot introduce a template expression of its own.
558
+ pattern = pattern.gsub("{+#{var_name}}", encode_reserved_value(params[var_key], normalize_unicode))
559
+ end
560
+ Addressable::Template.new(pattern)
561
+ end
562
+
563
+ # Percent-encodes every character except '/' and unreserved characters.
564
+ #
565
+ # @param [Object] value
566
+ # The parameter value.
567
+ # @param [Boolean] normalize_unicode
568
+ # Whether to apply Unicode NFC normalization to the value.
569
+ # @return [String]
570
+ # The encoded value.
571
+ def encode_reserved_value(value, normalize_unicode)
572
+ value = value.to_s
573
+ # Addressable only normalizes values left in the mapping, so normalize here to match.
574
+ value = value.unicode_normalize(:nfc) if normalize_unicode
575
+ # Split on '/' and keep empty segments (-1), so slashes stay literal.
576
+ # Each segment is then fully encoded, including '%' itself (as '%25').
577
+ unreserved = Addressable::URI::CharacterClasses::UNRESERVED
578
+ value.split('/', -1).map { |segment| Addressable::URI.encode_component(segment, unreserved) }.join('/')
579
+ end
487
580
  end
488
581
  end
489
582
  end
@@ -16,7 +16,7 @@ module Google
16
16
  module Apis
17
17
  module Core
18
18
  # Core version
19
- VERSION = "1.2.5".freeze
19
+ VERSION = "1.3.0".freeze
20
20
  end
21
21
  end
22
22
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: google-apis-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.5
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Google LLC
@@ -167,7 +167,7 @@ licenses:
167
167
  metadata:
168
168
  bug_tracker_uri: https://github.com/googleapis/google-api-ruby-client/issues
169
169
  changelog_uri: https://github.com/googleapis/google-api-ruby-client/tree/main/google-apis-core/CHANGELOG.md
170
- documentation_uri: https://googleapis.dev/ruby/google-apis-core/v1.2.5
170
+ documentation_uri: https://googleapis.dev/ruby/google-apis-core/v1.3.0
171
171
  source_code_uri: https://github.com/googleapis/google-api-ruby-client/tree/main/google-apis-core
172
172
  rdoc_options: []
173
173
  require_paths: