class Honeybadger::Util::Sanitizer

Sanitizer sanitizes data for sending to Honeybadger’s API. The filters are based on Rails’ HTTP parameter filter.

Constants

BASIC_OBJECT
DEPTH
ENCODE_OPTS
FILTERED
IMMUTABLE
MAX_STRING_SIZE
RAISED
RECURSION
TRUNCATED
VALID_ENCODINGS

Attributes

blocks[R]
deep_regexps[R]
max_depth[R]
regexps[R]

Public Class Methods

new(max_depth: 20, filters: []) click to toggle source
# File lib/honeybadger/util/sanitizer.rb, line 35
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
sanitize(data) click to toggle source
# File lib/honeybadger/util/sanitizer.rb, line 30
def self.sanitize(data)
  @sanitizer ||= new
  @sanitizer.sanitize(data)
end

Public Instance Methods

filter_cookies(raw_cookies) click to toggle source
# File lib/honeybadger/util/sanitizer.rb, line 133
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
filter_url(url) click to toggle source
# File lib/honeybadger/util/sanitizer.rb, line 147
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
sanitize(data, depth = 0, stack = nil, parents = []) click to toggle source
# File lib/honeybadger/util/sanitizer.rb, line 59
def sanitize(data, depth = 0, stack = nil, parents = [])
  return BASIC_OBJECT if basic_object?(data)

  if recursive?(data)
    return RECURSION if stack && 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.kind_of?(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

Private Instance Methods

basic_object?(object) click to toggle source
# File lib/honeybadger/util/sanitizer.rb, line 197
def basic_object?(object)
  object.respond_to?(:to_s)
  false
rescue
  # BasicObject doesn't respond to `#respond_to?`.
  true
end
can_dup?(obj) click to toggle source
# File lib/honeybadger/util/sanitizer.rb, line 205
def can_dup?(obj)
  !IMMUTABLE.any? {|k| obj.kind_of?(k) }
end
filter_key?(key, parents = nil) click to toggle source
# File lib/honeybadger/util/sanitizer.rb, line 168
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
filters?() click to toggle source
# File lib/honeybadger/util/sanitizer.rb, line 164
def filters?
  !!@filters
end
inspected?(string) click to toggle source
# File lib/honeybadger/util/sanitizer.rb, line 209
def inspected?(string)
  String(string) =~ /#<.*>/
end
recursive?(data) click to toggle source
# File lib/honeybadger/util/sanitizer.rb, line 193
def recursive?(data)
  data.is_a?(Hash) || data.is_a?(Array) || data.is_a?(Set) || data.respond_to?(:to_honeybadger)
end
sanitize_string(string) click to toggle source
# File lib/honeybadger/util/sanitizer.rb, line 175
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
valid_encoding(string) click to toggle source
# File lib/honeybadger/util/sanitizer.rb, line 188
def valid_encoding(string)
  return string if valid_encoding?(string)
  string.encode(Encoding::UTF_8, **ENCODE_OPTS)
end
valid_encoding?(string) click to toggle source
# File lib/honeybadger/util/sanitizer.rb, line 181
def valid_encoding?(string)
  string.valid_encoding? && (
    VALID_ENCODINGS.include?(string.encoding) ||
    VALID_ENCODINGS.include?(Encoding.compatible?(''.freeze, string))
  )
end