class Honeybadger::Util::Sanitizer
are based on Rails’ HTTP parameter filter.
Sanitizer sanitizes data for sending to Honeybadger’s API. The filters
def self.sanitize(data)
def self.sanitize(data) @sanitizer ||= new @sanitizer.sanitize(data) end
def basic_object?(object)
def basic_object?(object) object.respond_to?(:to_s) false rescue # BasicObject doesn't respond to `#respond_to?`. true end
def can_dup?(obj)
def can_dup?(obj) !IMMUTABLE.any? { |k| obj.is_a?(k) } end
def filter_cookies(raw_cookies)
def filter_cookies(raw_cookies) return raw_cookies unless filters? cookies = [] raw_cookies.to_s.split(COOKIE_PAIRS).each do |pair| name, values = pair.split(COOKIE_SEP, 2) values = FILTERED if filter_key?(name) cookies << "#{name}=#{values}" end cookies.join(COOKIE_PAIR_SEP) end
def filter_key?(key, parents = nil)
def filter_key?(key, parents = nil) return false unless filters? return true if key.respond_to?(:=~) && regexps.any? { |r| key =~ r } return true if deep_regexps && parents && (joined = parents.join(".")) && deep_regexps.any? { |r| joined =~ r } false end
def filter_url(url)
def filter_url(url) return url unless filters? filtered_url = url.to_s.dup filtered_url.scan(/(?:^|&|\?)([^=?&]+)=([^&]+)/).each do |m| next unless filter_key?(m[0]) filtered_url.gsub!(/#{Regexp.escape(m[1])}/, FILTERED) end filtered_url end
def filters?
def filters? !!@filters end
def initialize(max_depth: 20, filters: [])
def initialize(max_depth: 20, filters: []) @filters = !filters.empty? @max_depth = max_depth strings, @regexps, @blocks = [], [], [] filters.each do |item| case item when Proc @blocks << item when Regexp @regexps << item else strings << Regexp.escape(item.to_s) end end @deep_regexps, @regexps = @regexps.partition { |r| r.to_s.include?('\\.'.freeze) } deep_strings, @strings = strings.partition { |s| s.include?('\\.'.freeze) } @regexps << Regexp.new(strings.join("|".freeze), true) unless strings.empty? @deep_regexps << Regexp.new(deep_strings.join("|".freeze), true) unless deep_strings.empty? end
def inspected?(string)
def inspected?(string) # Ensure string has valid encoding before pattern matching # to avoid ArgumentError with invalid byte sequences string = valid_encoding(string) unless valid_encoding?(string) string =~ /#<.*>/ rescue # If any encoding error occurs, assume it's not inspected false end
def recursive?(data)
def recursive?(data) data.is_a?(Hash) || data.is_a?(Array) || data.is_a?(Set) || data.respond_to?(:to_honeybadger) end
def sanitize(data, depth = 0, stack = nil, parents = [])
def sanitize(data, depth = 0, stack = nil, parents = []) return BASIC_OBJECT if basic_object?(data) if recursive?(data) return RECURSION if stack&.include?(data.object_id) stack = stack ? stack.dup : Set.new stack << data.object_id end case data when Hash return DEPTH if depth >= max_depth hash = data.to_hash new_hash = {} hash.each_pair do |key, value| parents.push(key) if deep_regexps key = key.is_a?(Symbol) ? key : sanitize(key, depth + 1, stack, parents) if filter_key?(key, parents) new_hash[key] = FILTERED else value = sanitize(value, depth + 1, stack, parents) if blocks.any? && !recursive?(value) key = key.dup if can_dup?(key) value = value.dup if can_dup?(value) blocks.each { |b| b.call(key, value) } end new_hash[key] = value end parents.pop if deep_regexps end new_hash when Array, Set return DEPTH if depth >= max_depth data.to_a.map do |value| sanitize(value, depth + 1, stack, parents) end when Numeric, TrueClass, FalseClass, NilClass data when String sanitize_string(data) when ->(d) { d.respond_to?(:to_honeybadger) } return DEPTH if depth >= max_depth begin data = data.to_honeybadger rescue return RAISED end sanitize(data, depth + 1, stack, parents) else # all other objects klass = data.class begin data = String(data) rescue return RAISED end return "#<#{klass.name}>" if inspected?(data) sanitize_string(data) end end
def sanitize_string(string)
def sanitize_string(string) string = valid_encoding(string) return string unless string.respond_to?(:size) && string.size > MAX_STRING_SIZE string[0...MAX_STRING_SIZE] + TRUNCATED end
def valid_encoding(string)
def valid_encoding(string) return string if valid_encoding?(string) string.encode(Encoding::UTF_8, **ENCODE_OPTS) end
def valid_encoding?(string)
def valid_encoding?(string) string.valid_encoding? && ( VALID_ENCODINGS.include?(string.encoding) || VALID_ENCODINGS.include?(Encoding.compatible?("".freeze, string)) ) end