# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true module Contrast module Utils # used in Contrast::Agent::Request module RequestUtils # TOKENS: NUM_ = '/{n}/' ID_ = '{ID}' # PATTERNS: NUM_PATTERN = %r{/\d+/}.cs__freeze END_PATTERN = %r{/\d+$}.cs__freeze STATIC_SUFFIXES = /\.(?:js|css|jpeg|jpg|gif|png|ico|woff|svg|pdf|eot|ttf|jar)$/i.cs__freeze UUID_PATTERN = Regexp.new('[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}').cs__freeze # rubocop:disable Metrics/LineLength # Regular expression to match any type of hash pattern that is 16 bytes like uuid with no # slashes, md5, sha1, sha256, etc HASH_PATTERN = Regexp.new('([a-fA-F0-9]{2}){16,}').cs__freeze WIN_PATTERN = Regexp.new('S-1-5-((32-\d+)|(21-\d+-\d+-\d+-\d+))').cs__freeze MEDIA_TYPE_MARKERS = %w[image/ text/css text/javascript].cs__freeze # Return a flattened hash of params with realized paths for keys, in # addition to a separate, valueless, entry for each nest key. # See RUBY-621 for more details. # { key : { nested_key : ['x','y','z' ] } } # becomes # { # key[nested_key][0] : 'x' # key[nested_key][1] : 'y' # key[nested_key][2] : 'z' # key : '' # nested_key : '' # } # @return params_hash [Hash] def normalize_params val, prefix: nil # In non-recursive invocations, val should always be a Hash # (rather than breaking this out into two methods) case val when Tempfile # Skip if it's the auto-generated value from rails when it handles # file uploads. The file name will still be sent to SR for analysis. {} when Hash res = val.each_with_object({}) do |(k, v), hash| k = Contrast::Utils::StringUtils.force_utf8(k) nested_prefix = prefix.nil? ? k : "#{ prefix }[#{ k }]" hash[k] = Contrast::Utils::ObjectShare::EMPTY_STRING hash.merge!(normalize_params(v, prefix: nested_prefix)) end res[prefix] = Contrast::Utils::ObjectShare::EMPTY_STRING if prefix res when Enumerable idx = 0 res = {} while idx < val.length res.merge!(normalize_params(val[idx], prefix: "#{ prefix }[#{ idx }]")) idx += 1 end res[prefix] = Contrast::Utils::ObjectShare::EMPTY_STRING if prefix res else { prefix => Contrast::Utils::StringUtils.force_utf8(val) } end end # Read the response body and rewind. # A well behaved middleware would read the IO object and then rewind. # # @return body [String] def read_body body return body if body.is_a?(String) begin can_rewind = Contrast::Utils::DuckUtils.quacks_to?(body, :rewind) # if we are after a middleware that failed to rewind body.rewind if can_rewind body.read rescue StandardError => e logger.error('Error in attempt to read body', message: e.message) logger.trace('With Stack', e) body.to_s ensure # be a good citizen and rewind body.rewind if can_rewind end end # @param multipart_data [Object] # @param current_names [Hash] # @return current_names [Hashfile_name] def traverse_parsed_multipart multipart_data, current_names return current_names unless multipart_data multipart_data.each_value do |data_value| next unless data_value.is_a?(Hash) tempfile = data_value[:tempfile] if tempfile.nil? traverse_parsed_multipart(data_value, current_names) else name = data_value[:name].to_s file_name = data_value[:filename].to_s current_names[name] = file_name end end current_names end end end end