Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 87 additions & 39 deletions google-apis-core/lib/google/apis/core/http_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class HttpCommand
RETRIABLE_ERRORS = [Google::Apis::ServerError,
Google::Apis::RateLimitError,
Google::Apis::TransmissionError,
Google::Apis::RequestTimeOutError]
Google::Apis::RequestTimeOutError].freeze

begin
require 'opencensus'
Expand Down Expand Up @@ -83,15 +83,15 @@ def initialize(method, url, body: nil)
self.url = url
self.url = Addressable::Template.new(url) if url.is_a?(String)
self.method = method
self.header = Hash.new
self.header =({})
self.body = body
self.query = {}
self.params = {}
@opencensus_span = nil
if OPENCENSUS_AVAILABLE
logger.warn 'OpenCensus support is now deprecated. ' +
'Please refer https://github.com/googleapis/google-api-ruby-client#tracing for migrating to use OpenTelemetry.'
'Please refer https://github.com/googleapis/google-api-ruby-client#tracing for migrating to use OpenTelemetry.'

end
end

Expand All @@ -114,8 +114,8 @@ def execute(client, &block)
release!
end

def do_retry func, client
begin
def do_retry(func, client)

Retriable.retriable tries: options.retries + 1,
max_elapsed_time: options.max_elapsed_time,
base_interval: options.base_interval,
Expand All @@ -130,19 +130,17 @@ def do_retry func, client
on: [Google::Apis::AuthorizationError, Signet::AuthorizationError, Signet::RemoteServerError, Signet::UnexpectedStatusError],
on_retry: proc { |*| refresh_authorization } do
send(func, client).tap do |result|
if block_given?
yield result, nil
end
yield result, nil if block_given?
end
end
end
rescue => e
rescue StandardError => e
if block_given?
yield nil, e
else
raise e
end
end

end

# Refresh the authorization authorization after a 401 error
Expand Down Expand Up @@ -171,6 +169,7 @@ def prepare!
query.update(options.query) if options.query
normalize_unicode = options.normalize_unicode
end
validate_path_parameters! if url.is_a?(Addressable::Template)
self.url = url.expand(params, nil, normalize_unicode) if url.is_a?(Addressable::Template)
url.query_values = normalize_query_values(query).merge(url.query_values || {})

Expand All @@ -182,17 +181,16 @@ def prepare!
@form_encoded = false
end

self.body = '' if self.body.nil? && [:post, :put, :patch].include?(method)
self.body = '' if self.body.nil? && %i[post put patch].include?(method)
if defined?(::Google::Apis::Core::CompositeIO) && body.is_a?(::Google::Apis::Core::CompositeIO)
header["Content-Length"] ||= body.size.to_s
header['Content-Length'] ||= body.size.to_s
end
end

# Release any resources used by this command
# @private
# @return [void]
def release!
end
def release!; end

# Check the response and either decode body or raise error
#
Expand Down Expand Up @@ -298,7 +296,7 @@ def error(err, rethrow: false, &block)
err = Google::Apis::TransmissionError.new(err)
end
block.call(nil, err) if block_given?
fail err if rethrow || block.nil?
raise err if rethrow || block.nil?
end

# Execute the command once.
Expand All @@ -323,7 +321,7 @@ def execute_once(client)
logger.debug { safe_single_line_representation @http_res }
response = process_response(@http_res.status.to_i, @http_res.headers, @http_res.body)
success(response)
rescue => e
rescue StandardError => e
logger.debug { sprintf('Caught error %s', e) }
error(e, rethrow: true)
end
Expand All @@ -343,28 +341,29 @@ def apply_request_options(req_header)
end

def allow_form_encoding?
[:post, :put].include?(method) && body.nil?
%i[post put].include?(method) && body.nil?
end

# Set the API version header for the service if not empty.
# @return [void]
def set_api_version_header api_version
def set_api_version_header(api_version)
self.header['X-Goog-Api-Version'] = api_version unless api_version.empty?
end

private

UNSAFE_CLASS_NAMES = [
"Google::Apis::CloudkmsV1::DecryptResponse",
"Google::Apis::SecretmanagerV1::SecretPayload",
"Google::Apis::SecretmanagerV1beta1::SecretPayload"
]
'Google::Apis::CloudkmsV1::DecryptResponse',
'Google::Apis::SecretmanagerV1::SecretPayload',
'Google::Apis::SecretmanagerV1beta1::SecretPayload'
].freeze

module RedactingPPMethods
def pp_object obj
def pp_object(obj)
return super unless UNSAFE_CLASS_NAMES.include? obj.class.name

object_address_group obj do
text "(fields redacted)"
text '(fields redacted)'
end
end
end
Expand All @@ -377,16 +376,16 @@ class RedactingSingleLine < PP::SingleLine
include RedactingPPMethods
end

def safe_pretty_representation obj
out = +""
def safe_pretty_representation(obj)
out = +''
printer = RedactingPP.new out, 79
printer.guard_inspect_key { printer.pp obj }
printer.flush
out << "\n"
end

def safe_single_line_representation obj
out = +""
def safe_single_line_representation(obj)
out = +''
printer = RedactingSingleLine.new out
printer.guard_inspect_key { printer.pp obj }
printer.flush
Expand All @@ -400,18 +399,16 @@ def opencensus_begin_span

@opencensus_span = OpenCensus::Trace.start_span url.path.to_s
@opencensus_span.kind = OpenCensus::Trace::SpanBuilder::CLIENT
@opencensus_span.put_attribute "http.host", url.host.to_s
@opencensus_span.put_attribute "http.method", method.to_s.upcase
@opencensus_span.put_attribute "http.path", url.path.to_s
@opencensus_span.put_attribute 'http.host', url.host.to_s
@opencensus_span.put_attribute 'http.method', method.to_s.upcase
@opencensus_span.put_attribute 'http.path', url.path.to_s
sent_size =
if body.respond_to? :bytesize
body.bytesize
elsif body.nil?
0
end
if sent_size
@opencensus_span.put_message_event OpenCensus::Trace::SpanBuilder::SENT, 1, sent_size
end
@opencensus_span.put_message_event OpenCensus::Trace::SpanBuilder::SENT, 1, sent_size if sent_size

formatter = OpenCensus::Trace.config.http_formatter
if formatter.respond_to? :header_name
Expand All @@ -436,7 +433,7 @@ def opencensus_end_span
status = @http_res.status.to_i
if status > 0
@opencensus_span.set_status map_http_status status
@opencensus_span.put_attribute "http.status_code", status
@opencensus_span.put_attribute 'http.status_code', status
end
end

Expand All @@ -452,7 +449,7 @@ def form_encoded?
@form_encoded
end

def map_http_status http_status
def map_http_status(http_status)
case http_status
when 200..399 then 0 # OK
when 400 then 3 # INVALID_ARGUMENT
Expand All @@ -468,9 +465,8 @@ def map_http_status http_status
end

def normalize_query_values(input)
input.inject({}) do |h, (k, v)|
input.each_with_object({}) do |(k, v), h|
h[k] = normalize_query_value(v)
h
end
end

Expand All @@ -484,6 +480,58 @@ def normalize_query_value(v)
v.to_s
end
end

# Validate path parameters against template to prevent path traversal and injection exploits
# @raise [Google::Apis::Error] if validation fails
def validate_path_parameters!
template_pattern = url.pattern

# Parse variables and operators
variables = []
template_pattern.scan(/\{([\+#\.\/;\?&])?([^}]+)\}/) do |operator, var_list|
var_list.split(',').each do |var|
var_name = var.split(':').first.split('*').first
variables << { name: var_name, operator: operator, reserved: (operator == '+' || operator == '#') }
end
end

variables.each do |v|
var_name = v[:name]
var_key = params.key?(var_name) ? var_name : var_name.to_sym
next unless params.key?(var_key)
value = params[var_key].to_s

if value.include?('?') || value.include?('#')
raise Google::Apis::Error, "Parameter #{var_name} contains invalid characters (? or #)"
end

if v[:reserved]
value_segments = value.split('/', -1)
net_depth = 0

value_segments.each do |seg|
if seg == '..'
if net_depth > 0
net_depth -= 1
else
raise Google::Apis::Error, "Path traversal escape detected in parameter #{var_name}: #{value}"
end
elsif seg == '.' || seg == ''
raise Google::Apis::Error, "Invalid path segment '#{seg}' in parameter #{var_name}"
else
net_depth += 1
end
end
else
if value.include?('/')
raise Google::Apis::Error, "Simple parameter #{var_name} cannot contain slashes: #{value}"
end
if value == '.' || value == '..'
raise Google::Apis::Error, "Simple parameter #{var_name} cannot be '.' or '..': #{value}"
end
end
end
end
end
end
end
Expand Down
Loading
Loading